commit 1fe2b563f2b5fbbd3c181923db92074df3f176d0 Author: rohit Date: Mon Jun 23 19:07:37 2025 +0530 DICOM test1 diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..efd3373 --- /dev/null +++ b/.clang-format @@ -0,0 +1,57 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: true +AlignTrailingComments: false +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 200 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ContinuationIndentWidth: 2 +IncludeCategories: + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +InsertNewlineAtEOF: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +TabWidth: 2 +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bc2fda --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Build directories +Build/ +OrthancServer/Build/ +OrthancFramework/Build/ + +# CMake files +CMakeFiles/ + +Makefile + +# Compiled object files +*.o +*.obj +*.so +*.a +*.lib +*.dll +*.exe +*.out + +# OS generated files +.DS_Store +Thumbs.db + +# Editor/IDE files +*.swp +*.swo +*.user +*.workspace +*.idea/ +*.vscode/ + +# Logs +*.log + +# Orthanc storage (if used) +OrthancStorage/ \ No newline at end of file diff --git a/.hg_archival.txt b/.hg_archival.txt new file mode 100644 index 0000000..cefcf33 --- /dev/null +++ b/.hg_archival.txt @@ -0,0 +1,6 @@ +repo: 3959d33612ccaadc0d4d707227fbed09ac35e5fe +node: 0d239fb160606b0007ef41060eed44d7576215c8 +branch: Orthanc-1.12.8 +latesttag: toa2020012703 +latesttagdistance: 1957 +changessincelatesttag: 2494 diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..7156ac6 --- /dev/null +++ b/.hgignore @@ -0,0 +1,18 @@ +syntax: glob +ThirdPartyDownloads/ +CMakeLists.txt.user +*.cpp.orig +*.h.orig +.vs/ +.vscode/ +*~ +*.cmake.orig +.idea/ + +# when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore: +.settings/ +.classpath +.project +Resources/Testing/Issue32/Java/bin +Resources/Testing/Issue32/Java/target +build/ diff --git a/.hgtags b/.hgtags new file mode 100644 index 0000000..5fe9d05 --- /dev/null +++ b/.hgtags @@ -0,0 +1,4 @@ +a95beca72e99f3a1110cffd252bcf3abf5a2db27 dcmtk-3.6.1 +19966d29968506773f90b733b6e34559839ca5c7 toa2020012701 +dfd9a2229c18abd5c794d8fec967ef0ed10b8e91 toa2020012702 +799a8278b151222ea9e8b8628b1d57b5b7943f41 toa2020012703 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..703c043 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,76 @@ +language: cpp + +env: + - TRAVIS_MINGW=OFF + #- TRAVIS_MINGW=ON # MinGW32 is not available anymore (2020-07-08) + +compiler: + - gcc + - clang + +os: + - osx + - linux + +osx_image: xcode61 + +matrix: + exclude: + # This excludes OSX builds from the build matrix for gcc + - os: osx + compiler: gcc + + # Do not compile for OS X or clang when MinGW is enabled + - os: osx + env: TRAVIS_MINGW=ON + - compiler: clang + env: TRAVIS_MINGW=ON + +before_install: + - if [ $TRAVIS_OS_NAME == linux ]; then sudo apt-get update -qq && sudo apt-get install + -qq build-essential unzip cmake mercurial uuid-dev libcurl4-openssl-dev liblua5.1-0-dev + libgtest-dev libpng-dev libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev libwrap0-dev + libcharls-dev; fi + # For DCMTK 3.6.2 - Can't make it compile in static mode with MinGW32 on the + # Ubuntu Precise (12.04) that is used by Travis: + # - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw-w64 gcc-mingw-w64-i686 g++-mingw-w64-i686 wine; fi + + # For DCMTK 3.6.0: + - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw32; fi + +before_script: + - mkdir Build + - cd Build + - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == OFF ]; then cmake + -DCMAKE_BUILD_TYPE=Debug "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog" + -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_BOOST=OFF -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_SYSTEM_JSONCPP=OFF + -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON + -DBOOST_LOCALE_BACKEND=icu -DUSE_SYSTEM_OPENSSL=OFF -DUSE_SYSTEM_CURL=OFF + ../OrthancServer; fi + - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then cmake + -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON + -DCMAKE_TOOLCHAIN_FILE=Resources/MinGWToolchain.cmake -DDCMTK_STATIC_VERSION=3.6.0 + -DUSE_LEGACY_JSONCPP=ON -DBOOST_LOCALE_BACKEND=libiconv + ../OrthancServer; fi + - if [ $TRAVIS_OS_NAME == osx ]; then cmake + -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON + -DBOOST_LOCALE_BACKEND=icu + ../OrthancServer; fi + +# Old releases of MinGW are not compatible with GoogleTest 1.8.1 +script: make Orthanc ServeFolders ModalityWorklists && if [ $TRAVIS_MINGW == OFF ]; then make UnitTests && ./UnitTests; fi + +#script: cp ../README Orthanc +#deploy: +# provider: releases +# api_key: +# secure: WU+niKLAKMoJHST5EK23BayK4qXSrXELKlJYc8wRjMO4ay1KSgvzlY2UGKeW1EPClBfZZ0Uh5VKF8l34exsfirFuwCX2qceozduZproUszZ4Z88X8wt8Ctu8tBuuKLZYFc9iNH4zw+QZyRuPyXK9iWpS0L9O20pqy5upTsagM3o= +# file_glob: true +# file: +# - 'Build/Orthanc' +# - 'Build/UnitTests' +# - 'BuildMinGW32/Orthanc.exe' +# - 'BuildMinGW32/UnitTests.exe' +# skip_cleanup: true +# on: +# all_branches: true diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..66e1770 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,32 @@ +Orthanc - A Lightweight, RESTful DICOM Server +============================================= + + +Authors of Orthanc +------------------ + +* Sebastien Jodogne + + Overall design and lead developer. + +* Department of Medical Physics + University Hospital of Liege + 4000 Liege + Belgium + +* Osimis S.A. + Quai Banning 6 + 4000 Liege + Belgium + +* Orthanc Team SRL + Rue Joseph Marchal 14 + 4910 Theux + Belgium + https://orthanc.team/ + +* ICTEAM, UCLouvain + Place de l'Universite 1 + 1348 Ottignies-Louvain-la-Neuve + Belgium + https://uclouvain.be/icteam diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..f01619c --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,14 @@ +cff-version: "1.1.0" +message: "If you use this software, please cite it using these metadata." +title: Orthanc +abstract: "Orthanc is a lightweight open-source DICOM server for medical imaging supporting representational state transfer (REST)." +authors: + - + affiliation: UCLouvain + family-names: Jodogne + given-names: "Sébastien" +doi: "10.1007/s10278-018-0082-y" +license: "GPL-3.0-or-later" +repository-code: "https://orthanc.uclouvain.be/hg/orthanc/" +version: 1.12.8 +date-released: 2025-06-13 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..10926e8 --- /dev/null +++ b/COPYING @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/DarwinCompilation.txt b/DarwinCompilation.txt new file mode 100644 index 0000000..cbc09f4 --- /dev/null +++ b/DarwinCompilation.txt @@ -0,0 +1,54 @@ +This file is a complement to "INSTALL", which contains instructions +that are specific to Mac OS X (Darwin). + + +Static linking for OS X using XCode +=================================== + +The most simple way of building Orthanc under OS X consists in +statically linking against all the third-party dependencies. In this +case, no package manager such as Homebrew or MacPorts is required. +The build tool (CMake) will download the sources of all the required +packages and automatically compile them. + + +Prerequisites +------------- + +1) XCode must be installed. + +2) CMake must be installed (http://www.cmake.org/). + +3) It is assumed that Orthanc source code is placed in the folder + "~/Orthanc" and that the binaries will be compiled to + "~/Orthanc/Build". + + +Prepare the build with CMake +---------------------------- + +# cd ./Build +# cmake -GXcode -DCMAKE_OSX_DEPLOYMENT_TARGET=10.8 -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON ../OrthancServer + +NB: Adapt the value of "CMAKE_OSX_DEPLOYMENT_TARGET" with respect to +your version of OS X. This version can obtained by typing: + +# sw_vers + + +Build the Debug version of Orthanc +---------------------------------- + +# xcodebuild +# ./Debug/UnitTests + +The binaries of Orthanc are located at "~/Orthanc/Build/Debug/Orthanc". + + +Build the Release version of Orthanc +------------------------------------ + +# xcodebuild -configuration Release +# ./Release/UnitTests + +The binaries of Orthanc are located at "~/Orthanc/Build/Release/Orthanc". diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..27852bb --- /dev/null +++ b/INSTALL @@ -0,0 +1,184 @@ +Orthanc - A Lightweight, RESTful DICOM Server +============================================= + + +Dependencies +------------ + +1) CMake: Orthanc uses CMake (http://www.cmake.org/) to automate its + building process. + +2) Python: Some code is autogenerated through Python + (http://www.python.org/). + +3) Mercurial: To use the cutting edge code, a Mercurial client must be + installed (http://mercurial.selenic.com/). We recommend TortoiseHg. + +W) 7-Zip: For the native build under Windows, the 7-Zip tool is used + to uncompress the third-party packages (http://www.7-zip.org/). + +You thus have to download and install CMake, Python, Mercurial and +possibly 7-Zip first. The path to their executable must be in the +"PATH" environment variable. + +The other third party dependencies are automatically downloaded by the +CMake scripts. The downloaded packages are stored in the +"ThirdPartyDownloads" directory. + + +Building Orthanc at a glance +---------------------------- + +To build Orthanc, you must: + +1) Download the source code (either using Mercurial, or through the + official releases). For the examples below, we assume the source + directory is "~/Orthanc". + +2) Create a build directory. For the examples below, we assume the + build directory is "~/Orthanc/Build". + +3) Depending on your platform, follow the build instructions below. + + +WARNING 1: If you do not create a fresh "~/Orthanc/Build" directory +after upgrading the source code (i.e. if you reuse the build directory +that was used to build a different version of Orthanc), the build +might fail because of changes in the compilation/linking flags. Always +prefer to force a re-build in a new directory. + +WARNING 2: If cmake complains about not being able to uncompress +third-party dependencies, delete the "~/Orthanc/ThirdPartyDownloads/" +folder, then restart cmake. + +WARNING 3: If performance is important to you, make sure to add the +option "-DCMAKE_BUILD_TYPE=Release" when invoking cmake. Indeed, by +default, run-time debug assertions are enabled, which can seriously +impact performance, especially if your Orthanc server stores a lot of +DICOM instances. + + +Native GNU/Linux Compilation +---------------------------- + +See the file "LinuxCompilation.txt". + + +Native OS X Compilation +----------------------- + +See the file "DarwinCompilation.txt". + + + +Native Windows build with Microsoft Visual Studio 2008 +------------------------------------------------------ + +# cd [...]\Orthanc\Build +# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON \ + -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G "Visual Studio 9 2008" [...]\OrthancServer + +Then open the "[...]\Orthanc\Build\Orthanc.sln" with Visual Studio. + +NOTES: +* More recent versions of Visual Studio than 2008 should also + work. Type "cmake" without arguments to have the list of generators + that are available on your computer. +* You will have to install the Platform SDK (version 6 or above) for + Visual Studio 2005: + http://en.wikipedia.org/wiki/Microsoft_Windows_SDK. + Read the CMake FAQ: http://goo.gl/By90B +* The "-DUSE_LEGACY_JSONCPP=ON" must be set for versions of + Visual Studio that do not support C++11 + + +Orthanc as compiled above will not work properly with some Asian +encodings (unit tests will fail). In international setups, you can +compile Orthanc together with ICU as follows: + +# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON \ + -DBOOST_LOCALE_BACKEND=icu -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON \ + -G "Visual Studio 9 2008" [...]\Orthanc + + + +Native Windows build with Microsoft Visual Studio 2015, Ninja and QtCreator +--------------------------------------------------------------------------- + +Open a Visual Studio 2015 x64 Command Prompt. + +# cd [...]\Orthanc\Build +# cmake -G Ninja -DSTATIC_BUILD=ON [...]\OrthancServer +# ninja + +Then, you can open an existing project in QtCreator: +* Select the CMakeLists.txt in [...]\OrthancServer +* Import build from [...]\Build + + +Instructions to include support for Asian encodings: + +# cmake -G Ninja -T host=x64 -DSTATIC_BUILD=ON -DBOOST_LOCALE_BACKEND=icu [...]\OrthancServer + +The option "-T host=x64" is necessary to prevent error "C1060: +compiler is out of heap space" when compiling Orthanc with ICU. + + +Native 64-bit Windows build with Microsoft Visual Studio 2017 (msbuild) +----------------------------------------------------------------------- +# cd [...]\Build +# cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DALLOW_DOWNLOADS=ON [...]\OrthancServer + +Instructions to include support for Asian encodings: +# cmake -G "Visual Studio 15 2017 Win64" -T host=x64 -DSTATIC_BUILD=ON -DBOOST_LOCALE_BACKEND=icu -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DALLOW_DOWNLOADS=ON [...]\OrthancServer + + +Native 64-bit Windows build with Microsoft Visual Studio 2019 (msbuild) +----------------------------------------------------------------------- +# cd [...]\Build +# cmake -G "Visual Studio 16 2019" -A x64 -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DALLOW_DOWNLOADS=ON [...]\OrthancServer + +Instructions to include support for Asian encodings: +# cmake -G "Visual Studio 16 2019" -A x64 -T host=x64 -DSTATIC_BUILD=ON -DBOOST_LOCALE_BACKEND=icu -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DALLOW_DOWNLOADS=ON [...]\OrthancServer + + +Cross-Compilation for Windows under GNU/Linux +--------------------------------------------- + +Some versions of MinGW-W64 may have insufficient support C++11 to +compile recent versions of Boost or ICU (notably those shipped in +Ubuntu 22.04 LTS, in the "g++-mingw-w64-i686-win32" package). Use the +following command to disable C++11 in Boost and ICU: + +# cd ~/Orthanc/Build +# cmake ../OrthancServer \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake \ + -DSTANDALONE_BUILD=ON \ + -DSTATIC_BUILD=ON \ + -DBOOST_LOCALE_BACKEND=icu \ + -DUSE_LEGACY_BOOST=ON \ + -DUSE_LEGACY_LIBICU=ON +# make + +NB: Use the toolchain "MinGW-W64-Toolchain64.cmake" to produce 64bit +Windows binaries. + + + +Legacy MinGW32 compilers (notably those shipped in Ubuntu 14.04 LTS, +in the "mingw32" package) are incompatible with DCMTK 3.6.2 and +C++11. Use the following command to force using DCMTK 3.6.0 and +disable C++11: + +# cd ~/Orthanc/Build +# cmake ../OrthancServer \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake \ + -DSTANDALONE_BUILD=ON \ + -DSTATIC_BUILD=ON \ + -DDCMTK_STATIC_VERSION=3.6.0 \ + -DUSE_LEGACY_JSONCPP=ON \ + -DUSE_LEGACY_BOOST=ON \ + -DUSE_LEGACY_LIBICU=ON +# make diff --git a/LinuxCompilation.txt b/LinuxCompilation.txt new file mode 100644 index 0000000..eed1b7f --- /dev/null +++ b/LinuxCompilation.txt @@ -0,0 +1,270 @@ +This file is a complement to "INSTALL", which contains instructions +that are specific to GNU/Linux. + + +Static linking for GNU/Linux +============================ + +The most simple way of building Orthanc under GNU/Linux consists in +statically linking against all the third-party dependencies. In this +case, the system-wide libraries will not be used. The build tool +(CMake) will download the sources of all the required packages and +automatically compile them. + +This process should work on any GNU/Linux distribution, provided that a +C/C++ compiler ("build-essential" in Debian-based systems), the Python +interpreter, CMake, the "unzip" system tool, and the development +package for libuuid ("uuid-dev" in Debian) are installed. + + +We now make the assumption that Orthanc source code is placed in the +folder "~/Orthanc" and that the binaries will be compiled to +"~/Orthanc/Build". To build binaries with debug information: + +# cd ./Build +# cmake -DSTATIC_BUILD=ON -DCMAKE_BUILD_TYPE=Debug ../OrthancServer/ +# make +# make doc + + +To build a release version: + +# cd ./Build +# cmake -DSTATIC_BUILD=ON -DCMAKE_BUILD_TYPE=Release ../OrthancServer/ +# make +# make doc + + +Note 1- When the "STATIC_BUILD" option is set to "ON", the build tool +will not ask you the permission to download packages from the +Internet. + +Note 2- If the development package of libuuid was not installed when +first invoking cmake, you will have to manually remove the build +directory ("rm -rf ~/Orthanc/Build") after installing this package, +then run cmake again. + +Note 3- To build the documentation, you will have to install doxygen. + + +Use system-wide libraries under GNU/Linux +========================================= + +Under GNU/Linux, by default, Orthanc links against the shared +libraries of your system (the "STATIC_BUILD" option is set to +"OFF"). This greatly speeds up the compilation. This is also required +when building packages for GNU/Linux distributions. Because using +system libraries is the default behavior, you just have to use: + +# cd ./Build +# cmake -DCMAKE_BUILD_TYPE=Debug ../OrthancServer +# make + +Note that to build the documentation, you will have to install doxygen. + +However, on some GNU/Linux distributions, it is still required to +download and static link against some third-party dependencies, +e.g. when the system-wide library is not shipped or is +outdated. Because of difference in the packaging of the various +GNU/Linux distribution, it is also sometimes required to fine-tune +some options. + +You will find below build instructions for specific GNU/Linux +distributions. Distributions tagged by "SUPPORTED" are tested by +Sébastien Jodogne. Distributions tagged by "CONTRIBUTED" come from +Orthanc users. + + +SUPPORTED - Debian Jessie/Sid +----------------------------- + +# sudo apt-get install build-essential unzip cmake mercurial patch \ + uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \ + libgtest-dev libpng-dev libjpeg-dev \ + libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev \ + libboost-all-dev libwrap0-dev libjsoncpp-dev libpugixml-dev + +# cd ./Build +# cmake -DALLOW_DOWNLOADS=ON \ + -DUSE_SYSTEM_CIVETWEB=OFF \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ + -DDCMTK_LIBRARIES=dcmjpls \ + -DCMAKE_BUILD_TYPE=Release \ + ../OrthancServer/ +# make + +Note: Have also a look at the official package: +http://anonscm.debian.org/viewvc/debian-med/trunk/packages/orthanc/trunk/debian/ + + +SUPPORTED - Ubuntu 14.04 LTS +---------------------------- + +# sudo apt-get install build-essential unzip cmake mercurial patch \ + uuid-dev libcurl4-openssl-dev \ + libgtest-dev libpng-dev libsqlite3-dev libssl-dev libjpeg-dev \ + zlib1g-dev libdcmtk2-dev libboost-all-dev libwrap0-dev \ + libcharls-dev libjsoncpp-dev libpugixml-dev + +# cd ./Build +# cmake -DALLOW_DOWNLOADS=ON \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ + -DUSE_SYSTEM_BOOST=OFF \ + -DUSE_SYSTEM_CIVETWEB=OFF \ + -DUSE_SYSTEM_DCMTK=OFF \ + -DUSE_SYSTEM_JSONCPP=OFF \ + -DUSE_SYSTEM_LUA=OFF \ + -DCIVETWEB_OPENSSL_API=1.0 \ + -DCMAKE_BUILD_TYPE=Release \ + ../OrthancServer/ +# make + + +SUPPORTED - Ubuntu 16.04 LTS +---------------------------- + +# sudo apt-get install build-essential unzip cmake mercurial patch \ + uuid-dev libcurl4-openssl-dev liblua5.3-dev \ + libgtest-dev libpng-dev libsqlite3-dev libssl-dev libjpeg-dev \ + zlib1g-dev libdcmtk-dev libboost-all-dev libwrap0-dev \ + libcharls-dev libjsoncpp-dev libpugixml-dev tzdata + +# cd ./Build +# cmake -DALLOW_DOWNLOADS=ON \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ + -DUSE_SYSTEM_CIVETWEB=OFF \ + -DCIVETWEB_OPENSSL_API=1.0 \ + -DDCMTK_LIBRARIES=dcmjpls \ + -DCMAKE_BUILD_TYPE=Release \ + ../OrthancServer/ +# make + + +NB: Instructions to use clang and ninja: + +# sudo apt-get install ninja-build +# cd ./Build +# CC=/usr/bin/clang CXX=/usr/bin/clang++ cmake -G Ninja \ + -DALLOW_DOWNLOADS=ON \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ + -DUSE_SYSTEM_CIVETWEB=OFF \ + -DDCMTK_LIBRARIES=dcmjpls \ + -DCMAKE_BUILD_TYPE=Release \ + ../OrthancServer/ +# ninja + + +SUPPORTED - Ubuntu 18.04 LTS +---------------------------- + +# sudo apt-get install build-essential unzip cmake mercurial patch \ + uuid-dev libcurl4-openssl-dev liblua5.3-dev \ + libgtest-dev libpng-dev libsqlite3-dev libssl-dev libjpeg-dev \ + zlib1g-dev libdcmtk-dev libboost-all-dev libwrap0-dev \ + libcharls-dev libjsoncpp-dev libpugixml-dev locales protobuf-compiler + +# cd ./Build +# cmake -DALLOW_DOWNLOADS=ON \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ + -DUSE_SYSTEM_CIVETWEB=OFF \ + -DDCMTK_LIBRARIES=dcmjpls \ + -DCMAKE_BUILD_TYPE=Release \ + ../OrthancServer/ +# make + + +NB: A suitable environment for locales can be setup as follows: + +# echo "en_US.UTF-8 UTF-8" > /etc/locale.gen +# locale-gen + + + +SUPPORTED - Fedora 20-22 +------------------------ + +# sudo yum install unzip make automake gcc gcc-c++ python cmake \ + boost-devel curl-devel dcmtk-devel \ + gtest-devel libpng-devel libsqlite3x-devel libuuid-devel jpeg-devel \ + mongoose-devel openssl-devel jsoncpp-devel lua-devel pugixml-devel + +You will also have to install "gflags-devel" on Fedora 21&22: + +# sudo yum install gflags-devel + +# cd ./Build +# cmake "-DDCMTK_LIBRARIES=CharLS" \ + -DCIVETWEB_OPENSSL_API=1.0 \ + -DENABLE_CIVETWEB=OFF \ + -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + ../OrthancServer/ +# make + +Note: Have also a look at the official package: +http://pkgs.fedoraproject.org/cgit/orthanc.git/tree/?h=f18 + + + +SUPPORTED - FreeBSD 10.1 +------------------------ + +# pkg install jsoncpp pugixml lua51 curl googletest dcmtk cmake jpeg \ + e2fsprogs-libuuid boost-libs sqlite3 python libiconv + +# cd ./Build +# cmake -DALLOW_DOWNLOADS=ON \ + -DUSE_SYSTEM_CIVETWEB=OFF \ + -DDCMTK_LIBRARIES="dcmdsig;charls;dcmjpls" \ + -DCMAKE_BUILD_TYPE=Release \ + ../OrthancServer/ +# make + + + +Other GNU/Linux distributions? +------------------------------ + +Don't hesitate to send us your build instructions (by a mail to +s.jodogne@orthanc-labs.com)! + +The file "./Resources/OldBuildInstructions.txt" contains build +instructions that once worked for older versions of Orthanc or older +GNU/Linux distributions, but are not tested anymore. Even if they may +not work anymore as such, they can serve as a basis. + +You can find build instructions for Orthanc up to 0.7.0 on the +following Wiki page: +https://orthanc.uclouvain.be/book/faq/compiling-old.html + +These instructions will not work as such beyond Orthanc 0.7.0, but +they might give indications. + + + +Additional information +---------------------- + +* It has been reported that distributions coming with Boost >= 1.70.0 + might need the option "-DBoost_NO_BOOST_CMAKE=ON" to be added to the + "cmake" command line. + https://groups.google.com/d/msg/orthanc-users/nXq2qOndw9c/0PGnaOqiAgAJ + +* Starting with Orthanc 1.10.0, if you use a distribution with an old + version of gcc (typically gcc 4.8 on CentOS), you might have to add + the option "-DCMAKE_CXX_FLAGS=-std=c++11" when invoking the "cmake" + command line. This flag was previously automatically added, but this + feature was removed according to the following discussion: + https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1000222 + + + +Using ccache +============ + +Under GNU/Linux, you also have the opportunity to use "ccache" to +dramatically decrease the compilation time when rebuilding +Orthanc. This is especially useful for developers. To this end, you +would use: + +# CC="ccache gcc" CXX="ccache g++" cmake ../OrthancServer/ [Other Options] diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..61903ac --- /dev/null +++ b/NEWS @@ -0,0 +1,2860 @@ +Pending changes in the mainline +=============================== + + +Version 1.12.8 (2025-06-13) +=========================== + +General +------- + +* The default SQLite database engine now supports metadata and attachment revisions. + +REST API +-------- + +* API version upgraded to 29 +* If the database backend provides the "HasExtendedFind" primitive, the + value "IsProtected" can be included in the "ResponseContent" field of + "/tools/find" to request the "IsProtected" status of patient resources. + +Plugin SDK +---------- + +* Added new functions (available to all plugins) to access key-value + stores and queues stored as a part of the Orthanc database. +* New SDK to create storage area plugins (V3) that associate custom data with + attachments. The built-in SQLite database engine supports such custom data. +* New SDK to handle custom data for attachments, key-value stores, and queues + by custom database backends (cf. "OrthancDatabasePlugin.proto"). +* Added OrthancPluginAdoptDicomInstance() to adopt DICOM instances stored elsewhere + than in the storage area (to be used by "orthanc-advanced-storage" plugin). + +Plugins +------- + +* New sample plugins: "CppSkeleton" and "AdoptDicomInstance" +* Housekeeper plugin: + - If "LimitMainDicomTagsReconstructLevel" was set, files were not transcoded + if they had to. The "LimitMainDicomTagsReconstructLevel" configuration is now + ignored when a full processing is required. +* Delayed Deletion plugin: + - Added an index in the delayed-deletion SQLite external DB to speed up delayed + deletions. This new index will only apply to new databases. If you want to speed + up an existing installation, run "CREATE INDEX PendingIndex ON Pending(uuid)" + manually in the plugin SQLite DB. With this patch, we observed a 100 fold + performance improvement when the "Pending" table contains 1-2 millions files. + Contribution by Yurii (George) from ivtech.dev. + +Maintenance +----------- + +* In verbose logs, the elapsed time spent in each HTTP call is now reported. +* Fix computation of MD5 hashes for memory buffers whose size is larger than 2^31 bytes. +* Configuration options "RejectSopClasses" and "RejectedSopClasses" are taken as synonyms. + In Orthanc 1.12.6 and 1.12.7, "RejectSopClasses" was used instead of the expected + "RejectedSopClasses" spelling. +* Fix the re-encoding of DICOM files larger than 4GB. +* Improved translations of HTTP error codes when a plugin calls the core REST API. + In particular, a plugin could receive an error OrthancPluginErrorCode_UnknownResource (code 17) + when the underlying REST handler was actually returning an HTTP error 415. The plugin will + now receive an error OrthancPluginErrorCode_UnsupportedMediaType (code 3000). + + +Version 1.12.7 (2025-04-07) +=========================== + +REST API +-------- + +* API version upgraded to 28 +* 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 sibling routes now all accept a "filename" GET argument. +* POST "/studies/../archive" and sibling routes now all accept a "Filename" query argument. +* GET "/instances/../file" and sibling "../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 + accept 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. +* GET "/series/../study" now also contain LastUpdate field: + https://discourse.orthanc-server.org/t/lastupdate-coherency/5524 + +Maintenance +----------- + +* In the "ExtendedFind" mode: + - optimized "tools/find" if "StorageAccessMode" is set to "Never". + - "tools/find" now returns results if e.g, ordering instances against a metadata they don't have. + - Get SOPClassUID from metadata if available -> this fixes display of PDF files in Stone if + "StorageAccessOnFind" is set to "Never". +* Fixed OpenAPI documentation for "/modalities/../get". +* Fixed interpretation of "returnUnsupportedImage" in "/preview" route. +* Recovered compatibility with Windows XP that was broken because of DCMTK 3.6.9 +* Enabled support of the 1.2.840.10008.1.2.1.99 transfer syntax + (Deflated Explicit VR Little Endian) in static builds, and 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 + 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. + https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=240 +* Housekeeper plugin: + - If encountering an error, the housekeeper now skips the resource and continues processing. +* Orthanc Explorer: + - Allow "-" and "_" in labels. +* Upgraded dependencies for static builds: + - lua 5.4.7 + + +Version 1.12.6 (2025-01-22) +=========================== + +General +------- + +* DICOM: Added support for C-GET SCU. +* Added new configuration options: + - "AcceptedSopClasses" and "RejectedSopClasses" to limit the SOP classes + accepted by Orthanc when acting as C-STORE SCP. + - "DicomDefaultRetrieveMethod" to define whether Orthanc uses C-MOVE or + C-GET to retrieve a resource after a C-FIND (if calling "/queries/.../retrieve"). + This configuration can be overridden for each DICOM modality by setting + the "RetrieveMethod" field in the "DicomModalities" section. + Default value: "C-MOVE" to preserve backward compatibility. + - "MaximumConcurrentDcmtkTranscoders" to reduce CPU and memory usage by limiting + the number of concurrent DCMTK transcoders that are simultaneously running + at any given time. + +REST API +-------- + +* API version upgraded to 27 +* C-GET SCU requests can be triggered through the new route "/modalities/{id}/get" + +Plugins +------- + +* SDK: Added "OrthancPluginStartStreamAnswer()" and "OrthancPluginSendStreamChunk()" + to allow the sending of HTTP responses by chunks. + +Maintenance +----------- + +* Fix: In the "ExtendedFind" mode, using "tools/find" while querying against + "ModalitiesInStudy" was not compatible with pagination. This notably prevented + the use of the modality filter in Orthanc Explorer 2. +* If the "HttpsCACertificates" configuration is empty, Orthanc now uses the + operating system native CA store (if any). This is equivalent to the "--ca-native" + curl option. +* Housekeeper plugin: + - Fix the "Force" configuration that was ineffective. + - Allow transcoding to lossy transfer syntax. Orthanc will leave + the "SOPInstanceUID" DICOM tag untouched in this case. +* In the "/archive" routes: + - The numbers in the filenames now match the "InstanceNumber" tag whenever possible. + When not, the files are ordered in the same order as the instances in the series. + - Added optimization to use the "ExtendedFind" extension, hereby reducing the number + of SQL queries. +* DICOM negotiation: + - When opening a DICOM SCU connection, Orthanc now only proposes the contexts that it is + going to use in the connection, and not all the contexts as in previous versions + (e.g., if performing a C-ECHO, Orthanc will not propose C-MOVE or C-FIND). +* DICOM C-GET SCP: Orthanc will not refuse anymore to send, for instance, a + LittleEndianExplicit file, if the accepted transfer syntax is a compressed one. +* By default, DCMTK now uses its own "oficonv" library for character set conversion. + This can be tuned using the new CMake option "-DDCMTK_LOCALE_BACKEND=oficonv". +* Improved progress reporting for DicomMoveScu jobs. +* Upgraded dependencies for static builds: + - dcmtk 3.6.9 + + +Version 1.12.5 (2024-12-17) +=========================== + +General +------- + +* Database: + - Introduced the database optimization "ExtendedFind" to replace many small SQL queries + by a single, large SQL query. This can greatly reduce the cost related to latency + when working with large databases (e.g., if using PostgreSQL). + Furthermore, "ExtendedFind" brings new sorting and filtering features to the + REST API, mainly in "/tools/find". + - Introduced the new database primitive "ExtendedChanges" to allow filtering on "/changes". + - Reduced the number of SQL queries when ingesting DICOM files. +* Introduced a new configuration "ReadOnly" to forbid an Orthanc instance to perform + any modification to the index database or to the storage area. + + +REST API +-------- + +* API version upgraded to 26 +* Support HTTP "Range" request header on "{...}/attachments/{...}/data" and + "{...}/attachments/{...}/compressed-data" +* Improved parsing of multiple numerical values in DICOM tags + https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6 +* In "/system", added a new field "Capabilities" with new values: + - "HasExtendedChanges" if the index database provides this optimization + - "HasExtendedFind" if the index database provides this primitive +* If the index database provides the "HasExtendedChanges" primitive, "/changes" + supports two additional arguments: + - "type" to filter the changes returned by the query + - "to" to possibly cycle through changes in reverse order + Example: "/changes?type=StableStudy&to=7584&limit=100" +* If the index database provides the "HasExtendedFind" primitive, "/tools/find" + supports new options: + - "OrderBy" to order by DICOM tag or metadata value + - "ParentPatient", "ParentStudy", and "ParentSeries" to retrieve only descendants of a + given DICOM resource + - "MetadataQuery" to filter results based on metadata values + - "ResponseContent" to define what shall be included in the response for each returned + resource (e.g: Metadata, Children,...) +* In "/tools/find", the "Limit" and "Since" arguments are not allowed anymore if the + query requests filtering on DICOM tags that are not stored in the index database +* The new "/tools/count-resources" API route is similar to "tools/find" but only + returns the number of resources matching the criteria +* "/studies?since=x&limit=0" and similar routes for patients, series, and instances: + "limit=0" now means "no limit" instead of "no results" as in previous versions of Orthanc +* In DICOMweb JSON, the "DS - Decimal String" values were previously represented as float + numbers, but are now represented as strings to avoid introduction of long float representation + (e.g 0.1429999999999 vs "0.143") and to be compliant with the DICOMweb standard: + https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html + This has no impact on the Stone Web viewer and OHIF: + https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195 + +Maintenance +----------- + +* DICOM TLS: "DicomTlsTrustedCertificates" is not required anymore when issuing + an outgoing SCU connection if "DicomTlsRemoteCertificateRequired" is set to "false" +* Fix C-Find queries not returning computed tags such as ModalitiesInStudy, + NumberOfStudyRelatedSeries,... in very specific use cases +* Fix C-Find queries not returning private tags in the modality worklist plugin +* Fix an extremely rare error when 2 threads are trying to create the same folder + in the File Storage at the same time +* Fix crashes if handling very large images +* Fix deadlock when parsing specific invalid DICOM files +* Loading plugins: Orthanc will now fail to start when provided with a plugin path + that can not be found +* Metrics: + - Fix a few metrics that were not published + - Added 2 metrics: "orthanc_storage_cache_miss_count" and "orthanc_storage_cache_hit_count" +* Added a new fallback when trying to decode a frame: transcode the file using the plugin + before decoding the frame. This solves some issues with JP2K Lossy compression: + https://discourse.orthanc-server.org/t/decoding-displaying-jpeg2000-lossy-images/5117 +* Added new warnings that can be disabled in the configuration: + - W003_DecoderFailure + - W004_NoMainDicomTagsSignature + - W005_RequestingTagFromLowerResourceLevel + - W006_RequestingTagFromMetaHeader + - W007_MissingRequestedTagsNotReadFromDisk +* New default MainDicomTags are now stored in the DB: + - At Study Level: + - TimezoneOffsetFromUTC (used in QIDO-RS default queries) + - At Series Level: + - TimezoneOffsetFromUTC (used in QIDO-RS default queries) + - PerformedProcedureStepStartDate (used in QIDO-RS default queries) + - PerformedProcedureStepStartTime (used in QIDO-RS default queries) + - RequestAttributesSequence (used in QIDO-RS default queries) + - Note that, in order to access these values for resources that were ingested in Orthanc + before this release, you will have to run the Housekeeper plugin or to call + "/reconstruct" on every resource +* Upgraded dependencies for static builds: + - boost 1.86.0 + - curl 8.9.0 + - SQLite 3.46 + + +Version 1.12.4 (2024-06-05) +=========================== + +REST API +-------- + +* API version upgraded to 24 +* Added "MaximumPatientCount" in /system +* Added a new "LimitToThisLevelMainDicomTags" field in the payload of + /patients|studies|series/instances/../reconstruct to speed up the reconstruction + in case you just want to update the MainDicomTags of that resource level only + (e.g., after you have updated the "ExtraMainDicomTags" for this level) +* The "requestedTags" GET argument is deprecated in favor of "requested-tags" +* Added "?whole" option to "/instances/{id}/tags" to access tags stored after pixel data + +Plugins +------- + +* Multitenant DICOM plugin: added support for locales. +* Housekeeper plugin: + - Added an option "LimitMainDicomTagsReconstructLevel" + (allowed values: "Patient", "Study", "Series", "Instance"). This can greatly speed + up the housekeeper process, e.g. if you have only updated the Study level ExtraMainDicomTags. + - Fixed broken /instances/../tags route after running the Housekeeper + after having changed the "IngestTranscoding". +* SDK: added OrthancPluginLogMessage() as a new primitive for plugins + to log messages. This new primitive will display the plugin name, + the plugin file name, and the plugin line number in the logs. If + they are not using the LOG() facilities provided by the + OrthancFramework, plugins should now use ORTHANC_PLUGINS_LOG_INFO(), + ORTHANC_PLUGINS_LOG_WARNING(), and ORTHANC_PLUGINS_LOG_ERROR(). + +Maintenance +----------- + +* C-Find queries: + - In C-Find queries including "GenericGroupLength" tags, Orthanc was still + extracting these tags from the storage although they were already ignored + and not returned in the response. + They are now removed from the query earlier to avoid this disk access that + could slow down the response time. Note that this seems to happen mainly + when the query originates from some GE devices (AWS). + - "TimezoneOffsetFromUTC" is now ignored for matching. +* The 0x0111 DIMSE Status is now considered as a warning instead of an error + when received as a response to a C-Store. + See https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3 +* Removed potential PHI from the logs when Orthanc encounters an error while + creating a ZIP archive. +* Monitoring of stable resources now also takes into consideration the + resource type, not only the resource identifier identifier. +* DICOM TLS: + - In prior versions, when "DicomTlsRemoteCertificateRequired" was set to false, Orthanc + was still sending a client certificate request during the TLS handshake but was not + triggering and error if the client certificate was not trusted (equivalent to the + "--verify-peer-cert" DCMTK option). Starting with Orthanc 1.12.4, if this option is + set to "false", Orthanc will not send a client certificate request during the TLS + handshake anymore (equivalent to the "--ignore-peer-cert" DCMTK option). + - When working with "DicomTlsEnabled": true and "DicomTlsRemoteCertificateRequired": false, + Orthanc was refusing to start if no "DicomTlsTrustedCertificates" was provided. + - New configuration options: + - "DicomTlsMinimumProtocolVersion" to select the minimum TLS protocol version + - "DicomTlsCiphersAccepted" to fine tune the list of accepted ciphers +* Fixed broken /instances/../tags route after calling of + /studies/../reconstruct after having changed the "IngestTranscoding". +* Upgraded dependencies for static builds: + - boost 1.85.0 + + +Version 1.12.3 (2024-01-31) +=========================== + +General +------- + +* Performance of databases: + - At startup, if using a database plugin, displays the latency to access the DB. + - Added support for new DB primitives to enable the "READ COMMITTED" + transaction mode in the PostgreSQL plugin. + +REST API +-------- + +* API version upgraded to 23 +* Added a 'KeepLabels' option in /modify routes (default = false) + +Maintenance +----------- + +* Upgraded dependencies for static builds: + - boost 1.84.0 + - curl 8.5.0 + - dcmtk 3.6.8 + - jsoncpp 1.9.5 + - libjpeg 9f + - libpng 1.6.40 + - openssl 3.1.4 + - pugixml 1.14 + - zlib 1.3.1 + + +Version 1.12.2 (2023-12-19) +=========================== + +General +------- + +* Performance: + - Allow multiple plugins to use the plugin SDK at the same time. In previous versions, + functions like instance transcoding or instance reading where mutually exclusive. + This can bring some significant improvements, especially in viewers. + - Optimized the StorageCache to prevent loading the same file multiple times if + multiple users request the same file at the same time. + - The StorageCache is now also storing transcoded instances that have been requested by /file?transcode=... + that is now used by the DICOMweb plugin. This speeds up retrieval of transcoded frames through WADO-RS. + - Now displaying timings when reading from/writing to disk in the verbose logs. +* HTTP compression: + - The default value of the "HttpCompressionEnabled" is now false by default. This reduces + the Orthanc overall CPU usage and latency. This is suitable for setups with large + bandwidth network like LAN. + - When "HttpCompressionEnabled" is true, only the content that is clearly identified as + compressible is compressed (JSON, XML, HTML, text, ...). DICOM files are never + compressed over HTTP. In prior versions, all content types were compressed. + This notably greatly improves loading time of large DICOM + files through WADO-RS e.g in StoneViewer when working on large bandwidth networks. + - When "HttpCompressionEnabled" is true, content < 2KB are never compressed. +* Logs: + - Each line of log now contains the name of the thread that is logging the message. + A new "--logs-no-thread" command line option can be used to get back to the previous behavior to + keep backward compatibility. + +REST API +-------- + +* API version upgraded to 22 +* Added a route to delete completed jobs from history: DELETE /jobs/{id} +* Added a "transcode" option to the /file route: + e.g: /instances/../file?transcode=1.2.840.10008.1.2.4.80 +* now accepting GET requests on these 3 routes to create archive/media: + /tools/create-archive?resources=..,..2&transcode=1.2.840.10008.1.2.4.80 + /tools/create-media?resources=..,..2&transcode=1.2.840.10008.1.2.4.80 + /tools/create-media-extended?resources=..,..2&transcode=1.2.840.10008.1.2.4.80 +* All "expand" GET arguments now accepts "expand=true" and "expand=false" values. + The /studies/../instances and sibling routes are the only whose expand is true if not specified. + These routes now accepts "expand=false" to simply list the child resources ids. +* In /tools/metrics-prometheus: + - "orthanc_dicom_cache_size" renamed as "orthanc_dicom_cache_size_mb" + - added "orthanc_storage_cache_count" and "orthanc_storage_cache_size_mb" + +Plugins +------- + +* Housekeeper plugin: + - Update to rebuild the cache of the DICOMweb plugin when updating to DICOMweb 1.15. + - New trigger configuration: "DicomWebCacheChange" + - Fixed reading the triggers configuration. + - Introduced a "sleep" to lower CPU usage when idle. +* Plugins are now allowed to modify/delete private metadata/attachments + (i.e. whose identifiers are < 1024) +* Added "OrthancPluginSetCurrentThreadName()" in the plugin SDK. + +Maintenance +----------- + +* Fix unit test PngWriter.Color16Pattern on big-endian architectures, + as suggested by Etienne Mollier: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1041813 +* Prevent the leak of the full path of the source files in the binaries +* Fix loading of DCMTK dictionary in the MultitenantDicom plugin when built dynamically: + https://discourse.orthanc-server.org/t/dimse-failure-using-multitenant-plugin/3665 +* Support multiple values in SpecificCharacterSet in C-Find answers: + https://discourse.orthanc-server.org/t/c-find-fails-on-unknown-specific-character-set-iso-2022-ir-6-iso-2022-ir-100/3947 +* When exporting a study archive, make sure to use the PatientName from the study and not from the patient + in case of PatientID collision. +* DICOM C-Store: + - Avoid some unnecessary renegotiation of DICOM association. + - Force renegotiation in case no presentation context were accepted in previous association (we have + observed PACS that were not consistent in the accepted presentation contexts) + - Improved logging +* Solved a deadlock related to the Job Engine events and plugins. Job events are now pushed + into a queue to be handled asynchronously by plugins. +* ZIP of studies whose PatientName and PatientID did not contain any ASCII character are now valid. +* Upgraded minizip library to stay away from CVE-2023-45853 although Orthanc is likely not affected since ZIP + filenames are based on DICOM Tag values whose length is limited in size. + Great thanks to James Addison for notifying us about the vulnerability and patch to apply ! +* Fix XSS in Orthanc error reporting (as reported by Sébastien Doria, Vumetric Cybersecurity) by: + - always including a "Content-Type" header in HTTP responses with a body. + - always including "X-Content-Type-Options: nosniff" +* Upgraded dependencies for static builds: + - boost 1.83.0 + + +Version 1.12.1 (2023-07-04) +=========================== + +General +------- + +* Orthanc now anonymizes according to Basic Profile of PS 3.15-2023b Table E.1-1 +* Added metrics: + - "orthanc_storage_read_bytes" + - "orthanc_storage_written_bytes" + - "orthanc_memory_trimming_duration_ms" + +REST API +-------- + +* API version upgraded to 21 +* "/tools/create-dicom" can now be used to create Encapsulated 3D + Manufacturing Model IODs (MTL, OBJ, or STL) +* Added a route to delete the output of an asynchronous job (right now + only for archive jobs): e.g. DELETE /jobs/../archive + +Plugins +------- + +* Added "OrthancPluginLoadDicomInstance()" to load DICOM instances from the database +* Added "OrthancPluginSetMetricsIntegerValue()" to track metrics with integer values + +Maintenance +----------- + +* Fix decoding of YBR_FULL RLE images for which the "Planar Configuration" + tag (0028,0006) equals 1 +* Made Orthanc more resilient to common spelling errors in SpecificCharacterSet +* Modality worklists plugin: Allow searching on private tags (exact match only) +* Fix orphan files remaining in storage when working with MaximumStorageSize + (https://discourse.orthanc-server.org/t/issue-with-deleting-incoming-dicoms-when-maximumstoragesize-is-reached/3510) +* When deleting a resource, the "LastUpdate" metadata of its parents are now updated +* Reduced the memory usage when downloading archives when "ZipLoaderThreads" > 0 +* Metrics can be stored either as floating-point numbers, or as integers +* Reduce the frequency of memory trimming from 100ms to 30s to avoid high idle + CPU load (https://discourse.orthanc-server.org/t/onchange-callbacks-and-cpu-loads/3534). +* Upgraded dependencies for static builds: + - boost 1.82.0 + + +Version 1.12.0 (2023-04-14) +=========================== + +General +------- + +* Support for labels associated with patients, studies, series, and instances +* Added a sample plugin bringing multitenant DICOM support through labels + +REST API +-------- + +* API version upgraded to 20 +* New URIs "/.../{id}/labels/{label}" to test/set/remove labels +* "/patients/{id}", "/studies/{id}", "/series/{id}" and "/instances/{id}" + contain the "Labels" field +* "/tools/find" now accepts the "Labels" and "LabelsConstraint" arguments +* "/tools/labels" lists all the labels that are associated with any resource +* "/system": added "UserMetadata" and "HasLabels" +* Added option "?numeric" if listing metadata + +Plugins +------- + +* Added "OrthancPluginRegisterDatabaseBackendV4()" to communicate using Google + Protocol Buffers between the Orthanc core and database plugins + +Orthanc Explorer +---------------- + +* Added support for labels +* Added buttons to copy the URL of ZIP archives and DICOM files to the clipboard + +Maintenance +----------- + +* Enforce the existence of the patient/study/instance while creating its archive +* Security: New configuration option "RestApiWriteToFileSystemEnabled" + to allow "/instances/../export" (the latter is now disabled by default) +* Fix issue 214: VOILUTSequence is not returned in Wado-RS +* Fix /tools/reset crashing when ExtraMainDicomTags were defined +* Fix Housekeeper plugin infinite loop if Orthanc is empty. +* Fix a crash in /tools/reconstruct triggered by the Housekeeper plugin + when only changing the StorageCompression. +* Avoid the use of "externalproject_add()" to build the sample plugins +* Upgraded dependencies for static builds: + - openssl 3.1.0 + + +Version 1.11.3 (2023-02-03) +=========================== + +General +------- + +* C-Store SCU now gives priority to the preferred TransferSyntax + proposed by the receiving SCP instead of Orthanc own + AcceptedTransferSyntaxes. +* Made the default SQLite DB more robust wrt future updates like + adding new columns in DB. +* Made the HTTP Client errors more verbose by including the URL in the logs. +* Optimization: now using multiple threads to transcode files for + asynchronous download of studies archive. +* New configuration "KeepAliveTimeout" with a default value of 1 second. +* ResourceModification jobs (/modify + /anonymize) can now use multiple threads to speed up processing + - New configuration "JobsEngineThreadsCount.ResourceModification" to configure the number of threads. +* For systems using glibc > 2.8 (most of Linux systems except LSB + binaries): Introduced a new thread for to trim memory in Orthanc (different + from the Housekeeper sample plugin). This thread regularly try to + give back memory that Orthanc no longer uses to the system. This + reduces the overall memory consumption. More information in + OrthancServer/Resources/ImplementationNotes/memory_consumption.txt. + +REST API +-------- + +* API version upgraded to 19 +* Loosen the sanity checks for DICOM modifications, if "Force" option is given: + - allow modification of PatientID at study level + - allow modification of PatientID, StudyInstanceUID at series level + - allow modification of PatientID, StudyInstanceUID, SeriesInstanceUID at instance level + - allow modification of a patient without changing her PatientID + Added sanity checks for modifications to make sure the user preserves the DICOM model when modifying high level tags. + E.g. if you modify the PatientID at study level, also make sure to modify all other Patient related + tags (PatientName, PatientBirthDate, ...) +* Automatically reconstruct the modified resources at the end of the DICOM modifications job to ensure + improved consistency of the DICOM model. +* If specifying 'Transcode' option to /modify or /anonymize, this value will take over the 'IngestTranscoding' + global configuration +* Allow the HTTP server to return responses > 2GB (fixes asynchronous download of zip studies > 2GB) +* /modalities/.../store now accepts "CalledAet", "Host", "Port" to override the modality configuration + from the configuration file for a specific operation. +* /tools/metrics-prometheus: added orthanc_last_change and orthanc_up_time_s +* Tolerance for "image/jpg" MIME type instead of "image/jpeg" in /tools/create-dicom +* /system: added MaximumStorageMode and MaximumStorageSize + +Plugins +------- + +* Added a "header" argument to all OrthancPeers::DoPost, DoPut, ... in the "OrthancPluginCppWrapper" +* Added "OrthancPluginCreateJob2()" in the plugin SDK to avoid + possible crashes when "OrthancPluginJobGetContent()" or + "OrthancPluginJobGetSerialized()" get called + +Maintenance +----------- + +* Fix decoding of RLE images for which the "Planar Configuration" tag (0028,0006) equals 1 +* Fix issue #212 (Anonymization process transcodes data and loses resource link). + + +Version 1.11.2 (2022-08-30) +=========================== + +General +------- + +* Added support for RGBA64 images in tools/create-dicom and /preview +* New configuration "MaximumStorageMode" to choose between recyling of + old patients (default behavior) and rejection of new incoming data when + the MaximumStorageSize has been reached. + +Bug Fixes +--------- + +* Fix the "Never" option of the "StorageAccessOnFind" that was sill accessing + files (bug introduced in 1.11.0). +* Fix the Storage Cache for compressed files (bug introduced in 1.11.1). + +Maintenance +----------- + +* DelayedDeletion plugin: Fix leaking of symbols +* SQLite now closes and deletes WAL and SHM files on exit. This should improve + handling of SQLite DB over network drives. +* Fix static compilation of boost 1.69 on Ubuntu 22.04 +* Upgraded dependencies for static builds: + - boost 1.80.0 + - dcmtk 3.6.7 (fixes CVE-2022-2119 and CVE-2022-2120) + - openssl 3.0.5 + + +Version 1.11.1 (2022-06-30) +=========================== + +General +------- + +* New sample plugin: "DelayedDeletion" that will delete files from disk + asynchronously to speed up deletion of large studies. +* Lua: new "SetHttpTimeout" function +* Lua: new "OnHeartBeat" callback called at regular interval provided that + you have configured "LuaHeartBeatPeriod" > 0. +* "ExtraMainDicomTags" configuration now accepts Dicom Sequences. Sequences are + stored in a dedicated new metadata "MainDicomSequences". This should improve + DicomWeb QIDO-RS and avoid warnings like "Accessing Dicom tags from storage when + accessing series : 0040,0275". + Main dicom sequences can now be returned in "MainDicomTags" and in "RequestedTags". + +Bug Fixes +--------- + +* Fix the storage cache that was not used by the Plugin SDK. This fixes the + DicomWeb plugin "/rendered" route performance issues. + +Maintenance +----------- + +* Housekeeper plugin: Fix resume of previous processing +* Added missing MOVEPatientRootQueryRetrieveInformationModel in + DicomControlUserConnection::SetupPresentationContexts() +* Improved HttpClient error logging (add method + URL) + +REST API +-------- + +* API version upgraded to 18 +* /system is now reporting "DatabaseServerIdentifier" +* Added an Asynchronous mode to /modalities/../move. +* "RequestedTags" option can now include DICOM sequences. + +Plugins +------- + +* New function in the SDK: "OrthancPluginGetDatabaseServerIdentifier" + +OrthancFramework (C++) +---------------------- + +* DicomMap::ParseMainDicomTags has been deprecated -> retrieve "full" tags + and use DicomMap::FromDicomAsJson instead + + +Version 1.11.0 (2022-05-09) +=========================== + +General +------- + +* New configuration "ExtraMainDicomTags" to store more tags in the Index DB + to speed up, e.g, building C-Find, dicom-web or tools/find answers +* New sample plugin: "Housekeeper" that will re-construct the DB/Storage + when it detects there is room for improvements, e.g: + - if files were stored with a version of Orthanc prior to 1.9.1, + the storage might still contain dicom-as-json files that are not needed + anymore -> it will remove them + - if "ExtraMainDicomTags" has changed. + - if "StorageCompression" or "IngestTranscoding" has chagned. +* New configuration "Warnings" to enable/disable individual warnings that can + be identified by a W0XX prefix in the logs. + These warnings have been added: + - W001_TagsBeingReadFromStorage + - W002_InconsistentDicomTagsInDb +* C-Find and QIDO-RS can now return the InstanceAvailability tag. Value is + always "ONLINE" +* Improved decoding of US Images with Implicit VR. +* Speed-up handling of DicomModalitiesInStudy in C-Find and tools/find queries. + +REST API +-------- + +* API version upgraded to 17 +* new options in tools/find: + - "RequestedTags" (to use together with "Expand": true) contains a list of tags + that you'll receive in the "RequestedTags" field in the answers. These tags + may be tags from the MainDicomTags in DB, from the DICOM file or 'computed' + like ModalitiesInStudy. Check the new configuration "ExtraMainDicomTags" and + "Warnings" to optimize your queries. +* new query argument "requestedTags" in all API routes listing resources: + - /patients, /patients/../studies, /patients/../series, /patients/../instances + - /studies, /studies/../series, /studies/../instances + - /series, /series/../instances + - /instances + ex: + - /studies/c27857df-4078c84c-1a79ea78-ac357bb2-9dadc119?requestedTags=ModalitiesInStudy + - /studies?expand&since=0&limit=10&requestedTags=ModalitiesInStudy + +* /reconstruct routes: + - new options "ReconstructFiles" (false by default to keep backward compatibility) to + potentialy compress/uncompress the files or transcode them if "StorageCompression" + or "IngestTranscoding" has changed since the file has been ingested. + POSSIBLE BREAKING-CHANGES: + - the /reconstruct routes now preserve all metadata + - the /reconstruct routes now skip the IncomingInstanceFilter + - the /reconstruct routes won't generate new events like NewStudy, StableStudy, ... + therefore, the corresponding callbacks won't be called anymore + - the /reconstruct routes won't affect the patient recycling anymore +* new fields reported in the /system route: + - "MainDicomTags" to list the tags that are saved in DB + - "StorageCompression", "OverwriteInstances", "IngestTranscoding" reported from the + configuration file +* New option "filename" in "/.../{id}/archive" and "/.../{id}/media" to + manually set the filename in the "Content-Disposition" HTTP header + + +Version 1.10.1 (2022-03-23) +=========================== + +General +------- + +* Improved DICOM authorization checks when multiple modalities are + declared with the same AET. + +Plugins +------- + +* New function in the SDK: "OrthancPluginRegisterWebDavCollection()" + to map a WebDAV virtual filesystem into the REST API of Orthanc. + +Documentation +------------- + +* Removed the "LimitJobs" configuration that is not used anymore since + the new JobEngine has been introduced (in Orthanc 1.4.0). The + pending list of jobs is unlimited. + + +Version 1.10.0 (2022-02-23) +=========================== + +General +------- + +* New configuration "DicomAlwaysAllowFindWorklist" to complement the existing + "DicomAlwaysAllowFind" configuration. "DicomAlwaysAllowFind" applies now + only to C-Find for Patients/Studies/Series/Instances while C-Find for worklists are + covered by "DicomAlwaysAllowFindWorklist". The same changes applies to new + configurations in "DicomModalities": "AllowFind" is now complemented by + "AllowFindWorklist". + This new option allows improved security management. E.g: a modality might have + only "AllowStore" and "AllowFindWorklist" enabled but might have "AllowFind" + disabled to prevent listing past patient studies. + Possible BREAKING-CHANGE: if you relied on "DicomAlwaysAllowFind" or "AllowFind" + to specifically authorize C-Find for worklist, you now need to explicitly enable + "DicomAlwaysAllowFindWorklist" and/or "AllowFindWorklist" +* Added a storage cache in RAM to avoid reading the same files multiple times from + the storage. This greatly improves, among other things, the performance of WADO-RS + retrieval of individual frames of multiframe instances. +* New configuration option "MaximumStorageCacheSize" to configure the size of + the new storage cache. +* New experimental configuration option "ZipLoaderThreads" to configure the number of + threads used to read instances from storage when creating a Zip archive/media. +* Support decoding of black-and-white images (with 1 bit per pixel), notably DICOM SEG +* Added links to download attachments from the Orthanc Explorer +* Fix XSS inside DICOM in Orthanc Explorer (as reported by Stuart Kurutac, NCC Group). + XSS Issues were re-introduced in Orthanc 1.9.4. + +REST API +-------- + +* API version upgraded to 16 +* If an image can not be decoded, "../preview" and "../rendered" routes + are now returning "unsupported.png" only if the + "?returnUnsupportedImage" option is specified; otherwise, it raises + a 415 HTTP error code. +* Archive jobs response now contains a header Content-Disposition:filename='archive.zip' +* "/instances/{...}/frames/{...}/numpy": Download the frame as a Python numpy array +* "/instances/{...}/numpy": Download the instance as a Python numpy array +* "/series/{...}/numpy": Download the series as a Python numpy array +* Added a "?full" option to "/patients|studies|series|instances/{...}/attachments" route + to show the mapping alias<->numerical id. +* Added "/patients|studies|series|instances/{...}/attachments/{...}/info" route to retrieve + the full information about an attachment (size, type, MD5 and UUID) + +Lua +--- + +* New "ReceivedCStoreInstanceFilter" Lua callback to filter instances received + through C-Store and return a specific C-Store status code. + +Plugins +------- + +* New functions in the SDK: + - OrthancPluginRegisterIncomingCStoreInstanceFilter() + - OrthancPluginRegisterReceivedInstanceCallback() + +Maintenance +----------- + +* Removed the OpenSSL license exception, as binary versions of Orthanc are now + designed to use OpenSSL 3.x, that was re-licensed under Apache 2.0, making + it compatible with the GPL/AGPL licenses used by the Orthanc project: + https://en.wikipedia.org/wiki/OpenSSL#Licensing + https://people.gnome.org/~markmc/openssl-and-the-gpl.html +* Fix handling of option "DeidentifyLogs", notably for tags (0010,0010) and (0010,0020) +* New configuration options: + - "DicomThreadsCount" to set the number of threads in the embedded DICOM server +* Fix instances accumulating in DB while their attachments were not stored because of + MaximumStorageSize limit reached with a single patient in DB. +* Dropped support for static compilation of OpenSSL 1.0.2 +* Upgraded dependencies for static builds (notably on Windows and LSB): + - openssl 3.0.1 + + +Version 1.9.7 (2021-08-31) +========================== + +General +------- + +* New configuration option "DicomAlwaysAllowMove" to disable verification of + the remote modality in C-MOVE SCP + +REST API +-------- + +* API version upgraded to 15 +* Added "Level" option to POST /tools/bulk-modify +* Added missing OpenAPI documentation of "KeepSource" in ".../modify" and ".../anonymize" + +Maintenance +----------- + +* Added file CITATION.cff +* Linux Standard Base (LSB) builds of Orthanc can load non-LSB builds of plugins +* Fix upload of ZIP archives containing a DICOMDIR file +* Fix computation of the estimated time of arrival in jobs +* Support detection of windowing and rescale in Philips multiframe images + + +Version 1.9.6 (2021-07-21) +========================== + +Orthanc Explorer +---------------- + +* In lookup and query/retrieve, possibility to provide a specific study date +* Clicking on "Send to remote modality" displays the job information to monitor progress + +Maintenance +----------- + +* Fix orphaned attachments if bad revision number is provided + + +Version 1.9.5 (2021-07-08) +========================== + +General +------- + +* Anonymization is applied recursively to nested tags + +REST API +-------- + +* API version upgraded to 14 +* Added "Short", "Simplify" and/or "Full" options to control the format of DICOM tags in: + - POST /modalities/{id}/find-worklist + - POST /queries/{id}/answers/{index}/retrieve + - POST /queries/{id}/retrieve + +Maintenance +----------- + +* Fix broken "Do lookup" button in Orthanc Explorer +* Error code and description of jobs are now saved into the Orthanc database + + +Version 1.9.4 (2021-06-24) +========================== + +General +------- + +* Orthanc now anonymizes according to Basic Profile of PS 3.15-2021b Table E.1-1 +* New configuration options: + - "ExternalDictionaries" to load external DICOM dictionaries (useful for DICONDE) + - "SynchronousZipStream" to disable streaming of ZIP + +Orthanc Explorer +---------------- + +* Orthanc Explorer supports the DICONDE dictionary + +REST API +-------- + +* API version upgraded to 13 +* New routes to handle groups of multiple, unrelated DICOM resources at once: + - "/tools/bulk-anonymize" to anonymize a set of resources + - "/tools/bulk-content" to get the content of a set of resources + - "/tools/bulk-delete" to delete a set of resources + - "/tools/bulk-modify" to modify a set of resources +* ZIP archive/media generated in synchronous mode are now streamed by default +* "Replace" tags in "/modify" and "/anonymize" now supports value representation AT +* "/jobs/..." has new field "ErrorDetails" to help identify the cause of an error +* "Replace", "Keep" and "Remove" in "/modify" and "/anonymize" accept paths to subsequences + using the syntax of the dcmodify command-line tool (wildcards are supported as well) +* Added "short", "simplify" and/or "full" options to control the format of DICOM tags in: + - GET /patients, GET /studies, GET /series, GET /instances (together with "&expand") + - GET /patients/{id}, GET /studies/{id}, GET /series/{id}, GET /instances/{id} + - GET /patients/{id}/studies, GET /patients/{id}/series, GET /patients/{id}/instances + - GET /studies/{id}/patient, GET /studies/{id}/series, GET /studies/{id}/instances + - GET /series/{id}/patient, GET /series/{id}/study, GET /series/{id}/instances + - GET /instances/{id}/patient, GET /instances/{id}/study, GET /instances/{id}/series + - GET /patients/{id}/instances-tags, GET /patients/{id}/shared-tags + - GET /studies/{id}/instances-tags, GET /series/{id}/shared-tags + - GET /series/{id}/instances-tags, GET /studies/{id}/shared-tags + - GET /patients/{id}/module, GET /patients/{id}/patient-module + - GET /series/{id}/module, GET /studies/{id}/module, GET /instances/{id}/module + - GET /queries/{id}/answers&expand, GET /queries/{id}/answers/{index}/content + - POST /tools/find +* "/studies/{id}/split" accepts "Instances" parameter to split instances instead of series +* "/studies/{id}/merge" accepts instances inside its "Resources" parameter + +Maintenance +----------- + +* Full support of hierarchical relationships in tags whose VR is UI during anonymization +* C-MOVE SCP: added possible DIMSE status "Sub-operations Complete - One or more Failures" +* Fix issue #146 (Update Anonyization to 2019c) - was actually updated to 2021b +* Upgraded dependencies for static builds (notably on Windows): + - curl 7.77.0 + + +Version 1.9.3 (2021-05-07) +========================== + +General +------- + +* New configuration option: "DicomTlsRemoteCertificateRequired" to allow secure DICOM TLS + connections without certificate + +REST API +-------- + +* "ETag" headers for metadata and attachments now allow strong comparison (MD5 is included) + +Maintenance +----------- + +* New CMake option: "ORTHANC_LUA_VERSION" to use a specific version of system-wide Lua +* Fix the lifetime of temporary files associated with jobs that create ZIP archive/media: + - In synchronous mode, their number could grow up to "JobsHistorySize" in Orthanc <= 1.9.2 + - In asynchronous mode, the temporary files are removed as soon as their job gets canceled +* Fix regression in the handling of "DicomCheckModalityHost" configuration option + introduced by changeset 4182 in Orthanc 1.7.4 +* Reduced memory consumption of "OrthancPluginHttpClient()", "OrthancPluginHttpClient2()" and + "OrthancPluginCallPeerApi()" on POST/PUT if chunked transfer is disabled +* Fix issue #195 (No need for BulkDataURI when Data Element is empty) + + +Version 1.9.2 (2021-04-22) +========================== + +General +------- + +* New configuration options related to multiple readers/writers: + - "DatabaseServerIdentifier" identifies the server in the DB among a pool of Orthanc servers + - "CheckRevisions" to protect against concurrent modifications of metadata and attachments + +REST API +-------- + +* API version upgraded to 12 +* "/system" reports the value of the "CheckRevisions" global option +* "/.../{id}/metadata/{name}" and "/.../{id}/attachments/{name}/..." URIs handle the + HTTP headers "If-Match", "If-None-Match" and "ETag" to cope with revisions + +Plugins +------- + +* New function in the SDK: OrthancPluginCallRestApi() +* Full refactoring of the database plugin SDK to handle multiple readers/writers, + which notably implies the handling of retries in the case of collisions + +Maintenance +----------- + +* Use the local timezone for query/retrieve in the Orthanc Explorer interface (was UTC before) +* Fix "OrthancServer/Resources/Samples/Python/Replicate.py" for Python 3.x +* Fix issue #83 (ServerIndex shall implement retries for DB temporary errors) +* Upgraded dependencies for static builds (notably on Windows and LSB): + - civetweb 1.14 + - openssl 1.1.1k + + +Version 1.9.1 (2021-02-25) +========================== + +General +------- + +* The "dicom-as-json" attachments are not explicitly stored anymore to improve performance +* If the storage area doesn't support range reading, or if "StorageCompression" + is enabled, a new type of attachment "dicom-until-pixel-data" is generated +* New metadata automatically computed at the instance level: "PixelDataOffset" +* New configuration option related to networking: + - "Timeout" in "DicomModalities" to set DICOM SCU timeout on a per-modality basis + - "Timeout" in "OrthancPeers" to set HTTP client timeout on a per-peer basis + +REST API +-------- + +* API version upgraded to 11 +* BREAKING CHANGES: + - External applications should not call "/instances/.../attachments/dicom-as-json" anymore, + and should use "/instances/.../tags" instead + - "/instances/.../tags" route does not report the tags after "Pixel Data" (7fe0,0010) anymore +* "/peers/{id}/store-straight": Synchronously send the DICOM instance in POST body to the peer +* New arguments in the REST API: + - "Timeout" in "/modalities/.../query" + - "Timeout" in "/modalities/.../storage-commitment" + - "Timeout" in "/queries/.../answers/.../query-{studies|series|instances}" + +Plugins +------- + +* New value in enumeration: OrthancPluginDicomToJsonFlags_StopAfterPixelData +* New value in enumeration: OrthancPluginDicomToJsonFlags_SkipGroupLengths + +Maintenance +----------- + +* Improved precision of floating-point numbers in DICOM-as-JSON and DICOM summary +* Optimization in C-STORE SCP by avoiding an unnecessary DICOM parsing +* Fix build on big-endian architectures +* Handle public tags with "UN" value representation and containing a string (cf. DICOM CP 246) +* The numbering of sequences in Orthanc Explorer now uses the DICOM convention (starts at 1) +* Possibility to generate a static library containing the Orthanc Framework + + +Version 1.9.0 (2021-01-29) +========================== + +General +------- + +* Support of DICOM TLS +* New configuration options related to DICOM networking: + - "DicomTlsEnabled" to enable DICOM TLS in Orthanc SCP + - "DicomTlsCertificate" to provide the TLS certificate to be used in both Orthanc SCU and SCP + - "DicomTlsPrivateKey" to provide the private key of the TLS certificate + - "DicomTlsTrustedCertificates" to provide the list of TLS certificates to be trusted by Orthanc + - "UseDicomTls" in "DicomModalities" to enable DICOM TLS in outgoing SCU on a per-modality basis + - "MaximumPduLength" to tune the maximum PDU length (Protocol Data Unit) + - "LocalAet" in "DicomModalities" to overwrite global "DicomAet" for SCU on a per-modality basis + - "AcceptedTransferSyntaxes" to set the transfer syntax UIDs accepted by Orthanc C-STORE SCP + - "H265TransferSyntaxAccepted" to enable/disable all the transfer syntaxes related to H.265 + - "DicomAlwaysAllowFind" to disable verification of the remote modality in C-FIND SCP + - "DicomAlwaysAllowGet" to disable verification of the remote modality in C-GET SCP +* New configuration option: "DicomScuPreferredTransferSyntax" to control transcoding in C-STORE SCU +* New command-line option: "--openapi" to write the OpenAPI documentation of the REST API to a file +* New metadata automatically computed at the series level: "RemoteAET" + +Orthanc Explorer +---------------- + +* The DICOM meta-header and the transfer syntax are displayed at the "Instance" level + +REST API +-------- + +* API version upgraded to 10 +* "/tools/accepted-transfer-syntaxes": Get/set transfer syntaxes accepted by Orthanc C-STORE SCP +* "/tools/unknown-sop-class-accepted": Get/set whether C-STORE SCP accepts unknown SOP class UID +* "/modalities/{...}/query": New string argument "LocalAet" +* "/tools/create-dicom": New flag "Force" to bypass consistency checks for the DICOM tags + +Lua +--- + +* BREAKING CHANGE: All the Lua callbacks "IsXXXTransferSyntaxAccepted()" and + "IsUnknownSopClassAccepted()" have been removed + +Plugins +------- + +* New functions in the SDK: + - OrthancPluginCreateMemoryBuffer64() + - OrthancPluginRegisterStorageArea2() + - OrthancPluginCreateDicom2() + +Maintenance +----------- + +* Refactoring and improvements to the cache of DICOM files (it can now hold many files) +* New Prometheus metrics "orthanc_dicom_cache_count" and "orthanc_dicom_cache_size" +* Fix upload of multiple DICOM files using one single POST call to "multipart/form-data" + Could be the final resolution of issue #21 (DICOM files missing after uploading with Firefox) +* Partial fix of issue #48 (Windows service not stopped properly), cf. comments 4 and 5 +* Explicitly use little-endian to encode uncompressed file size with zlib compression +* Upgraded dependencies for static builds (notably on Windows): + - dcmtk 3.6.6 + - jsoncpp 1.9.4 + + +Version 1.8.2 (2020-12-18) +========================== + +General +------- + +* ZIP archives containing DICOM files can be uploaded using WebDAV +* New config option "MallocArenaMax" to control memory usage on GNU/Linux +* Explicit error log if trying to load a 32bit (resp. 64bit) plugin into + a 64bit (resp. 32bit) version of Orthanc +* New configuration options contributed by Varian Medical Systems: + - "DeidentifyLogs" to remove patient identification from the logs (C-GET, C-MOVE, C-FIND) + - "DeidentifyLogsDicomVersion" to specify the deidentification rules for the logs + - "OrthancExplorerEnabled" to enable/disable the Orthanc Explorer Web user interface + - "SslMinimumProtocolVersion" to set the minimal SSL protocol version (now defaults to SSL 1.2) + - "SslCiphersAccepted" to set the accepted ciphers over SSL (now defaults to FIPS 140-2) +* New configuration options related to ingest transcoding: + - "IngestTranscodingOfUncompressed" to control whether uncompressed transfer syntaxes are transcoded + - "IngestTranscodingOfCompressed" to control whether compressed transfer syntaxes are transcoded + +REST API +-------- + +* "/instances" can be used to import ZIP archives provided in the POST body + +Maintenance +----------- + +* Allow concurrency on the OrthancPluginRegisterIncomingHttpRequestFilter() callbacks +* Allow empty request body in "/modalities/{id}/echo" +* If meta-header is missing, best-effort to extract "TransferSyntax" in "/instances/{id}/metadata" + + +Version 1.8.1 (2020-12-07) +========================== + +General +------- + +* New sample tool "OrthancImport.py" to easily import compressed archives (ZIP) into Orthanc +* Logging categories (cf. command-line options starting with "--verbose-" and "--trace=") +* New command-line option "--trace-dicom" to access full debug information from DCMTK +* New config option "DicomEchoChecksFind" to automatically complement C-GET SCU with C-FIND SCU + +REST API +-------- + +* API version upgraded to 9 +* "/tools/dicom-echo": Execute C-Echo SCU to a modality that is not registered in "/modalities" +* "/tools/log-level-*": Dynamically access and/or change the verbosity of logging categories +* "/peers/{id}/configuration": Get the configuration of one peer (cf. "/peers?expand") +* "/modalities/{id}/configuration": Get the configuration of one modality (cf. "/modalities?expand") +* "/tools/dicom-echo" and "/modalities/{id}/echo" now accept the field "CheckFind" in their JSON + body to complement C-GET SCU with C-FIND SCU ("DicomEchoChecksFind" on a per-connection basis) +* Archive/media jobs report the size of the created ZIP file in content field "ArchiveSizeMB" + +Plugins +------- + +* New function in the SDK: OrthancPluginGenerateRestApiAuthorizationToken() + +Maintenance +----------- + +* C-GET SCP: Fix responses and handling of cancel +* Fix decoding sequence if "BuiltinDecoderTranscoderOrder" is "Before" +* Fix keep-alive in the embedded HTTP server by setting the "Keep-Alive" HTTP header +* Fix access to videos as a single raw frame (feature broken since Orthanc 1.6.0) +* REST API now returns 404 error if deleting an inexistent peer or modality +* Improved forward ABI compatibility of Orthanc Framework (notably, no inline methods anymore) +* Upgraded dependencies for static builds (notably on Windows and LSB): + - civetweb 1.13 + + +Version 1.8.0 (2020-10-16) +========================== + +General +------- + +* Serving the content of Orthanc as a WebDAV network share +* New config options: "WebDavEnabled", "WebDavDeleteAllowed" and "WebDavUploadAllowed" + +Plugins +------- + +* New available origin for a DICOM instance: "OrthancPluginInstanceOrigin_WebDav" + + +Version 1.7.4 (2020-09-18) +========================== + +General +------- + +* New configuration options to enable HTTP peers identification through certificates: + "SslVerifyPeers" and "SslTrustedClientCertificates" +* New configuration option "SyncStorageArea" to immediately commit the files onto the disk + (through fsync()), so as to avoid discrepencies between DB and filesystem in case of hard + shutdown of the machine running Orthanc. This slows down adding new files into Orthanc. + +Maintenance +----------- + +* Underscores are now allowed in peers/modalities symbolic names +* Fix compatibility with C-MOVE SCU requests issued by Ambra +* Fix transcoding in C-MOVE SCP, in the case where "SynchronousCMove" is "true" +* When checking DICOM allowed methods, if there are multiple modalities with the same AET, + differentiate them from the calling IP +* Enable the access to raw frames in Philips ELSCINT1 proprietary compression +* Support empty key passwords when using HTTP client certificates +* Fix handling of "ModalitiesInStudy" (0008,0061) in C-FIND and "/tools/find" + + +Version 1.7.3 (2020-08-24) +========================== + +REST API +-------- + +* API version upgraded to 8 +* "/peers/{id}/store": New option "Compress" to compress DICOM data using gzip +* "OrthancPeerStore" jobs now report the transmitted size in their public content + +Plugins +------- + +* New config option "Worklist.LimitAnswers" for the sample modality worklist plugin + +Maintenance +----------- + +* Add missing tag "Retrieve AE Title (0008,0054)" in C-FIND SCP responses +* Fix DICOM SCP filters if some query tag has > 256 characters (list of UIDs matching) +* "/series/.../ordered-slices" supports spaces in Image Position/Orientation Patient tags +* Fix possible crash in HttpClient if sending multipart body (can occur in STOW-RS) +* Support receiving multipart messages larger than 2GB in the embedded HTTP server + + +Version 1.7.2 (2020-07-08) +========================== + +General +------- + +* C-FIND SCP now returns private tags (cf. option "DefaultPrivateCreator") +* Packaging of the Orthanc framework as a shared library + +Plugins +------- + +* New change types in the SDK: JobSubmitted, JobSuccess, JobFailure + +Maintenance +----------- + +* Issue #182: Better reporting of errors in plugins reading chunked HTTP body +* Fix issue #183 (C-ECHO always fails in Orthanc Explorer, regression from 1.6.1 to 1.7.0) + + +Version 1.7.1 (2020-05-27) +========================== + +* Fix decoding of DICOM images for plugins (for compatibility with + Orthanc Web Viewer 2.6) + + +Version 1.7.0 (2020-05-22) +========================== + +General +------- + +* Support of DICOM C-GET SCP (contribution by Varian Medical Systems) +* DICOM transcoding over the REST API +* Transcoding from compressed to uncompressed transfer syntaxes over DICOM + C-STORE SCU (if the remote modality doesn't support compressed syntaxes) +* New configuration options related to transcoding: + "TranscodeDicomProtocol", "BuiltinDecoderTranscoderOrder", + "IngestTranscoding" and "DicomLossyTranscodingQuality" + +REST API +-------- + +* API version upgraded to 7 +* Improved: + - "/instances/../modify": it is now possible to "Keep" the "SOPInstanceUID". + Note that it was already possible to "Replace" it. + - added "Timeout" parameter to every DICOM operation + - "/queries/.../answers/../retrieve": "TargetAet" not mandatory anymore + (defaults to the local AET) +* Changes: + - "/{patients|studies|series}/.../modify": New option "KeepSource" + - "/{patients|studies|series|instances}/.../modify": New option "Transcode" + - "/peers/{id}/store": New option "Transcode" + - ".../archive", ".../media", "/tools/create-media" and + "/tools/create-archive": New option "Transcode" + - "/ordered-slices": reverted the change introduced in 1.5.8 and go-back + to 1.5.7 behaviour. + +Plugins +------- + +* New functions in the SDK: + - OrthancPluginCreateDicomInstance() + - OrthancPluginCreateMemoryBuffer() + - OrthancPluginEncodeDicomWebJson2() + - OrthancPluginEncodeDicomWebXml2() + - OrthancPluginFreeDicomInstance() + - OrthancPluginGetInstanceAdvancedJson() + - OrthancPluginGetInstanceDecodedFrame() + - OrthancPluginGetInstanceDicomWebJson() + - OrthancPluginGetInstanceDicomWebXml() + - OrthancPluginGetInstanceFramesCount() + - OrthancPluginGetInstanceRawFrame() + - OrthancPluginRegisterTranscoderCallback() + - OrthancPluginSerializeDicomInstance() + - OrthancPluginTranscodeDicomInstance() +* "OrthancPluginDicomInstance" structure wrapped in "OrthancPluginCppWrapper.h" +* Allow concurrent calls to the custom image decoders provided by the plugins + +Maintenance +----------- + +* Moved the GDCM sample plugin out of the Orthanc repository as a separate plugin +* Fix missing body in "OrthancPluginHttpPost()" and "OrthancPluginHttpPut()" +* Fix issue #169 (TransferSyntaxUID change from Explicit to Implicit during C-STORE SCU) +* Fix issue #179 (deadlock in Python plugins) +* Upgraded dependencies for static builds (notably on Windows and LSB): + - openssl 1.1.1g + + +Version 1.6.1 (2020-04-21) +========================== + +REST API +-------- + +* API version has been upgraded to 6 +* Added: + - "/modalities/{id}/store-straight": Synchronously send the DICOM instance in POST + body to another modality (alternative to command-line tools such as "storescu") + +Plugins +------- + +* New functions in the SDK: + - OrthancPluginRegisterIncomingDicomInstanceFilter() + - OrthancPluginGetInstanceTransferSyntaxUid() + - OrthancPluginHasInstancePixelData() + +Lua +--- + +* New "info" field in "ReceivedInstanceFilter()" callback, containing + "HasPixelData" and "TransferSyntaxUID" information + +Maintenance +----------- + +* Source code repository moved from BitBucket to self-hosted server +* Fix OpenSSL initialization on Linux Standard Base +* Fix lookup form in Orthanc Explorer (wildcards not allowed in StudyDate) +* Fix signature of "OrthancPluginRegisterStorageCommitmentScpCallback()" in plugins SDK +* Error reporting on failure while initializing SSL +* Fix unit test ParsedDicomFile.ToJsonFlags2 on big-endian architectures +* Avoid one memcpy of the DICOM buffer on "POST /instances" +* Upgraded dependencies for static builds (notably on Windows): + - civetweb 1.12 + - openssl 1.1.1f + + +Version 1.6.0 (2020-03-18) +========================== + +General +------- + +* Support of DICOM storage commitment + +REST API +-------- + +* API version has been upgraded to 5 +* Added: + - "/peers/{id}/system": Test the connectivity with a remote peer + (and also retrieve its version number) + - "/tools/log-level": Access and/or change the log level without restarting Orthanc + - "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered": + Render frames, taking windowing and resizing into account + - "/modalities/{...}/storage-commitment": Trigger storage commitment SCU + - "/storage-commitment/{...}": Access storage commitment reports + - "/storage-commitment/{...}/remove": Remove instances from storage commitment reports +* Improved: + - "/changes": Allow the "limit" argument to be greater than 100 + - "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files + - ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags + - "/modalities/{...}/store": New Boolean argument "StorageCommitment" + +Plugins +------- + +* New sample plugin: "ConnectivityChecks" +* New primitives to handle storage commitment SCP by plugins + +Lua +--- + +* New events: + - "OnDeletedPatient", "OnDeletedStudy", "OnDeletedSeries", "OnDeletedInstance": + triggered when a resource is deleted + - "OnUpdatedPatient", "OnUpdatedStudy", "OnUpdatedSeries", "OnUpdatedInstance": + triggered when an attachment or a metadata is updated + +Maintenance +----------- + +* New configuration options: "DefaultPrivateCreator" and "StorageCommitmentReportsSize" +* Support of MPEG4 transfer syntaxes in C-Store SCP +* C-FIND SCU at Instance level now sets the 0008,0052 tag to IMAGE per default (was INSTANCE). + Therefore, the "ClearCanvas" and "Dcm4Chee" modality manufacturer have now been deprecated. +* More strict C-FIND SCP wrt. the DICOM standard: Forbid wildcard + matching on some VRs, ignore main tags below the queried level +* Fix issue #65 (Logging improvements) +* Fix issue #103 ("queries/.../retrieve" API returns HTTP code 200 even on server errors) +* Fix issue #140 (Modifying private tags with REST API changes VR from LO to UN) +* Fix issue #154 (Matching against list of UID-s by C-MOVE) +* Fix issue #156 (Chunked Dicom-web transfer uses 100% CPU) +* Fix issue #165 (Boundary parameter in multipart Content-Type is too long) +* Fix issue #166 (CMake find_boost version is now broken with newer boost/cmake) +* Fix issue #167 (Job can't be cancelled - Handling of timeouts after established association) +* Fix issue #168 (Plugins can't read private tags from the configuration file) +* Upgraded dependencies for static builds (notably on Windows): + - dcmtk 3.6.5 + - openssl 1.1.1d + - jsoncpp 0.10.7 for pre-C++11 compilers + + +Version 1.5.8 (2019-10-16) +========================== + +REST API +-------- + +* API version has been upgraded to 4 +* In /ordered-slices route, ignore instances without position/normal/seriesIndex, + unless there are only such instances in the series + +Maintenance +----------- + +* Security: If remote access is enabled, HTTP authentication is also + enabled by default. This modification was done to mitigate security + risks reported by independant security researcher Amitay Dan. +* Security: New configuration option "ExecuteLuaEnabled" to allow "/tools/execute-script" +* New configuration option: "HttpRequestTimeout" +* Log an explicit error if uploading an empty DICOM file using REST API +* Name of temporary files now include the process ID to ease design of scripts cleaning /tmp +* Fix compatibility of LSB binaries with Ubuntu >= 18.04 +* Fix generation of "SOP Instance UID" on split and merge +* Orthanc Explorer: include the URL search params into HTTP headers to + the REST API to ease usage of the Authorization plugin. Note that + only the 'token', 'auth-token' & 'authorization' search params are + transmitted into HTTP headers. +* Fix lost relationships between CT and RT-STRUCT during anonymization + + +Version 1.5.7 (2019-06-25) +========================== + +REST API +-------- + +* API version has been upgraded to 3 +* "/modalities/{id}/query": New argument "Normalize" can be set to "false" + to bypass the automated correction of outgoing C-FIND queries +* Reporting of "ParentResources" in "DicomModalityStore" and "DicomModalityStore" jobs + +Plugins +------- + +* New functions in the SDK: + - OrthancPluginHttpClientChunkedBody(): HTTP client for POST/PUT with a chunked body + - OrthancPluginRegisterMultipartRestCallback(): HTTP server for POST/PUT with multipart body + - OrthancPluginGetTagName(): Retrieve the name of a DICOM tag from its group and element + +Maintenance +----------- + +* Orthanc now accepts "-H 'Transfer-Encoding: chunked'" option from curl +* Size of the Orthanc static binaries are reduced by compressing ICU data +* Anonymization: Preserve hierarchical relationships in (0008,1115) [] (0020,000e) +* Allow the serialization of signed 16bpp images in PAM format +* HTTP header "Accept-Encoding" is honored for streams without built-in support for compression +* The default HTTP timeout is now 60 seconds (instead of 10 seconds in previous versions) +* Allow anonymizing/modifying instances without the PatientID tag +* Fix issue #106 (Unable to export preview as jpeg from Lua script) +* Fix issue #136 (C-FIND request fails when found DICOM file does not have certain tags) +* Fix issue #137 (C-STORE fails for unknown SOP Class although server is configured to accept any) +* Fix issue #138 (POST to modalities/{name} accepts invalid characters) +* Fix issue #141 (/tools/create-dicom removes non-ASCII characters from study description) + + +Version 1.5.6 (2019-03-01) +========================== + +Orthanc Explorer +---------------- + +* If performing a Query/Retrieve operation, the default value for the + tags is set to an empty string instead of '*', which allows one to match + even if the tag is not present. This allows malformed DICOM files to + be matched, even though they lack required tags such as "PatientSex" + +Maintenance +----------- + +* Enlarge the support of JSON-to-XML conversion in the REST API +* Fix missing DB transactions in some write operations +* Fix performance issue in DICOM protocol by disabling Nagle's algorithm + + +Version 1.5.5 (2019-02-25) +========================== + +General +------- + +* Support of the following multi-byte specific character sets: + - Japanese Kanji (ISO 2022 IR 87) + - Korean (ISO 2022 IR 149) + - Simplified Chinese (ISO 2022 IR 58) +* Basic support for character sets with code extensions (ISO 2022 escape sequences) + +REST API +-------- + +* API version has been upgraded to 2 +* "DicomMoveScu" jobs provide the associated C-FIND answer in their "Query" public field + +Plugins +------- + +* Separation of ideographic and phonetic characters in DICOMweb JSON and XML + +Maintenance +----------- + +* Accept SOP classes: BreastProjectionXRayImageStorageForProcessing/Presentation +* More tolerance wrt. missing DICOM tags that must be returned by Orthanc C-FIND SCP +* Orthanc now interprets the "DCMDICTPATH" environment variable the same way as DCMTK +* New CMake option: "-DMSVC_MULTIPLE_PROCESSES=ON" for parallel build with Visual Studio +* Fix issue #126 (Orthanc and DCMDICTPATH) +* Fix issue #131 (C-MOVE failure due to duplicate StudyInstanceUID in the database) +* Fix issue #134 (/patient/modify gives 500, should really be 400) +* Upgraded dependencies for static builds (notably on Windows): + - boost 1.69.0 + - curl 7.64.0 + - dcmtk 3.6.4 + - e2fsprogs 1.44.5 (libuuid) + - googletest 1.8.1 + - libjpeg 9c + - libpng 1.6.36 + - openssl 1.0.2p + - pugixml 1.9 + - sqlite amalgamation 3.27.1 + + +Version 1.5.4 (2019-02-08) +========================== + +General +------- + +* New configuration options: + - "MetricsEnabled" to enable the tracking of the metrics of Orthanc + - "HttpThreadsCount" to set the number of threads in the embedded HTTP server + - "TemporaryDirectory" to set the folder containing the temporary files + +REST API +-------- + +* API version has been upgraded to 1.4 +* URI "/instances/.../file" can return DICOMweb JSON or XML, depending + on the content of the "Accept" HTTP header +* New URI "/tools/metrics" to dynamically enable/disable the collection of metrics +* New URI "/tools/metrics-prometheus" to retrieve metrics using Prometheus text format +* URI "/peers?expand" provides more information about the peers + +Plugins +------- + +* New functions in the SDK: + - OrthancPluginSetMetricsValue() to set the value of a metrics + - OrthancPluginRegisterRefreshMetricsCallback() to ask to refresh metrics + - OrthancPluginEncodeDicomWebJson() to convert DICOM to "application/dicom+json" + - OrthancPluginEncodeDicomWebXml() to convert DICOM to "application/dicom+xml" +* New extensions in the database SDK: LookupResourceAndParent and GetAllMetadata + +Maintenance +----------- + +* Fix regression if calling "/tools/find" with the tag "ModalitiesInStudy" +* Fix build with unpatched versions of Civetweb (missing "mg_disable_keep_alive()") +* Fix issue #130 (Orthanc failed to start when /tmp partition was full) + + +Version 1.5.3 (2019-01-25) +========================== + +General +------- + +* New configuration option: "SaveJobs" to specify whether jobs are stored in the database + +Maintenance +----------- + +* Don't return tags whose group is below 0x0008 in C-FIND SCP answers +* Fix compatibility with DICOMweb plugin (allow multipart answers over HTTP Keep-Alive) +* Fix issue #73 (/modalities/{modalityId}/store raises 500 errors instead of 404) +* Fix issue #90 (C-Find shall match missing tags to null/empty string) +* Fix issue #119 (/patients/.../archive returns a 500 when JobsHistorySize is 0) +* Fix issue #128 (Asynchronous C-MOVE: invalid number of remaining sub-operations) + + +Version 1.5.2 (2019-01-18) +========================== + +General +------- + +* CivetWeb is now the default embedded HTTP server (instead of Mongoose) +* New configuration option: "TcpNoDelay" to disable Nagle's algorithm in HTTP server + +REST API +-------- + +* API version has been upgraded to 1.3 +* More consistent handling of the "Last" field returned by the "/changes" URI + +Plugins +------- + +* New primitives to speed up databases (custom index plugins) + +Maintenance +----------- + +* Ignore tags whose group is below 0x0008 in C-FIND SCP requests +* Compatibility with DCMTK 3.6.4 +* Fix issue #21 (DICOM files missing after uploading with Firefox) +* Fix issue #32 (HTTP keep-alive is now enabled by default) +* Fix issue #58 (Patient recycling order should be defined by their received last instance) +* Fix issue #118 (Wording in Configuration.json regarding SynchronousCMove) +* Fix issue #124 (GET /studies/ID/media fails for certain dicom file) +* Fix issue #125 (Mongoose: /instances/{id} returns 500 on invalid HTTP Method) +* Fixed Orthanc Explorer on IE and Firefox: Explorer always show "too many results" + and it's therefore impossible to browse the content +* Upgraded dependencies for static and Windows builds: + - civetweb 1.11 + + +Version 1.5.1 (2018-12-20) +========================== + +General +------- + +* Optimization: On C-FIND, avoid accessing the storage area whenever possible +* New configuration option: + - "StorageAccessOnFind" to rule the access to the storage area during C-FIND + +Maintenance +----------- + +* Removal of the "AllowFindSopClassesInStudy" old configuration option +* "/tools/create-dicom" is more tolerant wrt. invalid specific character set + + +Version 1.5.0 (2018-12-10) +========================== + +General +------- + +* Possibility to restrict the allowed DICOM commands for each modality +* The Orthanc configuration file can use environment variables +* New configuration options: + - "DicomModalitiesInDatabase" to store the definitions of modalities in the database + - "OrthancPeersInDatabase" to store the definitions of Orthanc peers in the database + +Orthanc Explorer +---------------- + +* The first screen of Orthanc Explorer is now a form to do studies lookups +* Support of large databases, by limiting the results to 100 patients or studies + +REST API +-------- + +* API version has been upgraded to 1.2 +* Asynchronous generation of ZIP archives and DICOM medias +* New URI: "/studies/.../merge" to merge a study +* New URI: "/studies/.../split" to split a study +* POST-ing a DICOM file to "/instances" also answers the patient/study/series ID +* GET "/modalities/?expand" now returns a JSON object instead of a JSON array +* New "Details" field in HTTP answers on error (cf. "HttpDescribeErrors" option) +* New options to URI "/queries/.../answers": "?expand" and "?simplify" +* New URIs to launch new C-FIND to explore the hierarchy of a C-FIND answer: + - "/queries/.../answers/.../query-instances" to C-FIND child instances + - "/queries/.../answers/.../query-series" to C-FIND child series + - "/queries/.../answers/.../query-studies" to C-FIND child studies +* New "DicomDiskSize" and "DicomUncompressedSize" fields in statistics about resources + +Plugins +------- + +* New functions in the SDK: + - "OrthancPluginSetHttpErrorDetails()" + - "OrthancPluginAutodetectMimeType()" + +Maintenance +----------- + +* "SynchronousCMove" is now "true" by default +* New modality manufacturer: "GE" for GE Healthcare EA and AW +* Executing a query/retrieve from the REST API now creates a job +* Fix: Closing DICOM associations after running query/retrieve from REST API +* Fix: Allow creation of MONOCHROME1 grayscale images in tools/create-dicom +* Remove invalid characters from badly-encoded UTF-8 strings (impacts PostgreSQL) +* Orthanc starts even if jobs from a previous execution cannot be unserialized +* New CMake option "ENABLE_DCMTK_LOG" to disable logging internal to DCMTK +* Fix issue 114 (Boost 1.68 doesn't support SHA-1 anymore) +* Support of "JobsHistorySize" set to zero +* Upgraded dependencies for static and Windows builds: + - boost 1.68.0 + - lua 5.3.5 + + +Version 1.4.2 (2018-09-20) +========================== + +General +------- + +* "OrthancPeers" configuration option now allows one to specify HTTP headers +* New main DICOM tag: "ImageOrientationPatient" at the instance level +* New configuration options: + - "HttpVerbose" to debug outgoing HTTP connections + - "OverwriteInstances" to choose how duplicate SOPInstanceUID are handled + +Orthanc Explorer +---------------- + +* Query/retrieve: Added button for "DX" modality + +REST API +-------- + +* "/tools/reconstruct" to reconstruct the main DICOM tags, the JSON summary and + the metadata of all the instances stored in Orthanc. This is a slow operation! + +Plugins +------- + +* New primitives to access Orthanc peers from plugins +* New events in change callbacks: "UpdatedPeers" and "UpdatedModalities" +* New primitives to handle jobs from plugins: "OrthancPluginSubmitJob()" + and "OrthancPluginRegisterJobsUnserializer()" + +Lua +--- + +* IncomingWorklistRequestFilter() to filter incoming C-FIND worklist queries + +Maintenance +----------- + +* Fix "/series/.../ordered-slices" in the presence of non-parallel slices +* Fix incoming DICOM C-Store filtering for JPEG-LS transfer syntaxes +* Fix OrthancPluginHttpClient() to return the HTTP status on errors +* Fix HTTPS requests to sites using a certificate encrypted with ECDSA +* Fix handling of incoming C-FIND queries containing Generic Group Length (*, 0x0000) +* Fix issue 54 (quoting multipart answers), for OsiriX compatibility through DICOMweb +* Fix issue 98 (DCMTK configuration fails with GCC 6.4.0 on Alpine) +* Fix issue 99 (PamWriter test segfaults on alpine linux with gcc 6.4.0) + + +Version 1.4.1 (2018-07-17) +========================== + +* Fix deadlock in Lua scripting +* Simplification to the "DatabaseWrapper" class + + +Version 1.4.0 (2018-07-13) +========================== + +General +------- + +* New advanced job engine +* New configuration options: + - "ConcurrentJobs": Max number of jobs that are simultaneously running + - "SynchronousCMove": Whether to run DICOM C-Move operations synchronously + - "JobsHistorySize": Max number of completed jobs that are kept in memory +* New metadata automatically computed at the instance level: + "RemoteIp", "CalledAet" and "HttpUsername" + +Orthanc Explorer +---------------- + +* New screen listing all the available studies + +REST API +-------- + +* "/jobs/..." to manage the jobs from the REST API +* New option "?short" to list DICOM tags using their hexadecimal ID in: + - "/instances/.../tags?short" + - "/instances/.../header?short" + - "/{patients|studies|series}/.../instances-tags?short" + - "/{patients|studies|series}/.../shared-tags?short" + - "/{patients|studies|series|instances}/.../module?short" + - "/studies/.../module-patient?short" +* "/instances/.../tags" URI was returning only the first value of + DicomTags containing multiple numerical value. It now returns all + values in a string separated by \\ (i.e.: "1\\2\\3"). Note that, + for data already in Orthanc, you'll need to reconstruct the data by + sending a POST request to the ".../reconstruct" URI. This change + triggered an update of ORTHANC_API_VERSION from 1.0 to 1.1 +* "/instances/.../frame/../image-uint8 and friends now accepts a + "image/pam" MIME type to retrieve images in PAM format + (https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format) +* New option "?expand" to "/instances/.../metadata" + +Plugins +------- + +* New primitive in database SDK: "lookupIdentifierRange" to speed up range searches +* New function in the SDK: "OrthancPluginCheckVersionAdvanced()" + +Maintenance +----------- + +* Configuration option "LogExportedResources" is now "false" by default +* Header "OrthancCppDatabasePlugin.h" is now part of the "orthanc-databases" project +* Fix generation of DICOMDIR if PatientID is empty +* Fix issue 25 (Deadlock with Lua scripts): The event queue is now implemented for Lua +* Fix issue 94 (Instance modification should not modify FrameOfReferenceUID) +* Fix issue 77 (Lua access to REST-API is null terminated) +* Fix memory leak introduced by changeset #99116ed6f38c in Orthanc 1.3.2 +* Upgraded dependencies for static and Windows builds: + - boost 1.67.0 + - openssl 1.0.2o + + +Version 1.3.2 (2018-04-18) +========================== + +REST API +-------- + +* "/system" URI returns the version of the Orthanc REST API +* "/tools/now" returns the current UTC (universal) time +* "/tools/now-local" returns the curent local time. + This was the behavior of "/tools/now" until release 1.3.1. +* Added "?expand" GET argument to "/peers" and "/modalities" routes +* New URI: "/tools/create-media-extended" to generate a DICOMDIR + archive from several resources, including additional type-3 tags +* Preservation of UID relationships while anonymizing + +Lua +--- + +* New CMake option: "-DENABLE_LUA_MODULES=ON" to enable support for + loading external Lua modules if the Lua engine is statically linked + +Plugins +------- + +* New error code: DatabaseUnavailable + +Maintenance +----------- + +* Orthanc now uses UTC (universal time) instead of local time in its database +* Fix to allow creating DICOM instances with empty Specific Character Set (0008,0005) +* Support of Linux Standard Base +* Static linking against libuuid (from e2fsprogs) +* Fix static build on CentOS 6 +* Possibility of using JsonCpp 0.10.6 if the compiler does not support C++11 + with the "-DUSE_LEGACY_JSONCPP=ON" CMake option +* Upgraded dependencies for static and Windows builds: + - boost 1.66.0 + - curl 7.57.0 + - jsoncpp 1.8.4 + - zlib 1.2.11 + + +Version 1.3.1 (2017-11-29) +========================== + +General +------- + +* Built-in decoding of palette images + +REST API +-------- + +* New URI: "/instances/.../frames/.../raw.gz" to compress raw frames using gzip +* New argument "ignore-length" to force the inclusion of too long tags in JSON +* New argument "/.../media?extended" to include additional type-3 tags in DICOMDIR + +Plugins +------- + +* New pixel formats exposed in SDK: BGRA32, Float32, Grayscale32, RGB48 + +Maintenance +----------- + +* Creation of ./Resources/CMake/OrthancFramework*.cmake to reuse the Orthanc + C++ framework in other projects +* New security-related options: "DicomAlwaysAllowEcho" +* Use "GBK" (frequently used in China) as an alias for "GB18030" +* Experimental support of actively maintained Civetweb to replace Mongoose 3.8 +* Fix issue 31 for good (create new modality types for Philips ADW, GE Xeleris, GE AWServer) +* Fix issue 64 (OpenBSD support) +* Fix static compilation of DCMTK 3.6.2 on Fedora +* Upgrade to Boost 1.65.1 in static builds +* Upgrade to SQLite amalgamation 3.21.0 in static builds + + +Version 1.3.0 (2017-07-19) +========================== + +General +------- + +* Orthanc now anonymizes according to Basic Profile of PS 3.15-2017c Table E.1-1 +* In the "DicomModalities" configuration: + - Manufacturer type MedInria is now obsolete + - Manufacturer types AgfaImpax and SyngoVia are obsolete too + (use GenericNoWildcardInDates instead) + - Obsolete manufacturers are still accepted but might disappear in the future + - Added new manufacturer: GenericNoUniversalWildcard to replace all '*' by '' in + outgoing C-Find requests +* New security-related options: "DicomAlwaysAllowStore" and "DicomCheckModalityHost" + +REST API +-------- + +* Argument "Since" in URI "/tools/find" (related to issue 53) +* Argument "DicomVersion" in URIs "/{...}/{...}/anonymization" + +Plugins +------- + +* New function: "OrthancPluginRegisterIncomingHttpRequestFilter2()" + +Lua +--- + +* Added HTTP headers support for Lua HttpPost/HttpGet/HttpPut/HttpDelete + +Orthanc Explorer +---------------- + +* Query/retrieve: Added button for "DR" modality + +Maintenance +----------- + +* Ability to retrieve raw frames encoded as unsigned 32-bits integers +* Fix issue 29 (more consistent handling of the "--upgrade" argument) +* Fix issue 31 (create new modality types for Philips ADW, GE Xeleris, GE AWServer) +* Fix issue 35 (AET name is not transferred to Orthanc using DCMTK 3.6.0) +* Fix issue 44 (bad interpretation of photometric interpretation MONOCHROME1) +* Fix issue 45 (crash when providing a folder to "--config" command-line option) +* Fix issue 46 (PHI remaining after anonymization) +* Fix issue 49 (worklists: accentuated characters are removed from C-Find responses) +* Fix issue 52 (DICOM level security association problems) +* Fix issue 55 (modification/anonymization of tags that might break the database + model now requires the "Force" parameter to be set to "true" in the query) +* Fix issue 56 (case-insensitive matching over accents) +* Fix Debian #865606 (orthanc FTBFS with libdcmtk-dev 3.6.1~20170228-2) +* Fix XSS inside DICOM in Orthanc Explorer (as reported by Victor Pasnkel, Morphus Labs) +* Upgrade to DCMTK 3.6.2 in static builds (released on 2017-07-17) +* Upgrade to Boost 1.64.0 in static builds +* New advanced "Locale" configuration option +* Removed configuration option "USE_DCMTK_361_PRIVATE_DIC" + + +Version 1.2.0 (2016/12/13) +========================== + +General +------- + +* Handling of private tags/creators in the "Dictionary" configuration option +* New configuration options: "LoadPrivateDictionary", "DicomScuTimeout" and "DicomScpTimeout" +* New metadata automatically computed at the instance level: "TransferSyntax" and "SopClassUid" + +REST API +-------- + +* "/tools/invalidate-tags" to invalidate the JSON summary of all the DICOM files + (useful if private tags are registered, or if changing the default encoding) +* "Permissive" flag for URI "/modalities/{...}/store" to ignore C-STORE transfer errors +* "Asynchronous" flag for URIs "/modalities/{...}/store" and "/peers/{...}/store" + to avoid waiting for the completion of image transfers +* Possibility to DELETE "dicom-as-json" attachments to reconstruct the JSON summaries + (useful if "Dictionary" has changed) +* "Keep" option for modifications to keep original DICOM identifiers (advanced feature) +* "/tools/default-encoding" to get or temporarily change the default encoding +* "/{resource}/{id}/reconstruct" to reconstruct the main DICOM tags, the JSON summary and + the metadata of a resource (useful to compute new metadata, or if using "Keep" above) + +Plugins +------- + +* New function: "OrthancPluginRegisterPrivateDictionaryTag()" to register private tags +* More control over client cache in the ServeFolders plugin +* New C++ help wrappers in "Plugins/Samples/Common/" to read DICOM datasets from REST API +* New data structure: "OrthancPluginFindMatcher" to match DICOM against C-FIND queries + +Maintenance +----------- + +* Fix handling of encodings in C-FIND requests (including for worklists) +* Use of DCMTK 3.6.1 dictionary of private tags in standalone builds +* Avoid hard crash if not enough memory (handling of std::bad_alloc) +* Improved robustness of Orthanc Explorer wrt. query/retrieve (maybe fix issue 24) +* Fix serious performance issue with C-FIND +* Fix extraction of the symbolic name of the private tags +* Performance warning if runtime debug assertions are turned on +* Improved robustness against files with no PatientID +* Upgrade to curl 7.50.3 for static and Windows builds +* Content-Type for JSON documents is now "application/json; charset=utf-8" +* Ignore "Group Length" tags in C-FIND queries +* Fix handling of worklist SCP with ReferencedStudySequence and ReferencedPatientSequence +* Fix handling of Move Originator AET and ID in C-MOVE SCP +* Fix vulnerability ZSL-2016-5379 "Unquoted Service Path Privilege Escalation" in the + Windows service +* Fix vulnerability ZSL-2016-5380 "Remote Memory Corruption Vulnerability" in DCMTK 3.6.0 + + +Version 1.1.0 (2016/06/27) +========================== + +General +------- + +* HTTPS client certificates can be associated with Orthanc peers to enhance security over Internet +* Possibility to use PKCS#11 authentication for hardware security modules with Orthanc peers +* New command-line option "--logfile" to output the Orthanc log to the given file +* Support of SIGHUP signal (restart Orthanc only if the configuration files have changed) + +REST API +-------- + +* New URI: "/instances/.../frames/.../raw" to access the raw frames (bypass image decoding) +* New URI "/modalities/.../move" to issue C-MOVE SCU requests +* "MoveOriginatorID" can be specified for "/modalities/.../store" + +Dicom protocol +-------------- + +* Support of optional tags for counting resources in C-FIND: + 0008-0061, 0008-0062, 0020-1200, 0020-1202, 0020-1204, 0020-1206, 0020-1208, 0020-1209 +* Support of Move Originator Message ID (0000,1031) in C-STORE responses driven by C-MOVE + +Plugins +------- + +* Speedup in plugins by removing unnecessary locks +* New callback to filter incoming HTTP requests: OrthancPluginRegisterIncomingHttpRequestFilter() +* New callback to handle non-worklists C-FIND requests: OrthancPluginRegisterFindCallback() +* New callback to handle C-MOVE requests: OrthancPluginRegisterMoveCallback() +* New function: "OrthancPluginHttpClient()" to do HTTP requests with full control +* New function: "OrthancPluginGenerateUuid()" to generate a UUID +* More than one custom image decoder can be installed (e.g. to handle different transfer syntaxes) + +Lua +--- + +* Possibility to dynamically fix outgoing C-FIND requests using "OutgoingFindRequestFilter()" +* Access to the HTTP headers in the "IncomingHttpRequestFilter()" callback + +Image decoding +-------------- + +* Huge speedup if decoding the family of JPEG transfer syntaxes +* Refactoring leading to speedups with custom image decoders (including Web viewer plugin) +* Support decoding of RLE Lossless transfer syntax +* Support of signed 16bpp images in ParsedDicomFile + +Maintenance +----------- + +* New logo of Orthanc +* Fix issue 11 (is_regular_file() fails for FILE_ATTRIBUTE_REPARSE_POINT) +* Fix issue 16 ("Limit" parameter error in REST API /tools/find method) +* Fix of Debian bug #818512 ("FTBFS: Please install libdcmtk*-dev") +* Fix of Debian bug #823139 ("orthanc: Please provide RecoverCompressedFile.cpp") +* Replaced "localhost" by "127.0.0.1", as it might impact performance on Windows +* Compatibility with CMake >= 3.5.0 +* Possibility to use forthcoming DCMTK 3.6.1 in static builds (instead of 3.6.0) +* Upgrade to Boost 1.60.0 for static builds +* Use of HTTP status 403 Forbidden (instead of 401) if access to a REST resource is disallowed +* Option "HttpsVerifyPeers" can be used to connect against self-signed HTTPS certificates +* New configuration option "AllowFindSopClassesInStudy" +* Macro "__linux" (now obsolete) replaced by macro "__linux__" (maybe solves Debian bug #821011) +* Modification of instances can now replace PixelData (resp. EncapsulatedDocument) with + provided a PNG/JPEG image (resp. PDF file) if it is encoded using Data URI Scheme +* Dropped support of Google Log + + +Version 1.0.0 (2015/12/15) +========================== + +* Lua: "IncomingFindRequestFilter()" to apply filters to incoming C-FIND requests +* New function in plugin SDK: "OrthancPluginSendMultipartItem2()" +* Fix of DICOMDIR generation with DCMTK 3.6.1, support of encodings +* Fix range search if the lower or upper limit is absent +* Fix modality worklists lookups if tags with UN (unknown) VR are present +* Warn about badly formatted modality/peer definitions in configuration file at startup + + +Version 0.9.6 (2015/12/08) +========================== + +* Promiscuous mode (accept unknown SOP class UID) is now turned off by default +* Fix serialization of DICOM buffers that might contain garbage trailing +* Fix modality worklists server if some fields are null +* More tolerant "/series/.../ordered-slices" with broken series +* Improved logging information if upgrade fails +* Fix formatting of multipart HTTP answers (bis) + + +Version 0.9.5 (2015/12/02) +========================== + +Major +----- + +* Experimental support of DICOM C-FIND SCP for modality worklists through plugins +* Support of DICOM C-FIND SCU for modality worklists ("/modalities/{dicom}/find-worklist") + +REST API +-------- + +* New URIs: + - "/series/.../ordered-slices" to order the slices of a 2D+t or 3D series + - "/tools/shutdown" to stop Orthanc from the REST API + - ".../compress", ".../uncompress" and ".../is-compressed" for attachments + - "/tools/create-archive" to create ZIP from a set of resources + - "/tools/create-media" to create ZIP+DICOMDIR from a set of resources + - "/instances/.../header" to get the meta information (header) of the DICOM instance +* "/tools/create-dicom": + - Support of binary tags encoded using data URI scheme + - Support of hierarchical structures (creation of sequences) + - Create tags with unknown VR +* "/modify" can insert/modify sequences +* ".../preview" and ".../image-uint8" can return JPEG images if the HTTP Accept Header asks so +* "Origin" metadata for the instances + +Minor +----- + +* New configuration options: + - "UnknownSopClassAccepted" to disable promiscuous mode (accept unknown SOP class UID) + - New configuration option: "Dictionary" to declare custom DICOM tags +* Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive") +* MIME content type can be associated to custom attachments (cf. "UserContentType") + +Plugins +------- + +* New functions: + - "OrthancPluginRegisterDecodeImageCallback()" to replace the built-in image decoder + - "OrthancPluginDicomInstanceToJson()" to convert DICOM to JSON + - "OrthancPluginDicomBufferToJson()" to convert DICOM to JSON + - "OrthancPluginRegisterErrorCode()" to declare custom error codes + - "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags + - "OrthancPluginLookupDictionary()" to get information about some DICOM tag + - "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API + - "OrthancPluginGetInstanceOrigin()" to know through which mechanism an instance was received + - "OrthancPluginCreateImage()" and "OrthancPluginCreateImageAccessor()" to create images + - "OrthancPluginDecodeDicomImage()" to decode DICOM images + - "OrthancPluginComputeMd5()" and "OrthancPluginComputeSha1()" to compute MD5/SHA-1 hash +* New events in change callbacks: + - "OrthancStarted" + - "OrthancStopped" + - "UpdatedAttachment" + - "UpdatedMetadata" +* "/system" URI gives information about the plugins used for storage area and DB back-end +* Plugin callbacks must now return explicit "OrthancPluginErrorCode" (instead of integers) + +Lua +--- + +* Optional argument "keepStrings" in "DumpJson()" + +Maintenance +----------- + +* Full indexation of the patient/study tags to speed up searches and C-FIND +* Many refactorings, notably of the searching features and of the image decoding +* C-MOVE SCP for studies using AccessionNumber tag +* Fix issue 4 (C-STORE Association not renegotiated on Specific-to-specific transfer syntax change) +* Fix formatting of multipart HTTP answers +* "--logdir" flag creates a single log file instead of 3 separate files for errors/warnings/infos +* "--errors" flag lists the error codes that could be returned by Orthanc +* Under Windows, the exit status of Orthanc corresponds to the encountered error code +* New "AgfaImpax", "EFilm2" and "Vitrea" modality manufacturers +* C-FIND SCP will return tags with sequence value representation +* Upgrade to Boost 1.59.0 for static builds + + +Version 0.9.4 (2015/09/16) +========================== + +* Preview of PDF files encapsulated in DICOM from Orthanc Explorer +* 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 +* New configuration option: "HttpTimeout" to set the default timeout for HTTP requests + +Lua +--- + +* More information about the origin request in the "OnStoredInstance()" and + "ReceivedInstanceFilter()" callbacks. WARNING: This can result in + incompatibilities wrt. previous versions of Orthanc. +* New function "GetOrthancConfiguration()" to get the Orthanc configuration + +Plugins +------- + +* New functions to compress/uncompress images using PNG and JPEG +* New functions to issue HTTP requests from plugins +* New function "OrthancPluginBufferCompression()" to (un)compress memory buffers +* New function "OrthancPluginReadFile()" to read files from the filesystem +* New function "OrthancPluginWriteFile()" to write files to the filesystem +* New function "OrthancPluginGetErrorDescription()" to convert error codes to strings +* New function "OrthancPluginSendHttpStatus()" to send HTTP status with a body +* New function "OrthancPluginRegisterRestCallbackNoLock()" for high-performance plugins +* Plugins have access to explicit error codes +* Improvements to the sample "ServeFolders" plugin +* Primitives to upgrade the database version in plugins + +Maintenance +----------- + +* Many code refactorings +* Improved error codes (no more custom descriptions in exceptions) +* If error while calling the REST API, the answer body contains description of the error + (this feature can be disabled with the "HttpDescribeErrors" option) +* Upgrade to curl 7.44.0 for static and Windows builds +* Upgrade to openssl 1.0.2d for static and Windows builds +* Depends on libjpeg 9a +* Bypass zlib uncompression if "StorageCompression" is enabled and HTTP client supports deflate + + +Version 0.9.3 (2015/08/07) +========================== + +* C-Echo testing can be triggered from Orthanc Explorer (in the query/retrieve page) +* Removal of the dependency upon Google Log, Orthanc now uses its internal logger + (use -DENABLE_GOOGLE_LOG=ON to re-enable Google Log) +* Upgrade to JsonCpp 0.10.5 for static and Windows builds + + +Version 0.9.2 (2015/08/02) +========================== + +* Upgrade to Boost 1.58.0 for static and Windows builds +* Source code repository moved from Google Code to BitBucket +* Inject version information into Windows binaries +* Fix access to binary data in HTTP/REST requests by Lua scripts +* Fix potential deadlock in the callbacks of plugins + + +Version 0.9.1 (2015/07/02) +========================== + +General +------- + +* The configuration can be splitted into several files stored inside the same folder +* Custom setting of the local AET during C-STORE SCU (both in Lua and in the REST API) +* Many code refactorings + +Lua +--- + +* Access to the REST API of Orthanc (RestApiGet, RestApiPost, RestApiPut, RestApiDelete) +* Functions to convert between Lua values and JSON strings: "ParseJson" and "DumpJson" +* New events: "OnStablePatient", "OnStableStudy", "OnStableSeries", "Initialize", "Finalize" + +Plugins +------- + +* Plugins can retrieve the configuration file directly as a JSON string +* Plugins can send answers as multipart messages + +Fixes +----- + +* Fix compatibility issues for C-FIND SCU to Siemens Syngo.Via modalities SCP +* Fix issue 15 (Lua scripts making HTTP requests) +* Fix issue 35 (Characters in PatientID string are not protected for C-FIND) +* Fix issue 37 (Hyphens trigger range query even if datatype does not support ranges) + + +Version 0.9.0 (2015/06/03) +========================== + +Major +----- + +* DICOM Query/Retrieve available from Orthanc Explorer +* C-MOVE SCU and C-FIND SCU are accessible through the REST API +* "?expand" flag for URIs "/patients", "/studies" and "/series" +* "/tools/find" URI to search for DICOM resources from REST +* Support of FreeBSD +* The "Orthanc Client" SDK is now a separate project + +Minor +----- + +* Speed-up in Orthanc Explorer for large amount of images +* Speed-up of the C-FIND SCP server of Orthanc +* Allow replacing PatientID/StudyInstanceUID/SeriesInstanceUID from Lua scripts +* Option "CaseSensitivePN" to enable case-insensitive C-FIND SCP + +Fixes +----- + +* Prevent freeze on C-FIND if no DICOM tag is to be returned +* Fix slow C-STORE SCP on recent versions of GNU/Linux, if + USE_SYSTEM_DCMTK is set to OFF (http://forum.dcmtk.org/viewtopic.php?f=1&t=4009) +* Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052)) +* Fix issue 32 (Cyrillic symbols): Introduction of the "Windows1251" encoding +* Plugins now receive duplicated GET arguments in their REST callbacks + + +Version 0.8.6 (2015/02/12) +========================== + +Major +----- + +* URIs to get all the parents of a given resource in a single REST call +* Instances without PatientID are now allowed +* Support of HTTP proxy to access Orthanc peers + +Minor +----- + +* Support of Tudor DICOM in Query/Retrieve +* More flexible "/modify" and "/anonymize" for single instance +* Access to called AET and remote AET from Lua scripts ("OnStoredInstance") +* Option "DicomAssociationCloseDelay" to set delay before closing DICOM association +* ZIP archives now display the accession number of the studies + +Plugins +------- + +* Introspection of plugins (cf. the "/plugins" URI) +* Plugins can access the command-line arguments used to launch Orthanc +* Plugins can extend Orthanc Explorer with custom JavaScript +* Plugins can get/set global properties to save their configuration +* Plugins can do REST calls to other plugins (cf. "xxxAfterPlugins()") +* Scan of folders for plugins + +Fixes +----- + +* Code refactorings +* Fix issue 25 (AET with underscore not allowed) +* Fix replacement and insertion of private DICOM tags +* Fix anonymization generating non-portable DICOM files + + +Version 0.8.5 (2014/11/04) +========================== + +General +------- + +* Major speed-up thanks to a new database schema +* Plugins can monitor changes through callbacks +* Download ZIP + DICOMDIR from Orthanc Explorer +* Sample plugin framework to serve static resources (./Plugins/Samples/WebSkeleton/) + +Fixes +----- + +* Fix issue 19 (YBR_FULL are decoded incorrectly) +* Fix issue 21 (Microsoft Visual Studio precompiled headers) +* Fix issue 22 (Error decoding multi-frame instances) +* Fix issue 24 (Build fails on OSX when directory has .DS_Store files) +* Fix crash when bad HTTP credentials are provided + + +Version 0.8.4 (2014/09/19) +========================== + +* "/instances-tags" to get the tags of all the child instances of a + patient/study/series with a single REST call (bulk tags retrieval) +* Configuration/Lua to select the accepted C-STORE SCP transfer syntaxes +* Fix reporting of errors in Orthanc Explorer when sending images to peers/modalities +* Installation of plugin SDK in CMake + + +Version 0.8.3 (2014/09/11) +========================== + +Major +----- + +* Creation of ZIP archives for media storage, with DICOMDIR +* URIs to get all the children of a given resource in a single REST call +* "/tools/lookup" URI to map DICOM UIDs to Orthanc identifiers +* Support of index-only mode (using the "StoreDicom" option) +* Plugins can implement a custom storage area + +Minor +----- + +* Configuration option to enable HTTP Keep-Alive +* Configuration option to disable the logging of exported resources in "/exports" +* Plugins can retrieve the path to Orthanc and to its configuration file +* "/tools/create-dicom" now accepts the "PatientID" DICOM tag (+ updated sample) +* Possibility to set HTTP headers from plugins +* "LastUpdate" metadata is now always returned for patients, studies and series + +Maintenance +----------- + +* Refactoring of HttpOutput ("Content-Length" header is now always sent) +* Upgrade to Mongoose 3.8 +* Fixes for Visual Studio 2013 and Windows 64bit +* Fix issue 16: Handling of "AT" value representations in JSON +* Fix issue 17 + + +Version 0.8.2 (2014/08/07) +========================== + +* Support of the standard text encodings +* Hot restart of Orthanc by posting to "/tools/reset" +* More fault-tolerant commands in Lua scripts +* Parameter to set the default encoding for DICOM files without SpecificCharacterSet +* Fix of issue #14 (support of XCode 5.1) +* Upgrade to Google Test 1.7.0 + + +Version 0.8.1 (2014/07/29) +========================== + +General +------- + +* Access patient module at the study level to cope with PatientID collisions +* On-the-fly conversion of JSON to XML according to the HTTP Accept header +* C-Echo SCU in the REST API +* DICOM conformance statement available at URI "/tools/dicom-conformance" + +Lua scripts +----------- + +* Lua scripts can do HTTP requests, and thus can call Web services +* Lua scripts can invoke system commands, with CallSystem() + +Plugins +------- + +* Lookup for DICOM UIDs in the plugin SDK +* Plugins have access to the HTTP headers and can answer with HTTP status codes +* Callback to react to the incoming of DICOM instances + +Fixes +----- + +* Fix build of Google Log with Visual Studio >= 11.0 +* Fix automated generation of the list of resource children in the REST API + + +Version 0.8.0 (2014/07/10) +========================== + +Major changes +------------- + +* Routing images with Lua scripts +* Introduction of the Orthanc Plugin SDK +* Official support of OS X (Darwin) 10.8 + +Minor changes +------------- + +* Extraction of tags for the patient/study/series/instance DICOM modules +* Extraction of the tags shared by all the instances of a patient/study/series +* Options to limit the number of results for an incoming C-FIND query +* Support of kFreeBSD +* Several code refactorings +* Fix OrthancCppClient::GetVoxelSizeZ() + + +Version 0.7.6 (2014/06/11) +========================== + +* Support of JPEG and JPEG-LS decompression +* Download DICOM images as Matlab/Octave arrays +* Precompiled headers for Microsoft Visual Studio + + +Version 0.7.5 (2014/05/08) +========================== + +* Dynamic negotiation of SOP classes for C-STORE SCU +* Creation of DICOM instances using the REST API +* Embedding of images within DICOM instances +* Adding/removal/modification of remote modalities/peers through REST +* Reuse of the previous SCU connection to avoid unnecessary handshakes +* Fix problems with anonymization and modification +* Fix missing licensing terms about reuse of some code from DCMTK +* Various code refactorings + + +Version 0.7.4 (2014/04/16) +========================== + +* Switch to openssl-1.0.1g in static builds (cf. Heartbleed exploit) +* Switch to boost 1.55.0 in static builds (to solve compiling errors) +* Better logging about nonexistent tags +* Dcm4Chee manufacturer +* Automatic discovering of the path to the DICOM dictionaries +* In the "DicomModalities" config, the port number can be a string + + +Version 0.7.3 (2014/02/14) +========================== + +Major changes +------------- + +* Fixes in the implementation of the C-FIND handler for Query/Retrieve +* Custom attachment of files to patients, studies, series or instances +* Access to lowlevel info about the attached files through the REST API +* Recover pixel data for more transfer syntaxes (notably JPEG) + +Minor changes +------------- + +* AET comparison is now case-insensitive by default +* Possibility to disable the HTTP server or the DICOM server +* Automatic computation of MD5 hashes for the stored DICOM files +* Maintenance tool to recover DICOM files compressed by Orthanc +* The newline characters in the configuration file are fixed for GNU/Linux +* Capture of the SIGTERM signal in GNU/Linux + + +Version 0.7.2 (2013/11/08) +========================== + +* Support of Query/Retrieve from medInria +* Accept more transfer syntaxes for C-STORE SCP and SCU (notably JPEG) +* Create the meta-header when receiving files through C-STORE SCP +* Fixes and improvements thanks to the static analyzer cppcheck + + +Version 0.7.1 (2013/10/30) +========================== + +* Use ZIP64 only when required to improve compatibility (cf. issue #7) +* Refactoring of the CMake options +* Fix for big-endian architectures (RedHat bug #985748) +* Use filenames with 8 characters in ZIP files for maximum compatibility +* Possibility to build Orthanc inplace (in the source directory) + + +Version 0.7.0 (2013/10/25) +========================== + +Major changes +------------- + +* DICOM Query/Retrieve is supported + +Minor changes +------------- + +* Possibility to keep the PatientID during an anonymization +* Check whether "unzip", "tar" and/or "7-zip" are installed from CMake + + +Version 0.6.2 (2013/10/04) +========================== + +* Build of the C++ client as a shared library +* Improvements and documentation of the C++ client API +* Fix of Debian bug #724947 (licensing issue with the SHA-1 library) +* Switch to Boost 1.54.0 (cf. issue #9) +* "make uninstall" is now possible + + +Version 0.6.1 (2013/09/16) +========================== + +* Detection of stable patients/studies/series +* C-FIND SCU at the instance level +* Link from modified to original resource in Orthanc Explorer +* Fix of issue #8 +* Anonymization of the medical alerts tag (0010,2000) + + +Version 0.6.0 (2013/07/16) +========================== + +Major changes +------------- + +* Introduction of the C++ client +* Send DICOM resources to other Orthanc instances through HTTP +* Access to signed images (instances/.../image-int16) + (Closes: Debian #716958) + +Minor changes +------------- + +* Export of DICOM files to the host filesystem (instances/.../export) +* Statistics about patients, studies, series and instances +* Link from anonymized to original resource in Orthanc Explorer +* Fixes for Red Hat and Debian packaging +* Fixes for history in Orthanc Explorer +* Fixes for boost::thread, as reported by Cyril Paulus +* Fix licensing (Closes: Debian #712038) + +Metadata +-------- + +* Access to the metadata through the REST API (.../metadata) +* Support of user-defined metadata +* "LastUpdate" metadata for patients, studies and series +* "/tools/now" to be used in combination with "LastUpdate" +* Improved support of series with temporal positions + + +Version 0.5.2 (2013/05/07) +========================== + +* "Bulk" Store-SCU (send several DICOM instances with the same + DICOM connection) +* Store-SCU for patients and studies in Orthanc Explorer +* Filtering of incoming DICOM instances (through Lua scripting) +* Filtering of incoming HTTP requests (through Lua scripting) +* Clearing of "/exports" and "/changes" +* Check MD5 of third party downloads +* Faking of the HTTP methods PUT and DELETE + + +Version 0.5.1 (2013/04/17) +========================== + +* Support of RGB images +* Fix of store SCU in release builds +* Possibility to store the SQLite index at another place than the + DICOM instances (for performance) + + +Version 0.5.0 (2013/01/31) +========================== + +Major changes +------------- + +* Download of modified or anonymized DICOM instances +* Inplace modification and anonymization of DICOM series, studies and patients + +Minor changes +------------- + +* Support of private tags +* Implementation of the PMSCT_RLE1 image decoding for Philips modalities +* Generation of random DICOM UID through the REST API (/tools/generate-uid) + + +Version 0.4.0 (2012/12/14) +========================== + +Major changes +------------- + +* Recycling of disk space +* Raw access to the value of the DICOM tags in the REST API + +Minor changes +------------- + +* Protection of patients against recycling (also in Orthanc Explorer) +* The DICOM dictionaries are embedded in Windows builds + + +Version 0.3.1 (2012/12/05) +========================== + +* Download archives of patients, studies and series as ZIP files +* Orthanc now checks the version of its database schema before starting + + +Version 0.3.0 (2012/11/30) +========================== + +Major changes +------------- + +* Transparent compression of the DICOM instances on the disk +* The patient/study/series/instances are now indexed by SHA-1 digests + of their DICOM Instance IDs (and not by UUIDs anymore): The same + DICOM objects are thus always identified by the same Orthanc IDs +* Log of exported instances through DICOM C-STORE SCU ("/exported" URI) +* Full refactoring of the DB schema and of the REST API +* Introduction of generic classes for REST APIs (in Core/RestApi) + +Minor changes +------------- + +* "/statistics" URI +* "last" flag to retrieve the last change from the "/changes" URI +* Generate a sample configuration file from command line +* "CompletedSeries" event in the changes API +* Thread to continuously flush DB to disk (SQLite checkpoints for + improved robustness) + + +Version 0.2.3 (2012/10/26) +========================== + +* Use HTTP Content-Disposition to set a filename when downloading JSON/DCM +* URI "/system" for general information about Orthanc +* Versioning info and help on the command line +* Improved logging +* Possibility of dynamic linking against jsoncpp, sqlite, boost and dmctk + for Debian packaging +* Fix some bugs +* Switch to default 8042 port for HTTP + + +Version 0.2.2 (2012/10/04) +========================== + +* Switch to Google Log +* Fixes to Debian packaging + + +Version 0.2.1 (2012/09/28) +========================== + +* Status of series +* Continuous Integration Server is up and running +* Ready for Debian packaging + + +Version 0.2.0 (2012/09/16) +========================== + +Major changes +------------- + +* Renaming to "Orthanc" +* Focus on security: Support of SSL, HTTP Basic Authentication and + interdiction of remote access +* Access to multi-frame images (for nuclear medicine) +* Access to the raw PNG images (in 8bpp and 16bpp) + +Minor changes +------------- + +* Change of the licensing of the "Core/SQLite" folder to BSD (to + reflect the original licensing terms of Chromium, from which the + code derives) +* Standalone build for cross-compilation + + +Version 0.1.1 (2012/07/20) +========================== + +* Fix Windows version +* Native Windows build with Microsoft Visual Studio 2005 +* Add path to storage in Configuration.json + + +Version 0.1.0 (2012/07/19) +========================== + +* Initial release diff --git a/OrthancFramework/COPYING b/OrthancFramework/COPYING new file mode 100644 index 0000000..5357f69 --- /dev/null +++ b/OrthancFramework/COPYING @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake b/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake new file mode 100644 index 0000000..f49954e --- /dev/null +++ b/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake @@ -0,0 +1,80 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +set(EMBED_RESOURCES_PYTHON "${CMAKE_CURRENT_LIST_DIR}/../EmbedResources.py" + CACHE INTERNAL "Path to the EmbedResources.py script from Orthanc") +set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED") +set(AUTOGENERATED_SOURCES) + +file(MAKE_DIRECTORY ${AUTOGENERATED_DIR}) +include_directories(${AUTOGENERATED_DIR}) + +macro(EmbedResources) + # Convert a semicolon separated list to a whitespace separated string + set(SCRIPT_OPTIONS) + set(SCRIPT_ARGUMENTS) + set(DEPENDENCIES) + set(IS_PATH_NAME false) + + set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources") + + # Loop over the arguments of the function + foreach(arg ${ARGN}) + # Extract the first character of the argument + string(SUBSTRING "${arg}" 0 1 FIRST_CHAR) + if (${FIRST_CHAR} STREQUAL "-") + # If the argument starts with a dash "-", this is an option to + # EmbedResources.py + if (${arg} MATCHES "--target=.*") + # Does the argument starts with "--target="? + string(SUBSTRING "${arg}" 9 -1 TARGET) # 9 is the length of "--target=" + set(TARGET_BASE "${AUTOGENERATED_DIR}/${TARGET}") + else() + list(APPEND SCRIPT_OPTIONS ${arg}) + endif() + else() + if (${IS_PATH_NAME}) + list(APPEND SCRIPT_ARGUMENTS "${arg}") + list(APPEND DEPENDENCIES "${arg}") + set(IS_PATH_NAME false) + else() + list(APPEND SCRIPT_ARGUMENTS "${arg}") + set(IS_PATH_NAME true) + endif() + endif() + endforeach() + + add_custom_command( + OUTPUT + "${TARGET_BASE}.h" + "${TARGET_BASE}.cpp" + COMMAND ${PYTHON_EXECUTABLE} ${EMBED_RESOURCES_PYTHON} + ${SCRIPT_OPTIONS} "${TARGET_BASE}" ${SCRIPT_ARGUMENTS} + DEPENDS + ${EMBED_RESOURCES_PYTHON} + ${DEPENDENCIES} + ) + + list(APPEND AUTOGENERATED_SOURCES + "${TARGET_BASE}.cpp" + ) +endmacro() diff --git a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake new file mode 100644 index 0000000..360ad51 --- /dev/null +++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake @@ -0,0 +1,451 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_BOOST) + set(BOOST_STATIC 1) +else() + # https://cmake.org/cmake/help/latest/policy/CMP0167.html + if (CMAKE_VERSION VERSION_GREATER "3.30") + find_package(Boost CONFIG) + else() + include(FindBoost) + endif() + + set(BOOST_STATIC 0) + #set(Boost_DEBUG 1) + #set(Boost_USE_STATIC_LIBS ON) + + if (ENABLE_LOCALE) + list(APPEND ORTHANC_BOOST_COMPONENTS locale) + endif() + + list(APPEND ORTHANC_BOOST_COMPONENTS filesystem thread system date_time regex iostreams) + find_package(Boost COMPONENTS ${ORTHANC_BOOST_COMPONENTS}) + + if (NOT Boost_FOUND) + foreach (item ${ORTHANC_BOOST_COMPONENTS}) + string(TOUPPER ${item} tmp) + + if (Boost_${tmp}_FOUND) + set(tmp2 "found") + else() + set(tmp2 "missing") + endif() + + message("Boost component ${item} - ${tmp2}") + endforeach() + + message(FATAL_ERROR "Unable to locate Boost on this system") + endif() + + + # Patch by xnox to fix issue #166 (CMake find_boost version is now + # broken with newer boost/cmake) + # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=166 + if (POLICY CMP0093) + set(BOOST144 1.44) + else() + set(BOOST144 104400) + endif() + + + # Boost releases 1.44 through 1.47 supply both V2 and V3 filesystem + # http://www.boost.org/doc/libs/1_46_1/libs/filesystem/v3/doc/index.htm + if (${Boost_VERSION} LESS ${BOOST144}) + add_definitions( + -DBOOST_HAS_FILESYSTEM_V3=0 + ) + else() + add_definitions( + -DBOOST_HAS_FILESYSTEM_V3=1 + -DBOOST_FILESYSTEM_VERSION=3 + ) + endif() + + include_directories(${Boost_INCLUDE_DIRS}) + link_libraries(${Boost_LIBRARIES}) +endif() + + +if (BOOST_STATIC AND USE_LEGACY_BOOST) + include(${CMAKE_CURRENT_LIST_DIR}/BoostConfigurationStatic-1.69.0.cmake) +endif() + + +if (BOOST_STATIC AND NOT USE_LEGACY_BOOST) + ## + ## Parameters for static compilation of Boost + ## + + set(BOOST_NAME boost_1_86_0) + set(BOOST_VERSION 1.86.0) + set(BOOST_BCP_SUFFIX bcpdigest-1.12.5) + set(BOOST_MD5 "20b9c325c0dde830889ee75a9e64ded8") + set(BOOST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz") + set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME}) + + if (IS_DIRECTORY "${BOOST_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + + DownloadPackage(${BOOST_MD5} ${BOOST_URL} "${BOOST_SOURCES_DIR}") + + + ## + ## Apply the patches to remove threads from boost::locale (required + ## since around Emscripten 3.x) + ## + + if (FirstRun) + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/boost-1.86.0-emscripten.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + endif() + + + ## + ## Generic configuration of Boost + ## + + if (CMAKE_COMPILER_IS_GNUCXX) + add_definitions(-isystem ${BOOST_SOURCES_DIR}) + endif() + + include_directories( + BEFORE + ${BOOST_SOURCES_DIR} + ${BOOST_SOURCES_DIR}/libs/atomic/src/ + ${BOOST_SOURCES_DIR}/libs/locale/src/ + ) + + if (ORTHANC_BUILDING_FRAMEWORK_LIBRARY) + add_definitions( + # Packaging Boost inside the Orthanc Framework DLL + -DBOOST_ALL_DYN_LINK # Expose Boost symbols into the DLL + -DBOOST_THREAD_BUILD_DLL + -DBOOST_REGEX_BUILD_DLL + -DBOOST_IOSTREAMS_SOURCE + ) + else() + add_definitions( + # Static build of Boost (this was the only possibility in + # Orthanc <= 1.7.1) + -DBOOST_ALL_NO_LIB + -DBOOST_ALL_NOLIB + -DBOOST_DATE_TIME_NO_LIB + -DBOOST_THREAD_BUILD_LIB + -DBOOST_PROGRAM_OPTIONS_NO_LIB + -DBOOST_REGEX_NO_LIB + -DBOOST_SYSTEM_NO_LIB + -DBOOST_LOCALE_NO_LIB + ) + endif() + + add_definitions( + # In static builds, explicitly prevent Boost from using the system + # locale in lexical casts. This is notably important if + # "boost::lexical_cast()" is applied to strings containing + # "," instead of "." as decimal separators. Check out function + # "OrthancStone::LinearAlgebra::ParseVector()". + -DBOOST_LEXICAL_CAST_ASSUME_C_LOCALE + + # Those definitions are necessary since Boost 1.80.0 + # https://github.com/RGLab/cytolib/issues/49 + -DBOOST_NO_AUTO_PTR + -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF + -DBOOST_FILESYSTEM_HAS_POSIX_AT_APIS + ) + + set(BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp + ) + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR + "${CMAKE_SYSTEM_NAME}" STREQUAL "Android") + add_definitions( + -DBOOST_SYSTEM_USE_STRERROR=1 + ) + endif() + + + ## + ## Configuration of boost::thread + ## + + if (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR + CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "kFreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR + CMAKE_SYSTEM_NAME STREQUAL "Android") + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/atomic/src/lock_pool.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/once.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/thread.cpp + ) + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR + CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64") + add_definitions(-DBOOST_HAS_SCHED_YIELD=1) + endif() + + # Fix for error: "boost_1_69_0/boost/chrono/detail/inlined/mac/thread_clock.hpp:54:28: + # error: use of undeclared identifier 'pthread_mach_thread_np'" + # https://github.com/envoyproxy/envoy/pull/1785 + if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + add_definitions(-D_DARWIN_C_SOURCE=1) + endif() + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/win32/thread.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_pe.cpp + ) + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # No support for threads in asm.js/WebAssembly + + else() + message(FATAL_ERROR "Support your platform here") + endif() + + + ## + ## Configuration of boost::regex + ## + + aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES) + + list(APPEND BOOST_SOURCES + ${BOOST_REGEX_SOURCES} + ) + + + ## + ## Configuration of boost::datetime + ## + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/date_time/src/gregorian/greg_month.cpp + ) + + + ## + ## Configuration of boost::filesystem and boost::iostreams + ## + + if (CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR + CMAKE_SYSTEM_NAME STREQUAL "Android") + # boost::filesystem is not available on PNaCl + add_definitions( + -DBOOST_HAS_FILESYSTEM_V3=0 + -D__INTEGRITY=1 + ) + else() + add_definitions( + -DBOOST_HAS_FILESYSTEM_V3=1 + ) + list(APPEND BOOST_SOURCES + ${BOOST_NAME}/libs/filesystem/src/codecvt_error_category.cpp + ${BOOST_NAME}/libs/filesystem/src/directory.cpp + ${BOOST_NAME}/libs/filesystem/src/exception.cpp + ${BOOST_NAME}/libs/filesystem/src/operations.cpp + ${BOOST_NAME}/libs/filesystem/src/path.cpp + ${BOOST_NAME}/libs/filesystem/src/path_traits.cpp + ${BOOST_NAME}/libs/filesystem/src/portability.cpp + ${BOOST_NAME}/libs/filesystem/src/unique_path.cpp + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/filesystem/src/utf8_codecvt_facet.cpp + ) + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(APPEND BOOST_SOURCES + ${BOOST_NAME}/libs/filesystem/src/windows_file_codecvt.cpp + ) + endif() + endif() + + list(APPEND BOOST_SOURCES + ${BOOST_NAME}/libs/iostreams/src/file_descriptor.cpp + ) + + + ## + ## Configuration of boost::locale + ## + + if (NOT ENABLE_LOCALE) + message("boost::locale is disabled") + else() + set(BOOST_ICU_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/boundary.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/codecvt.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/collator.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/conversion.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/date_time.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/formatter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/formatters_cache.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/icu_backend.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/icu/time_zone.cpp + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/encoding/codepage.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/date_time.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/formatting.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/generator.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/iconv_codecvt.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/ids.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/localization_backend.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/message.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/mo_lambda.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/codecvt_converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/default_locale.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/encoding.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/gregorian.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/info.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/locale_data.cpp + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR + CMAKE_SYSTEM_VERSION STREQUAL "LinuxStandardBase") + add_definitions( + -DBOOST_LOCALE_NO_WINAPI_BACKEND=1 + -DBOOST_LOCALE_NO_POSIX_BACKEND=1 + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/std/codecvt.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/std/collate.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/std/converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/std/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/std/std_backend.cpp + ) + + if (BOOST_LOCALE_BACKEND STREQUAL "gcc" OR + BOOST_LOCALE_BACKEND STREQUAL "libiconv") + add_definitions(-DBOOST_LOCALE_WITH_ICONV=1) + elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + add_definitions(-DBOOST_LOCALE_WITH_ICU=1) + list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES}) + else() + message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}") + endif() + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR + CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "kFreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR + CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # For WebAssembly or asm.js + add_definitions( + -DBOOST_LOCALE_NO_WINAPI_BACKEND=1 + -DBOOST_LOCALE_NO_STD_BACKEND=1 + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/posix/codecvt.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/posix/collate.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/posix/converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/posix/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/posix/posix_backend.cpp + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR + BOOST_LOCALE_BACKEND STREQUAL "gcc" OR + BOOST_LOCALE_BACKEND STREQUAL "libiconv") + # In WebAssembly or asm.js, we rely on the version of iconv + # that is shipped with the stdlib + add_definitions(-DBOOST_LOCALE_WITH_ICONV=1) + elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + add_definitions(-DBOOST_LOCALE_WITH_ICU=1) + list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES}) + else() + message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}") + endif() + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_definitions( + -DBOOST_LOCALE_NO_POSIX_BACKEND=1 + -DBOOST_LOCALE_NO_STD_BACKEND=1 + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/win32/collate.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/win32/converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/win32/lcid.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/win32/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/win32/win_backend.cpp + ) + + # Starting with release 0.8.2, Orthanc statically links against + # libiconv on Windows. Indeed, the "WCONV" library of Windows XP + # seems not to support properly several codepages (notably + # "Latin3", "Hebrew", and "Arabic"). Set "BOOST_LOCALE_BACKEND" + # to "wconv" to use WCONV anyway. + + if (BOOST_LOCALE_BACKEND STREQUAL "libiconv") + add_definitions(-DBOOST_LOCALE_WITH_ICONV=1) + elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + add_definitions(-DBOOST_LOCALE_WITH_ICU=1) + list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES}) + elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv") + message("Using Window's wconv") + add_definitions(-DBOOST_LOCALE_WITH_WCONV=1) + else() + message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND on Windows: ${BOOST_LOCALE_BACKEND}") + endif() + + else() + message(FATAL_ERROR "Support your platform here") + endif() + endif() + + + source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*) + +endif() diff --git a/OrthancFramework/Resources/CMake/BoostConfiguration.sh b/OrthancFramework/Resources/CMake/BoostConfiguration.sh new file mode 100755 index 0000000..803d381 --- /dev/null +++ b/OrthancFramework/Resources/CMake/BoostConfiguration.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -e +set -u + +## Starting with version 0.6.2, Orthanc is shipped with a subset of the +## Boost libraries that is generated with the BCP tool: +## +## http://www.boost.org/doc/libs/1_54_0/tools/bcp/doc/html/index.html +## +## This script generates this subset. +## +## History: +## - Orthanc between 0.6.2 and 0.7.3: Boost 1.54.0 +## - Orthanc between 0.7.4 and 0.9.1: Boost 1.55.0 +## - Orthanc between 0.9.2 and 0.9.4: Boost 1.58.0 +## - Orthanc between 0.9.5 and 1.0.0: Boost 1.59.0 +## - Orthanc between 1.1.0 and 1.2.0: Boost 1.60.0 +## - Orthanc 1.3.0: Boost 1.64.0 +## - Orthanc 1.3.1: Boost 1.65.1 +## - Orthanc 1.3.2: Boost 1.66.0 +## - Orthanc between 1.4.0 and 1.4.2: Boost 1.67.0 +## - Orthanc between 1.5.0 and 1.5.4: Boost 1.68.0 +## - Orthanc between 1.5.5 and 1.11.1: Boost 1.69.0 +## - Orthanc between 1.11.2 and 1.12.0: Boost 1.80.0 +## - Orthanc 1.12.1: Boost 1.82.0 +## - Orthanc 1.12.2: Boost 1.83.0 +## - Orthanc 1.12.3: Boost 1.84.0 +## - Orthanc > 1.12.3: Boost 1.85.0 +## - Orthanc 1.12.5: Boost 1.86.0 + +BOOST_VERSION=1_86_0 +ORTHANC_VERSION=1.12.5 + +rm -rf /tmp/boost_${BOOST_VERSION} +rm -rf /tmp/bcp/boost_${BOOST_VERSION} + +cd /tmp +echo "Uncompressing the sources of Boost ${BOOST_VERSION}..." +tar xfz ./boost_${BOOST_VERSION}.tar.gz + +echo "Generating the subset..." +mkdir -p /tmp/bcp/boost_${BOOST_VERSION} +bcp --boost=/tmp/boost_${BOOST_VERSION} thread system locale date_time filesystem math/special_functions algorithm uuid atomic iostreams program_options numeric/ublas geometry polygon signals2 chrono /tmp/bcp/boost_${BOOST_VERSION} + +echo "Removing documentation..." +rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/locale/doc/html +rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/algorithm/doc/html +rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/geometry/doc/html +rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/geometry/doc/doxy/doxygen_output/html +rm -rf /tmp/bcp/boost_${BOOST_VERSION}/libs/filesystem/example/ + +# https://stackoverflow.com/questions/1655372/longest-line-in-a-file +LONGEST_FILENAME=`find /tmp/bcp/ | awk '{print length, $0}' | sort -nr | head -1` +LONGEST=`echo "$LONGEST_FILENAME" | cut -d ' ' -f 1` + +echo +echo "Longest filename (${LONGEST} characters):" +echo "${LONGEST_FILENAME}" +echo + +if [ ${LONGEST} -ge 128 ]; then + echo "ERROR: Too long filename for Windows!" + echo + exit -1 +fi + +echo "Compressing the subset..." +cd /tmp/bcp +tar cfz boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz boost_${BOOST_VERSION} +ls -l boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz +md5sum boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz +readlink -f boost_${BOOST_VERSION}_bcpdigest-${ORTHANC_VERSION}.tar.gz diff --git a/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake b/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake new file mode 100644 index 0000000..1fe804d --- /dev/null +++ b/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake @@ -0,0 +1,361 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (BOOST_STATIC) + ## + ## Parameters for static compilation of Boost + ## + + set(BOOST_NAME boost_1_69_0) + set(BOOST_VERSION 1.69.0) + set(BOOST_BCP_SUFFIX bcpdigest-1.5.6) + set(BOOST_MD5 "579bccc0ea4d1a261c1d0c5e27446c3d") + set(BOOST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz") + set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME}) + + if (IS_DIRECTORY "${BOOST_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + + DownloadPackage(${BOOST_MD5} ${BOOST_URL} "${BOOST_SOURCES_DIR}") + + + ## + ## Patching boost + ## + + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/boost-${BOOST_VERSION}-linux-standard-base.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (FirstRun AND Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + + ## + ## Generic configuration of Boost + ## + + if (CMAKE_COMPILER_IS_GNUCXX) + add_definitions(-isystem ${BOOST_SOURCES_DIR}) + endif() + + include_directories( + BEFORE ${BOOST_SOURCES_DIR} + ) + + if (ORTHANC_BUILDING_FRAMEWORK_LIBRARY) + add_definitions( + # Packaging Boost inside the Orthanc Framework DLL + -DBOOST_ALL_DYN_LINK # Expose Boost symbols into the DLL + -DBOOST_THREAD_BUILD_DLL + -DBOOST_REGEX_BUILD_DLL + -DBOOST_IOSTREAMS_SOURCE + ) + else() + add_definitions( + # Static build of Boost (this was the only possibility in + # Orthanc <= 1.7.1) + -DBOOST_ALL_NO_LIB + -DBOOST_ALL_NOLIB + -DBOOST_DATE_TIME_NO_LIB + -DBOOST_THREAD_BUILD_LIB + -DBOOST_PROGRAM_OPTIONS_NO_LIB + -DBOOST_REGEX_NO_LIB + -DBOOST_SYSTEM_NO_LIB + -DBOOST_LOCALE_NO_LIB + ) + endif() + + add_definitions( + # In static builds, explicitly prevent Boost from using the system + # locale in lexical casts. This is notably important if + # "boost::lexical_cast()" is applied to strings containing + # "," instead of "." as decimal separators. Check out function + # "OrthancStone::LinearAlgebra::ParseVector()". + -DBOOST_LEXICAL_CAST_ASSUME_C_LOCALE + ) + + set(BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp + ) + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR + "${CMAKE_SYSTEM_NAME}" STREQUAL "Android") + add_definitions( + -DBOOST_SYSTEM_USE_STRERROR=1 + ) + endif() + + + ## + ## Configuration of boost::thread + ## + + if (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR + CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "kFreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR + CMAKE_SYSTEM_NAME STREQUAL "Android") + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/atomic/src/lockpool.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/once.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/thread.cpp + ) + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR + CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64") + add_definitions(-DBOOST_HAS_SCHED_YIELD=1) + endif() + + # Fix for error: "boost_1_69_0/boost/chrono/detail/inlined/mac/thread_clock.hpp:54:28: + # error: use of undeclared identifier 'pthread_mach_thread_np'" + # https://github.com/envoyproxy/envoy/pull/1785 + if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + add_definitions(-D_DARWIN_C_SOURCE=1) + endif() + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/win32/thread.cpp + ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_pe.cpp + ) + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # No support for threads in asm.js/WebAssembly + + else() + message(FATAL_ERROR "Support your platform here") + endif() + + + ## + ## Configuration of boost::regex + ## + + aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES) + + list(APPEND BOOST_SOURCES + ${BOOST_REGEX_SOURCES} + ) + + + ## + ## Configuration of boost::datetime + ## + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/date_time/src/gregorian/greg_month.cpp + ) + + + ## + ## Configuration of boost::filesystem and boost::iostreams + ## + + if (CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR + CMAKE_SYSTEM_NAME STREQUAL "Android") + # boost::filesystem is not available on PNaCl + add_definitions( + -DBOOST_HAS_FILESYSTEM_V3=0 + -D__INTEGRITY=1 + ) + else() + add_definitions( + -DBOOST_HAS_FILESYSTEM_V3=1 + ) + list(APPEND BOOST_SOURCES + ${BOOST_NAME}/libs/filesystem/src/codecvt_error_category.cpp + ${BOOST_NAME}/libs/filesystem/src/operations.cpp + ${BOOST_NAME}/libs/filesystem/src/path.cpp + ${BOOST_NAME}/libs/filesystem/src/path_traits.cpp + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/filesystem/src/utf8_codecvt_facet.cpp + ) + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(APPEND BOOST_SOURCES + ${BOOST_NAME}/libs/filesystem/src/windows_file_codecvt.cpp + ) + endif() + endif() + + list(APPEND BOOST_SOURCES + ${BOOST_NAME}/libs/iostreams/src/file_descriptor.cpp + ) + + + ## + ## Configuration of boost::locale + ## + + if (NOT ENABLE_LOCALE) + message("boost::locale is disabled") + else() + set(BOOST_ICU_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/boundary.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/codecvt.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/collator.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/conversion.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/date_time.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/formatter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/icu_backend.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/icu/time_zone.cpp + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/encoding/codepage.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/shared/generator.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/shared/date_time.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/shared/formatting.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/shared/ids.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/shared/localization_backend.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/shared/message.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/shared/mo_lambda.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/util/codecvt_converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/util/default_locale.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/util/gregorian.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/util/info.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/util/locale_data.cpp + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR + CMAKE_SYSTEM_VERSION STREQUAL "LinuxStandardBase") + add_definitions( + -DBOOST_LOCALE_NO_WINAPI_BACKEND=1 + -DBOOST_LOCALE_NO_POSIX_BACKEND=1 + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/std/codecvt.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/std/collate.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/std/converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/std/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/std/std_backend.cpp + ) + + if (BOOST_LOCALE_BACKEND STREQUAL "gcc" OR + BOOST_LOCALE_BACKEND STREQUAL "libiconv") + add_definitions(-DBOOST_LOCALE_WITH_ICONV=1) + elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + add_definitions(-DBOOST_LOCALE_WITH_ICU=1) + list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES}) + else() + message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}") + endif() + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR + CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "kFreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "PNaCl" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR + CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR + CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # For WebAssembly or asm.js + add_definitions( + -DBOOST_LOCALE_NO_WINAPI_BACKEND=1 + -DBOOST_LOCALE_NO_STD_BACKEND=1 + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/posix/codecvt.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/posix/collate.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/posix/converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/posix/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/posix/posix_backend.cpp + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR + BOOST_LOCALE_BACKEND STREQUAL "gcc" OR + BOOST_LOCALE_BACKEND STREQUAL "libiconv") + # In WebAssembly or asm.js, we rely on the version of iconv + # that is shipped with the stdlib + add_definitions(-DBOOST_LOCALE_WITH_ICONV=1) + elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + add_definitions(-DBOOST_LOCALE_WITH_ICU=1) + list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES}) + else() + message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}") + endif() + + elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_definitions( + -DBOOST_LOCALE_NO_POSIX_BACKEND=1 + -DBOOST_LOCALE_NO_STD_BACKEND=1 + ) + + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/locale/src/win32/collate.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/win32/converter.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/win32/lcid.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/win32/numeric.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/win32/win_backend.cpp + ) + + # Starting with release 0.8.2, Orthanc statically links against + # libiconv on Windows. Indeed, the "WCONV" library of Windows XP + # seems not to support properly several codepages (notably + # "Latin3", "Hebrew", and "Arabic"). Set "BOOST_LOCALE_BACKEND" + # to "wconv" to use WCONV anyway. + + if (BOOST_LOCALE_BACKEND STREQUAL "libiconv") + add_definitions(-DBOOST_LOCALE_WITH_ICONV=1) + elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + add_definitions(-DBOOST_LOCALE_WITH_ICU=1) + list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES}) + elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv") + message("Using Window's wconv") + add_definitions(-DBOOST_LOCALE_WITH_WCONV=1) + else() + message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND on Windows: ${BOOST_LOCALE_BACKEND}") + endif() + + else() + message(FATAL_ERROR "Support your platform here") + endif() + endif() + + + source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*) + +endif() diff --git a/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake new file mode 100644 index 0000000..1787c64 --- /dev/null +++ b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake @@ -0,0 +1,143 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_CIVETWEB) + + ## WARNING: "civetweb-1.14.tar.gz" comes with a subfolder + ## "civetweb-1.14/test/nonlatin" that cannot be removed by "hg purge + ## --all" on Windows hosts. We thus created a custom + ## "civetweb-1.14-fixed.tar.gz" as follows: + ## + ## $ cd /tmp + ## $ wget https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.14.tar.gz + ## $ tar xvf civetweb-1.14.tar.gz + ## $ rm -rf civetweb-1.14/src/third_party/ civetweb-1.14/test/ + ## $ tar cvfz civetweb-1.14-fixed.tar.gz civetweb-1.14 + ## + + set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.14) + set(CIVETWEB_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.14-fixed.tar.gz") + set(CIVETWEB_MD5 "1f25d516b7a4e65d8b270d1cc399e0a9") + + if (IS_DIRECTORY "${CIVETWEB_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + + DownloadPackage(${CIVETWEB_MD5} ${CIVETWEB_URL} "${CIVETWEB_SOURCES_DIR}") + + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/civetweb-1.14.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (FirstRun AND Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + include_directories( + ${CIVETWEB_SOURCES_DIR}/include + ) + + set(CIVETWEB_SOURCES + ${CIVETWEB_SOURCES_DIR}/src/civetweb.c + ) + + # New in Orthanc 1.6.0: Enable support of compression in civetweb + set(tmp "USE_ZLIB=1") + + if (ENABLE_SSL) + add_definitions( + -DNO_SSL_DL=1 + ) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") + link_libraries(dl) + endif() + + if (CIVETWEB_OPENSSL_API STREQUAL "1.0") + set(tmp "${tmp};OPENSSL_API_1_0=1") + elseif (CIVETWEB_OPENSSL_API STREQUAL "1.1") + set(tmp "${tmp};OPENSSL_API_1_1=1") + else() + message(FATAL_ERROR "Unsupported value for CIVETWEB_OPENSSL_API: ${CIVETWEB_OPENSSL_API}") + endif() + + else() + add_definitions( + -DNO_SSL=1 # Remove SSL support from civetweb + ) + endif() + + set_source_files_properties( + ${CIVETWEB_SOURCES} + PROPERTIES COMPILE_DEFINITIONS "${tmp}" + ) + + source_group(ThirdParty\\Civetweb REGULAR_EXPRESSION ${CIVETWEB_SOURCES_DIR}/.*) + + add_definitions( + -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1 + -DCIVETWEB_HAS_WEBDAV_WRITING=1 + ) + +else() + CHECK_INCLUDE_FILE_CXX(civetweb.h HAVE_CIVETWEB_H) + if (NOT HAVE_CIVETWEB_H) + message(FATAL_ERROR "Please install the libcivetweb-dev package") + endif() + + cmake_reset_check_state() + set(CMAKE_REQUIRED_LIBRARIES dl pthread) + CHECK_LIBRARY_EXISTS(civetweb mg_start "" HAVE_CIVETWEB_LIB) + if (NOT HAVE_CIVETWEB_LIB) + message(FATAL_ERROR "Please install the libcivetweb-dev package") + endif() + + link_libraries(civetweb) + + # Check whether the system distribution of civetweb contains the + # patch "../Patches/civetweb-1.13.patch" that allows to disable + # keep-alive on selected HTTP connections. This is useful to speed + # up multipart transfers, as encountered in DICOMweb. + CHECK_LIBRARY_EXISTS(civetweb mg_disable_keep_alive "" CIVETWEB_HAS_DISABLE_KEEP_ALIVE_1) # From "../Patches/civetweb-1.13.patch" + CHECK_LIBRARY_EXISTS(civetweb mg_disable_connection_keep_alive "" CIVETWEB_HAS_DISABLE_KEEP_ALIVE_2) # From civetweb >= 1.14 + if (CIVETWEB_HAS_DISABLE_KEEP_ALIVE_1 OR + CIVETWEB_HAS_DISABLE_KEEP_ALIVE_2) + add_definitions( + -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1 + -DCIVETWEB_HAS_WEBDAV_WRITING=1 + ) + message("Performance: Your system-wide distribution of civetweb is configured for best performance") + else() + message(WARNING "Performance: Your system-wide distribution of civetweb does not feature the mg_disable_keep_alive() function, and WebDAV will only be available for read-only access") + add_definitions( + -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0 + -DCIVETWEB_HAS_WEBDAV_WRITING=0 + ) + endif() + + unset(CMAKE_REQUIRED_LIBRARIES) # This reset must be after "CHECK_LIBRARY_EXISTS" +endif() diff --git a/OrthancFramework/Resources/CMake/Compiler.cmake b/OrthancFramework/Resources/CMake/Compiler.cmake new file mode 100644 index 0000000..a6c484d --- /dev/null +++ b/OrthancFramework/Resources/CMake/Compiler.cmake @@ -0,0 +1,315 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +# This file sets all the compiler-related flags + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + # Since Orthanc 1.12.7 that allows CMake 4.0, builds for macOS + # require the C++ standard to be explicitly set to C++11. Do *not* + # do this on GNU/Linux, as third-party system libraries could have + # been compiled with higher versions of the C++ standard. + set(CMAKE_CXX_STANDARD 11) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) +endif() + + +# Save the current compiler flags to the cache every time cmake configures the project +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "compiler flags" FORCE) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}" CACHE STRING "compiler flags" FORCE) + + +include(CheckLibraryExists) + +if ((CMAKE_CROSSCOMPILING AND NOT + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR + "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Cross-compilation necessarily implies standalone and static build + SET(STATIC_BUILD ON) + SET(STANDALONE_BUILD ON) +endif() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Cache the environment variables "LSB_CC" and "LSB_CXX" for further + # use by "ExternalProject" in CMake + SET(CMAKE_LSB_CC $ENV{LSB_CC} CACHE STRING "") + SET(CMAKE_LSB_CXX $ENV{LSB_CXX} CACHE STRING "") + + # This is necessary to build "Orthanc mainline - Framework LSB + # Release" on "buildbot-worker-debian11" + set(LSB_PTHREAD_NONSHARED "${LSB_PATH}/lib64-${LSB_TARGET_VERSION}/libpthread_nonshared.a") + if (EXISTS ${LSB_PTHREAD_NONSHARED}) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LSB_PTHREAD_NONSHARED}") + endif() +endif() + + +if (CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long") + + # --std=c99 makes libcurl not to compile + # -pedantic gives a lot of warnings on OpenSSL + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -Wno-variadic-macros") + + if (CMAKE_CROSSCOMPILING) + # http://stackoverflow.com/a/3543845/881731 + set(CMAKE_RC_COMPILE_OBJECT " -O coff -I ") + endif() + +elseif (MSVC) + # Use static runtime under Visual Studio + # http://www.cmake.org/Wiki/CMake_FAQ#Dynamic_Replace + # http://stackoverflow.com/a/6510446 + foreach(flag_var + CMAKE_C_FLAGS_DEBUG + CMAKE_CXX_FLAGS_DEBUG + CMAKE_C_FLAGS_RELEASE + CMAKE_CXX_FLAGS_RELEASE + CMAKE_C_FLAGS_MINSIZEREL + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_C_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS_RELWITHDEBINFO) + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}") + endforeach(flag_var) + + # Add /Zm256 compiler option to Visual Studio to fix PCH errors + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256") + + # New in Orthanc 1.5.5 + if (MSVC_MULTIPLE_PROCESSES) + # "If you omit the processMax argument in the /MP option, the + # compiler obtains the number of effective processors from the + # operating system, and then creates one process per effective + # processor" + # https://blog.kitware.com/cmake-building-with-all-your-cores/ + # https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") + endif() + + add_definitions( + -D_CRT_SECURE_NO_WARNINGS=1 + -D_CRT_SECURE_NO_DEPRECATE=1 + ) + + if (MSVC_VERSION LESS 1600) + # Starting with Visual Studio >= 2010 (i.e. macro _MSC_VER >= + # 1600), Microsoft ships a standard-compliant + # header. For earlier versions of Visual Studio, give access to a + # compatibility header. + # http://stackoverflow.com/a/70630/881731 + # https://en.wikibooks.org/wiki/C_Programming/C_Reference/stdint.h#External_links + include_directories(${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/VisualStudio) + endif() + + link_libraries(netapi32) +endif() + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # In FreeBSD/OpenBSD, the "/usr/local/" folder contains the ports and need to be imported + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I/usr/local/include") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L/usr/local/lib") +endif() + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + + if (# NOT ${CMAKE_SYSTEM_VERSION} STREQUAL "LinuxStandardBase" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + # The "--no-undefined" linker flag makes the shared libraries + # (plugins ModalityWorklists and ServeFolders) fail to compile on + # OpenBSD, and make the PostgreSQL plugin complain about missing + # "environ" global variable in FreeBSD. + # + # TODO - Furthermore, on Linux Standard Base running on Debian 12, + # the "-Wl,--no-undefined" seems to break the compilation (added + # after Orthanc 1.12.2). This is disabled for now. + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") + endif() + + # Remove the "-rdynamic" option + # http://www.mail-archive.com/cmake@cmake.org/msg08837.html + set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") + link_libraries(pthread) + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + link_libraries(rt) + endif() + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + link_libraries(dl) + endif() + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # The "--as-needed" linker flag is not available on FreeBSD and OpenBSD + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--as-needed") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--as-needed") + endif() + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # FreeBSD/OpenBSD have just one single interface for file + # handling, which is 64bit clean, so there is no need to define macro + # for LFS (Large File Support). + # https://ohse.de/uwe/articles/lfs.html + add_definitions( + -D_LARGEFILE64_SOURCE=1 + -D_FILE_OFFSET_BITS=64 + ) + endif() + +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + if (MSVC) + message("MSVC compiler version = " ${MSVC_VERSION} "\n") + # Starting Visual Studio 2013 (version 1800), it is not possible + # to target Windows XP anymore + if (MSVC_VERSION LESS 1800) + add_definitions( + -DWINVER=0x0501 + -D_WIN32_WINNT=0x0501 + ) + endif() + else() + add_definitions( + -DWINVER=0x0501 + -D_WIN32_WINNT=0x0501 + ) + endif() + + add_definitions( + -D_CRT_SECURE_NO_WARNINGS=1 + ) + link_libraries(rpcrt4 ws2_32 iphlpapi) # "iphlpapi" is for "SystemToolbox::GetMacAddresses()" + + if (CMAKE_COMPILER_IS_GNUCXX) + # Some additional C/C++ compiler flags for MinGW + SET(MINGW_NO_WARNINGS "-Wno-unused-function -Wno-unused-variable") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MINGW_NO_WARNINGS} -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MINGW_NO_WARNINGS}") + + if (DYNAMIC_MINGW_STDLIB) + else() + # This is a patch for MinGW64 + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++") + endif() + + CHECK_LIBRARY_EXISTS(winpthread pthread_create "" HAVE_WIN_PTHREAD) + if (HAVE_WIN_PTHREAD) + if (DYNAMIC_MINGW_STDLIB) + else() + # This line is necessary to compile with recent versions of MinGW, + # otherwise "libwinpthread-1.dll" is not statically linked. + SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic") + endif() + add_definitions(-DHAVE_WIN_PTHREAD=1) + else() + add_definitions(-DHAVE_WIN_PTHREAD=0) + endif() + endif() + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + + # fix this error that appears with recent compilers on MacOS: boost/mpl/aux_/integral_wrapper.hpp:73:31: error: integer value -1 is outside the valid range of values [0, 3] for this enumeration type [-Wenum-constexpr-conversion] + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-enum-constexpr-conversion") + + add_definitions( + -D_XOPEN_SOURCE=1 + ) + + # Linking with iconv breaks the Universal builds on modern compilers + # link_libraries(iconv) + +elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + message("Building using Emscripten (for WebAssembly or asm.js targets)") + include(${CMAKE_CURRENT_LIST_DIR}/EmscriptenParameters.cmake) + +elseif (CMAKE_SYSTEM_NAME STREQUAL "Android") + +else() + message("Unknown target platform: ${CMAKE_SYSTEM_NAME}") + message(FATAL_ERROR "Support your platform here") +endif() + + +if (DEFINED ENABLE_PROFILING AND ENABLE_PROFILING) + if (CMAKE_COMPILER_IS_GNUCXX OR + CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pg") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -pg") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pg") + else() + message(FATAL_ERROR "Don't know how to enable profiling on your configuration") + endif() +endif() + + +if (CMAKE_COMPILER_IS_GNUCXX) + # "When creating a static library using binutils (ar) and there + # exist a duplicate object name (e.g. a/Foo.cpp.o, b/Foo.cpp.o), the + # resulting static library can end up having only one of the + # duplicate objects. [...] This bug only happens if there are many + # objects." The trick consists in replacing the "r" argument + # ("replace") provided to "ar" (as used in CMake < 3.1) by the "q" + # argument ("quick append"). This is because of the fact that CMake + # will invoke "ar" several times with several batches of ".o" + # objects, and using "r" would overwrite symbols defined in + # preceding batches. https://cmake.org/Bug/view.php?id=14874 + set(CMAKE_CXX_ARCHIVE_APPEND " q ") +endif() + + +# This function defines macro "__ORTHANC_FILE__" as a replacement to +# macro "__FILE__", as the latter leaks the full path of the source +# files in the binaries +# https://stackoverflow.com/questions/8487986/file-macro-shows-full-path +# https://twitter.com/wget42/status/1676877802375634944?s=20 +function(DefineSourceBasenameForTarget targetname) + # Microsoft Visual Studio is extremely slow if using + # "set_property()", we only enable this feature for gcc and clang + if (CMAKE_COMPILER_IS_GNUCXX OR + CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + get_target_property(source_files "${targetname}" SOURCES) + foreach(sourcefile ${source_files}) + get_filename_component(basename "${sourcefile}" NAME) + set_property( + SOURCE "${sourcefile}" APPEND + PROPERTY COMPILE_DEFINITIONS "__ORTHANC_FILE__=\"${basename}\"") + endforeach() + endif() +endfunction() diff --git a/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake b/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake new file mode 100644 index 0000000..191060d --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake @@ -0,0 +1,335 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (NOT DEFINED ENABLE_DCMTK_NETWORKING) + set(ENABLE_DCMTK_NETWORKING ON) +endif() + +if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK) + if (DCMTK_STATIC_VERSION STREQUAL "3.6.0") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.0.cmake) + elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.2") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.2.cmake) + elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.4") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.4.cmake) + elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.5") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.5.cmake) + elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.6") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.6.cmake) + elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.7") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.7.cmake) + elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.8") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.8.cmake) + elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.9") + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.9.cmake) + else() + message(FATAL_ERROR "Unsupported version of DCMTK: ${DCMTK_STATIC_VERSION}") + endif() + + + ## + ## Commands shared by all versions of DCMTK + ## + + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmdata/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/ofstd/libsrc DCMTK_SOURCES) + + LIST(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdictbi.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdeftag.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/ofstd/libsrc/offilsys.cc + ${DCMTK_SOURCES_DIR}/ofstd/libsrc/ofwhere.c # Needed since DCMTK 3.6.9 + ) + endif() + + if (ENABLE_DCMTK_NETWORKING) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmnet/libsrc DCMTK_SOURCES) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmnet/include + ) + endif() + + if (ENABLE_DCMTK_TRANSCODING) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimgle/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimage/libsrc DCMTK_SOURCES) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmimage/include + ) + endif() + + if (ENABLE_DCMTK_TRANSCODING OR ENABLE_DCMTK_JPEG OR ENABLE_DCMTK_JPEG_LOSSLESS) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmimgle/include + ) + endif() + + if (ENABLE_DCMTK_JPEG) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8 DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12 DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg16 DCMTK_SOURCES) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmjpeg/include + ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8 + ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12 + ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg16 + ) + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/ddpiimpl.cc + + # Solves linking problem in WebAssembly: "wasm-ld: error: + # duplicate symbol: jaritab" (modification in Orthanc 1.5.9) + ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8/jaricom.c + ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12/jaricom.c + ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg24/jaricom.c + ) + + if (NOT ENABLE_DCMTK_TRANSCODING) + list(REMOVE_ITEM DCMTK_SOURCES + # Disable support for encoding JPEG (modification in Orthanc 1.0.1) + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djcodece.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsv1.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencbas.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencpro.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djenclol.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencode.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencext.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsps.cc + ) + endif() + endif() + + + if (ENABLE_DCMTK_JPEG_LOSSLESS) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpls/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpls/libcharls DCMTK_SOURCES) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmjpeg/include + ${DCMTK_SOURCES_DIR}/dcmjpls/include + ${DCMTK_SOURCES_DIR}/dcmjpls/libcharls + ) + list(APPEND DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djrplol.cc + ) + + if (NOT ENABLE_DCMTK_TRANSCODING) + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djcodece.cc + + # Disable support for encoding JPEG-LS (modification in Orthanc 1.0.1) + ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djencode.cc + ) + endif() + endif() + + + # New in Orthanc 1.9.0 for DICOM TLS + if (ENABLE_DCMTK_NETWORKING AND ENABLE_SSL) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmtls/libsrc DCMTK_SOURCES) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmtls/include + ) + + if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL) + # The function "SSL_CTX_get0_param()" is available on both + # OpenSSL 1.0.x and 1.1.x that are used for static builds + set(HAVE_SSL_CTX_GET0_PARAM ON) + else() + # The call below requires "OpenSslConfiguration.cmake" to have + # been included beforehand (which is automatically done if using + # "OrthancFrameworkConfiguration.cmake") + CHECK_LIBRARY_EXISTS(ssl "SSL_CTX_get0_param" "" HAVE_SSL_CTX_GET0_PARAM) + endif() + + if (HAVE_SSL_CTX_GET0_PARAM) + message("Have SSL_CTX_get0_param(): yes") + add_definitions(-DHAVE_SSL_CTX_GET0_PARAM=1) + else() + message("Have SSL_CTX_get0_param(): no") + endif() + + add_definitions(-DWITH_OPENSSL=1) + endif() + + + # This fixes crashes related to the destruction of the DCMTK OFLogger + # http://support.dcmtk.org/docs-snapshot/file_macros.html + add_definitions( + -DLOG4CPLUS_DISABLE_FATAL=1 + -DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER} + ) + + + if (NOT ENABLE_DCMTK_LOG) + # Disable logging internal to DCMTK + # https://groups.google.com/d/msg/orthanc-users/v2SzzAmY948/VxT1QVGiBAAJ + add_definitions( + -DDCMTK_LOG4CPLUS_DISABLE_FATAL=1 + -DDCMTK_LOG4CPLUS_DISABLE_ERROR=1 + -DDCMTK_LOG4CPLUS_DISABLE_WARN=1 + -DDCMTK_LOG4CPLUS_DISABLE_INFO=1 + -DDCMTK_LOG4CPLUS_DISABLE_DEBUG=1 + ) + endif() + + include_directories( + #${DCMTK_SOURCES_DIR} + ${DCMTK_SOURCES_DIR}/config/include + ${DCMTK_SOURCES_DIR}/ofstd/include + ${DCMTK_SOURCES_DIR}/oflog/include + ${DCMTK_SOURCES_DIR}/dcmdata/include + ) + + source_group(ThirdParty\\Dcmtk REGULAR_EXPRESSION ${DCMTK_SOURCES_DIR}/.*) + + if (STANDALONE_BUILD) + set(DCMTK_USE_EMBEDDED_DICTIONARIES 1) + set(DCMTK_DICTIONARIES + DICTIONARY_DICOM ${DCMTK_SOURCES_DIR}/dcmdata/data/dicom.dic + DICTIONARY_PRIVATE ${DCMTK_SOURCES_DIR}/dcmdata/data/private.dic + DICTIONARY_DICONDE ${DCMTK_SOURCES_DIR}/dcmdata/data/diconde.dic + ) + else() + set(DCMTK_USE_EMBEDDED_DICTIONARIES 0) + endif() + + +else() + if (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + + CHECK_INCLUDE_FILE_CXX(dcmtk/dcmdata/dcfilefo.h HAVE_DCMTK_H) + if (NOT HAVE_DCMTK_H) + message(FATAL_ERROR "Please install the libdcmtk-dev package") + endif() + + CHECK_LIBRARY_EXISTS(dcmdata "dcmDataDict" "" HAVE_DCMTK_LIB) + if (NOT HAVE_DCMTK_LIB) + message(FATAL_ERROR "Please install the libdcmtk package") + endif() + + find_path(DCMTK_INCLUDE_DIRS dcmtk/config/osconfig.h + /usr/include + ) + + link_libraries(dcmdata dcmnet dcmjpeg oflog ofstd) + + else() + # The following line allows one to manually add libraries at the + # command-line, which is necessary for Ubuntu/Debian packages + set(tmp "${DCMTK_LIBRARIES}") + include(FindDCMTK) + list(APPEND DCMTK_LIBRARIES "${tmp}") + + include_directories(${DCMTK_INCLUDE_DIRS}) + endif() + + add_definitions( + -DHAVE_CONFIG_H=1 + ) + + if (EXISTS "${DCMTK_config_INCLUDE_DIR}/cfunix.h") + set(DCMTK_CONFIGURATION_FILE "${DCMTK_config_INCLUDE_DIR}/cfunix.h") + elseif (EXISTS "${DCMTK_config_INCLUDE_DIR}/osconfig.h") # This is for Arch Linux + set(DCMTK_CONFIGURATION_FILE "${DCMTK_config_INCLUDE_DIR}/osconfig.h") + elseif (EXISTS "${DCMTK_INCLUDE_DIRS}/dcmtk/config/osconfig.h") # This is for Debian Buster + set(DCMTK_CONFIGURATION_FILE "${DCMTK_INCLUDE_DIRS}/dcmtk/config/osconfig.h") + else() + message(FATAL_ERROR "Please install libdcmtk*-dev") + endif() + + message("DCMTK configuration file: ${DCMTK_CONFIGURATION_FILE}") + + # Autodetection of the version of DCMTK + file(STRINGS + "${DCMTK_CONFIGURATION_FILE}" + DCMTK_VERSION_NUMBER1 REGEX + ".*PACKAGE_VERSION .*") + + string(REGEX REPLACE + ".*PACKAGE_VERSION.*\"([0-9]*)\\.([0-9]*)\\.([0-9]*)\"$" + "\\1\\2\\3" + DCMTK_VERSION_NUMBER + ${DCMTK_VERSION_NUMBER1}) + + set(DCMTK_USE_EMBEDDED_DICTIONARIES 0) +endif() + + +add_definitions(-DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER}) +message("DCMTK version: ${DCMTK_VERSION_NUMBER}") + + +add_definitions(-DDCMTK_USE_EMBEDDED_DICTIONARIES=${DCMTK_USE_EMBEDDED_DICTIONARIES}) +if (NOT DCMTK_USE_EMBEDDED_DICTIONARIES) + # Lookup for DICOM dictionaries, if none is specified by the user + if (DCMTK_DICTIONARY_DIR STREQUAL "") + find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic + /usr/share/dcmtk + /usr/share/dcmtk-3.6.8 + /usr/share/dcmtk-3.6.9 + /usr/share/libdcmtk1 + /usr/share/libdcmtk2 + /usr/share/libdcmtk3 + /usr/share/libdcmtk4 + /usr/share/libdcmtk5 + /usr/share/libdcmtk6 + /usr/share/libdcmtk7 + /usr/share/libdcmtk8 + /usr/share/libdcmtk9 + /usr/share/libdcmtk10 + /usr/share/libdcmtk11 + /usr/share/libdcmtk12 + /usr/share/libdcmtk13 + /usr/share/libdcmtk14 + /usr/share/libdcmtk15 + /usr/share/libdcmtk16 + /usr/share/libdcmtk17 + /usr/share/libdcmtk18 + /usr/share/libdcmtk19 + /usr/share/libdcmtk20 + /usr/local/share/dcmtk + /usr/local/share/dcmtk-3.6.8 + ) + + if (${DCMTK_DICTIONARY_DIR_AUTO} MATCHES "DCMTK_DICTIONARY_DIR_AUTO-NOTFOUND") + message(FATAL_ERROR "Cannot locate the DICOM dictionary on this system") + endif() + + if (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + # Remove the sysroot prefix + file(RELATIVE_PATH tmp ${CMAKE_FIND_ROOT_PATH} ${DCMTK_DICTIONARY_DIR_AUTO}) + set(DCMTK_DICTIONARY_DIR_AUTO /${tmp} CACHE INTERNAL "") + endif() + + message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}") + add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR_AUTO}") + else() + add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR}") + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake new file mode 100644 index 0000000..a521c3a --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake @@ -0,0 +1,197 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 360) +SET(DCMTK_PACKAGE_VERSION "3.6.0") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.0) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.0.zip") +SET(DCMTK_MD5 "219ad631b82031806147e4abbfba4fa4") + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # If using DCMTK 3.6.0, backport the "private.dic" file from DCMTK + # 3.6.2. This adds support for more private tags, and fixes some + # import problems with Philips MRI Achieva. + if (USE_DCMTK_362_PRIVATE_DIC) + message("Using the dictionary of private tags from DCMTK 3.6.2") + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.2-private.dic + ${DCMTK_SOURCES_DIR}/dcmdata/data/private.dic + COPYONLY) + else() + message("Using the dictionary of private tags from DCMTK 3.6.0") + endif() + + # Patches specific to DCMTK 3.6.0 + message("Applying patch to solve vulnerability in DCMTK 3.6.0") + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.0-dulparse-vulnerability.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + # This patch is not needed anymore thanks to the following commit + # (information sent by Jorg Riesmeier on Twitter on 2017-07-19): + # http://git.dcmtk.org/?p=dcmtk.git;a=commit;h=8df1f5e517b8629ae09088d0935c2a8dd333c76f + message("Applying patch for speed in DCMTK 3.6.0") + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.0-speed.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() +else() + message("The patches for DCMTK have already been applied") +endif() + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") + + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.2-linux-standard-base.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (FirstRun AND Failure) + message(FATAL_ERROR "Error while patching a file") + endif() +endif() + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) + + if (CMAKE_COMPILER_IS_GNUCXX) + # This is a patch for DCMTK 3.6.0 and MinGW64 + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.0-mingw64.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure AND FirstRun) + message(FATAL_ERROR "Error while patching a file") + endif() + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake new file mode 100644 index 0000000..964d102 --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake @@ -0,0 +1,216 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 362) +SET(DCMTK_PACKAGE_VERSION "3.6.2") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.2) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.2.tar.gz") +SET(DCMTK_MD5 "d219a4152772985191c9b89d75302d12") + +macro(DCMTK_UNSET) +endmacro() + +macro(DCMTK_UNSET_CACHE) +endmacro() + +set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/) +set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(DCMTK_WITH_THREADS OFF) # Disable thread support in wasm/asm.js +else() + set(DCMTK_WITH_THREADS ON) +endif() + +add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1) + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # "3.6.2 CXX11 fails on Linux; patch suggestions included" + # https://forum.dcmtk.org/viewtopic.php?f=3&t=4637 + message("Applying patch to detect mathematic primitives in DCMTK 3.6.2 with C++11") + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.2.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + COPYONLY) +else() + message("The patches for DCMTK have already been applied") +endif() + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") + + if (FirstRun) + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.2-linux-standard-base.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + endif() +endif() + + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(netapi32) # For NetWkstaUserGetInfo@12 + link_libraries(iphlpapi) # For GetAdaptersInfo@8 + + # Configure Wine if cross-compiling for Windows + if (CMAKE_COMPILER_IS_GNUCXX) + include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake) + FIND_PROGRAM(WINE_WINE_PROGRAM wine) + FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath) + list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static") + endif() +endif() + +# This step must be after the generation of "osconfig.h" +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES() +endif() + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) +endif() + + +#set_source_files_properties(${DCMTK_SOURCES} +# PROPERTIES COMPILE_DEFINITIONS +# "PACKAGE_VERSION=\"${DCMTK_PACKAGE_VERSION}\";PACKAGE_VERSION_NUMBER=\"${DCMTK_VERSION_NUMBER}\"") + + +# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by +# default since this does not seem to be appropriate (anymore) for +# most modern operating systems. In order to change this default, the +# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt +# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can +# be defined to change this setting at compilation time (see +# macros.txt for details). +# https://forum.dcmtk.org/viewtopic.php?t=4632 +add_definitions( + -DDISABLE_NAGLE_ALGORITHM=1 + ) diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake new file mode 100644 index 0000000..3cbaec3 --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake @@ -0,0 +1,201 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 364) +SET(DCMTK_PACKAGE_VERSION "3.6.4") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.4) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.4.tar.gz") +SET(DCMTK_MD5 "97597439a2ae7a39086066318db5f3bc") + +macro(DCMTK_UNSET) +endmacro() + +macro(DCMTK_UNSET_CACHE) +endmacro() + +set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/) +set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(DCMTK_WITH_THREADS OFF) # Disable thread support in wasm/asm.js +else() + set(DCMTK_WITH_THREADS ON) +endif() + +add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1) + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.4.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + COPYONLY) +else() + message("The patches for DCMTK have already been applied") +endif() + + +include_directories( + ${DCMTK_SOURCES_DIR}/dcmiod/include + ) + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") +endif() + + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(netapi32) # For NetWkstaUserGetInfo@12 + link_libraries(iphlpapi) # For GetAdaptersInfo@8 + + # Configure Wine if cross-compiling for Windows + if (CMAKE_COMPILER_IS_GNUCXX) + include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake) + FIND_PROGRAM(WINE_WINE_PROGRAM wine) + FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath) + list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static") + endif() +endif() + +# This step must be after the generation of "osconfig.h" +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES() +endif() + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) +endif() + + +# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by +# default since this does not seem to be appropriate (anymore) for +# most modern operating systems. In order to change this default, the +# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt +# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can +# be defined to change this setting at compilation time (see +# macros.txt for details). +# https://forum.dcmtk.org/viewtopic.php?t=4632 +add_definitions( + -DDISABLE_NAGLE_ALGORITHM=1 + ) diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake new file mode 100644 index 0000000..8a60ed8 --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake @@ -0,0 +1,226 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 365) +SET(DCMTK_PACKAGE_VERSION "3.6.5") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.5) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.5.tar.gz") +SET(DCMTK_MD5 "e19707f64ee5695c496b9c1e48e39d07") + +macro(DCMTK_UNSET) +endmacro() + +macro(DCMTK_UNSET_CACHE) +endmacro() + +set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/) +set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(DCMTK_WITH_THREADS OFF) # Disable thread support in wasm/asm.js +else() + set(DCMTK_WITH_THREADS ON) +endif() + +add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1) + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.5.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + COPYONLY) +else() + message("The patches for DCMTK have already been applied") +endif() + + +include_directories( + ${DCMTK_SOURCES_DIR}/dcmiod/include + ) + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") +endif() + + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(netapi32) # For NetWkstaUserGetInfo@12 + link_libraries(iphlpapi) # For GetAdaptersInfo@8 + + # Configure Wine if cross-compiling for Windows + if (CMAKE_COMPILER_IS_GNUCXX) + include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake) + FIND_PROGRAM(WINE_WINE_PROGRAM wine) + FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath) + list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static") + endif() +endif() + +# This step must be after the generation of "osconfig.h" +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES() +endif() + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) +endif() + + +# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by +# default since this does not seem to be appropriate (anymore) for +# most modern operating systems. In order to change this default, the +# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt +# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can +# be defined to change this setting at compilation time (see +# macros.txt for details). +# https://forum.dcmtk.org/viewtopic.php?t=4632 +add_definitions( + -DDISABLE_NAGLE_ALGORITHM=1 + ) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # For compatibility with Windows XP, avoid using fiber-local-storage + # in log4cplus, but use thread-local-storage instead. Otherwise, + # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll + add_definitions( + -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS + ) + + if (CMAKE_COMPILER_IS_GNUCXX OR # MinGW + "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # MSVC for 32bit (*) + + # (*) With multithreaded logging enabled, Visual Studio 2008 fails + # with error: ".\dcmtk-3.6.5\oflog\libsrc\globinit.cc(422) : error + # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot + # convert parameter 1 from 'void (__stdcall *)(void *)' to + # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'" + # None of the functions with this name in scope match the target type + + add_definitions( + -DDCMTK_LOG4CPLUS_SINGLE_THREADED + ) + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.6.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.6.cmake new file mode 100644 index 0000000..cbece6b --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.6.cmake @@ -0,0 +1,226 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 366) +SET(DCMTK_PACKAGE_VERSION "3.6.6") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.6) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.6.tar.gz") +SET(DCMTK_MD5 "f815879d315b916366a9da71339c7575") + +macro(DCMTK_UNSET) +endmacro() + +macro(DCMTK_UNSET_CACHE) +endmacro() + +set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/) +set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(DCMTK_WITH_THREADS OFF) # Disable thread support in wasm/asm.js +else() + set(DCMTK_WITH_THREADS ON) +endif() + +add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1) + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.6.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + COPYONLY) +else() + message("The patches for DCMTK have already been applied") +endif() + + +include_directories( + ${DCMTK_SOURCES_DIR}/dcmiod/include + ) + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") +endif() + + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(netapi32) # For NetWkstaUserGetInfo@12 + link_libraries(iphlpapi) # For GetAdaptersInfo@8 + + # Configure Wine if cross-compiling for Windows + if (CMAKE_COMPILER_IS_GNUCXX) + include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake) + FIND_PROGRAM(WINE_WINE_PROGRAM wine) + FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath) + list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static") + endif() +endif() + +# This step must be after the generation of "osconfig.h" +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES() +endif() + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) +endif() + + +# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by +# default since this does not seem to be appropriate (anymore) for +# most modern operating systems. In order to change this default, the +# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt +# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can +# be defined to change this setting at compilation time (see +# macros.txt for details). +# https://forum.dcmtk.org/viewtopic.php?t=4632 +add_definitions( + -DDISABLE_NAGLE_ALGORITHM=1 + ) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # For compatibility with Windows XP, avoid using fiber-local-storage + # in log4cplus, but use thread-local-storage instead. Otherwise, + # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll + add_definitions( + -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS + ) + + if (CMAKE_COMPILER_IS_GNUCXX OR # MinGW + "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # MSVC for 32bit (*) + + # (*) With multithreaded logging enabled, Visual Studio 2008 fails + # with error: ".\dcmtk-3.6.6\oflog\libsrc\globinit.cc(422) : error + # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot + # convert parameter 1 from 'void (__stdcall *)(void *)' to + # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'" + # None of the functions with this name in scope match the target type + + add_definitions( + -DDCMTK_LOG4CPLUS_SINGLE_THREADED + ) + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.7.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.7.cmake new file mode 100644 index 0000000..92b71d5 --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.7.cmake @@ -0,0 +1,272 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 367) +SET(DCMTK_PACKAGE_VERSION "3.6.7") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.7) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.7.tar.gz") +SET(DCMTK_MD5 "e4d519bb315ec3944f3f1d61df465cbd") + +macro(DCMTK_UNSET) +endmacro() + +macro(DCMTK_UNSET_CACHE) +endmacro() + +set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/) +set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(DCMTK_WITH_THREADS OFF) # Disable thread support in wasm/asm.js +else() + set(DCMTK_WITH_THREADS ON) +endif() + +add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1) + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.7.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + if (MSVC) + # Older versions of Microsoft Visual Studio (notably MSVC2008) + # don't like void usage of function arguments in C source files, + # in order to avoid a warning about unused arguments. This patch + # removes such usages that were not present in DCMTK <= 3.6.6. + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.7-visual-studio.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + COPYONLY) +else() + message("The patches for DCMTK have already been applied") +endif() + + +include_directories( + ${DCMTK_SOURCES_DIR}/dcmiod/include + ) + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") +endif() + + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + + +# For the dcmtls module, necessary since DCMTK 3.6.7 (cf. file +# "dcmtls/libsrc/tlslayer.cc"). This must be done before the +# invokation of "configure_file()"! +if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL) + # The "CHECK_FUNCTIONWITHHEADER_EXISTS()" provided by DCMTK only + # works with the system-wide version of OpenSSL. If statically + # linking against OpenSSL, we manually provide information about + # OpenSSL 3.0.x + set(HAVE_OPENSSL_PROTOTYPE_DH_BITS 1) + set(HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS 1) + set(HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID 1) + set(HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM 1) +else() + CHECK_FUNCTIONWITHHEADER_EXISTS("DH_bits" "openssl/dh.h" HAVE_OPENSSL_PROTOTYPE_DH_BITS) + CHECK_FUNCTIONWITHHEADER_EXISTS("EVP_PKEY_base_id" "openssl/evp.h" HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get0_param" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_cert_store" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_ciphers" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS) + CHECK_FUNCTIONWITHHEADER_EXISTS("X509_STORE_get0_param" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM) + CHECK_FUNCTIONWITHHEADER_EXISTS("X509_get_signature_nid" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID) +endif() + + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(netapi32) # For NetWkstaUserGetInfo@12 + link_libraries(iphlpapi) # For GetAdaptersInfo@8 + + # Configure Wine if cross-compiling for Windows + if (CMAKE_COMPILER_IS_GNUCXX) + include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake) + FIND_PROGRAM(WINE_WINE_PROGRAM wine) + FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath) + list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static") + endif() +endif() + +# This step must be after the generation of "osconfig.h" +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES() +endif() + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) +endif() + + +# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by +# default since this does not seem to be appropriate (anymore) for +# most modern operating systems. In order to change this default, the +# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt +# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can +# be defined to change this setting at compilation time (see +# macros.txt for details). +# https://forum.dcmtk.org/viewtopic.php?t=4632 +add_definitions( + -DDISABLE_NAGLE_ALGORITHM=1 + ) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # For compatibility with Windows XP, avoid using fiber-local-storage + # in log4cplus, but use thread-local-storage instead. Otherwise, + # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll + add_definitions( + -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS + ) + + if (CMAKE_COMPILER_IS_GNUCXX OR # MinGW + "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # MSVC for 32bit (*) + + # (*) With multithreaded logging enabled, Visual Studio 2008 fails + # with error: ".\dcmtk-3.6.7\oflog\libsrc\globinit.cc(422) : error + # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot + # convert parameter 1 from 'void (__stdcall *)(void *)' to + # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'" + # None of the functions with this name in scope match the target type + + add_definitions( + -DDCMTK_LOG4CPLUS_SINGLE_THREADED + ) + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.8.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.8.cmake new file mode 100644 index 0000000..e67644a --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.8.cmake @@ -0,0 +1,285 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 368) +SET(DCMTK_PACKAGE_VERSION "3.6.8") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-DCMTK-3.6.8) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.8.tar.gz") +SET(DCMTK_MD5 "ce3e878c05165f1a3322c29e67f2426f") + +macro(DCMTK_UNSET) +endmacro() + +macro(DCMTK_UNSET_CACHE) +endmacro() + +set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/) +set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(DCMTK_WITH_THREADS OFF) # Disable thread support in wasm/asm.js +else() + set(DCMTK_WITH_THREADS ON) +endif() + +add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1) + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.8.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + if (MSVC) + # Older versions of Microsoft Visual Studio (notably MSVC2008) + # don't like void usage of function arguments in C source files, + # in order to avoid a warning about unused arguments. This patch + # removes such usages that were not present in DCMTK <= 3.6.6. + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.8-visual-studio.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + COPYONLY) +else() + message("The patches for DCMTK have already been applied") +endif() + + +include_directories( + ${DCMTK_SOURCES_DIR}/dcmiod/include + ${DCMTK_SOURCES_DIR}/oficonv/include + ) + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") +endif() + + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + + +# For the dcmtls module, necessary since DCMTK 3.6.7 (cf. file +# "dcmtls/libsrc/tlslayer.cc"). This must be done before the +# invokation of "configure_file()"! +if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL) + # The "CHECK_FUNCTIONWITHHEADER_EXISTS()" provided by DCMTK only + # works with the system-wide version of OpenSSL. If statically + # linking against OpenSSL, we manually provide information about + # OpenSSL 3.0.x + set(HAVE_OPENSSL_PROTOTYPE_DH_BITS 1) + set(HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS 1) + set(HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID 1) + set(HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM 1) +else() + CHECK_FUNCTIONWITHHEADER_EXISTS("DH_bits" "openssl/dh.h" HAVE_OPENSSL_PROTOTYPE_DH_BITS) + CHECK_FUNCTIONWITHHEADER_EXISTS("EVP_PKEY_base_id" "openssl/evp.h" HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get0_param" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_cert_store" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_ciphers" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS) + CHECK_FUNCTIONWITHHEADER_EXISTS("X509_STORE_get0_param" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM) + CHECK_FUNCTIONWITHHEADER_EXISTS("X509_get_signature_nid" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID) +endif() + + +# "DCMTK_ENABLE_CHARSET_CONVERSION" is defined by "osconfig.h.in" +if (NOT DEFINED BOOST_LOCALE_BACKEND OR # This is the case if locale support is disabled (e.g. in Stone) + BOOST_LOCALE_BACKEND STREQUAL "gcc") + set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_STDLIBC_ICONV" CACHE STRING "" FORCE) +elseif (BOOST_LOCALE_BACKEND STREQUAL "libiconv") + set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_ICONV" CACHE STRING "" FORCE) +elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_ICU" CACHE STRING "" FORCE) +else() + message(FATAL_ERROR "Invalid value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}") +endif() + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(netapi32) # For NetWkstaUserGetInfo@12 + link_libraries(iphlpapi) # For GetAdaptersInfo@8 + + # Configure Wine if cross-compiling for Windows + if (CMAKE_COMPILER_IS_GNUCXX) + include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake) + FIND_PROGRAM(WINE_WINE_PROGRAM wine) + FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath) + list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static") + endif() +endif() + +# This step must be after the generation of "osconfig.h" +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES() +endif() + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) +endif() + + +# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by +# default since this does not seem to be appropriate (anymore) for +# most modern operating systems. In order to change this default, the +# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt +# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can +# be defined to change this setting at compilation time (see +# macros.txt for details). +# https://forum.dcmtk.org/viewtopic.php?t=4632 +add_definitions( + -DDISABLE_NAGLE_ALGORITHM=1 + ) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # For compatibility with Windows XP, avoid using fiber-local-storage + # in log4cplus, but use thread-local-storage instead. Otherwise, + # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll + add_definitions( + -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS + ) + + if (CMAKE_COMPILER_IS_GNUCXX OR # MinGW + "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # MSVC for 32bit (*) + + # (*) With multithreaded logging enabled, Visual Studio 2008 fails + # with error: ".\dcmtk-3.6.7\oflog\libsrc\globinit.cc(422) : error + # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot + # convert parameter 1 from 'void (__stdcall *)(void *)' to + # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'" + # None of the functions with this name in scope match the target type + + add_definitions( + -DDCMTK_LOG4CPLUS_SINGLE_THREADED + ) + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.9.cmake b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.9.cmake new file mode 100644 index 0000000..d07fb79 --- /dev/null +++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.9.cmake @@ -0,0 +1,309 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(DCMTK_VERSION_NUMBER 369) +SET(DCMTK_PACKAGE_VERSION "3.6.9") +SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.9) +SET(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.9.tar.gz") +SET(DCMTK_MD5 "cb30587f8da760c832a4f19d159acda5") + +macro(DCMTK_UNSET) +endmacro() + +macro(DCMTK_UNSET_CACHE) +endmacro() + +set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/) +set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(DCMTK_WITH_THREADS OFF) # Disable thread support in wasm/asm.js +else() + set(DCMTK_WITH_THREADS ON) +endif() + +add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1) + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + + +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.9.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + if (MSVC) + # Older versions of Microsoft Visual Studio (notably MSVC2008) + # don't like void usage of function arguments in C source files, + # in order to avoid a warning about unused arguments. This patch + # removes such usages that were not present in DCMTK <= 3.6.6. + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-3.6.9-visual-studio.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/dcmtk-dcdict_orthanc.cc + ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc + COPYONLY) +else() + message("The patches for DCMTK have already been applied") +endif() + + +include_directories( + ${DCMTK_SOURCES_DIR}/dcmiod/include + ${DCMTK_SOURCES_DIR}/oficonv/include + ) + + +# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake" +IF (CMAKE_CROSSCOMPILING) + if (CMAKE_COMPILER_IS_GNUCXX AND + CMAKE_SYSTEM_NAME STREQUAL "Windows") # MinGW + SET(C_CHAR_UNSIGNED 1 CACHE INTERNAL "Whether char is unsigned.") + + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js + + # Check out "../WebAssembly/ArithmeticTests/" to regenerate the + # "arith.h" file + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/WebAssembly/arith.h + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/arith.h + COPYONLY) + + UNSET(C_CHAR_UNSIGNED CACHE) + SET(C_CHAR_UNSIGNED 0 CACHE INTERNAL "") + + else() + message(FATAL_ERROR "Support your platform here") + endif() +ENDIF() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(DCMTK_ENABLE_CHARSET_CONVERSION "iconv" CACHE STRING "") + SET(HAVE_SYS_GETTID 0 CACHE INTERNAL "") +endif() + + +SET(DCMTK_SOURCE_DIR ${DCMTK_SOURCES_DIR}) +include(GNUInstallDirs) # Needed since DCMTK 3.6.9 +include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) +include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) + + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or + # asm.js The macros below are not properly discovered by DCMTK + # when using WebAssembly. Check out "../WebAssembly/arith.h" for + # how we produced these values. This step MUST be after + # "GenerateDCMTKConfigure" and before the generation of + # "osconfig.h". + UNSET(SIZEOF_VOID_P CACHE) + UNSET(SIZEOF_CHAR CACHE) + UNSET(SIZEOF_DOUBLE CACHE) + UNSET(SIZEOF_FLOAT CACHE) + UNSET(SIZEOF_INT CACHE) + UNSET(SIZEOF_LONG CACHE) + UNSET(SIZEOF_SHORT CACHE) + UNSET(SIZEOF_VOID_P CACHE) + + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") + SET(SIZEOF_CHAR 1 CACHE INTERNAL "") + SET(SIZEOF_DOUBLE 8 CACHE INTERNAL "") + SET(SIZEOF_FLOAT 4 CACHE INTERNAL "") + SET(SIZEOF_INT 4 CACHE INTERNAL "") + SET(SIZEOF_LONG 4 CACHE INTERNAL "") + SET(SIZEOF_SHORT 2 CACHE INTERNAL "") + SET(SIZEOF_VOID_P 4 CACHE INTERNAL "") +endif() + + +set(DCMTK_PACKAGE_VERSION_SUFFIX "") +set(DCMTK_PACKAGE_VERSION_NUMBER ${DCMTK_VERSION_NUMBER}) + + +# For the dcmtls module, necessary since DCMTK 3.6.7 (cf. file +# "dcmtls/libsrc/tlslayer.cc"). This must be done before the +# invokation of "configure_file()"! +if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL) + # The "CHECK_FUNCTIONWITHHEADER_EXISTS()" provided by DCMTK only + # works with the system-wide version of OpenSSL. If statically + # linking against OpenSSL, we manually provide information about + # OpenSSL 3.0.x + set(HAVE_OPENSSL_PROTOTYPE_DH_BITS 1) + set(HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE 1) + set(HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS 1) + set(HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID 1) + set(HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM 1) +else() + CHECK_FUNCTIONWITHHEADER_EXISTS("DH_bits" "openssl/dh.h" HAVE_OPENSSL_PROTOTYPE_DH_BITS) + CHECK_FUNCTIONWITHHEADER_EXISTS("EVP_PKEY_base_id" "openssl/evp.h" HAVE_OPENSSL_PROTOTYPE_EVP_PKEY_BASE_ID) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get0_param" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET0_PARAM) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_cert_store" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CERT_STORE) + CHECK_FUNCTIONWITHHEADER_EXISTS("SSL_CTX_get_ciphers" "openssl/ssl.h" HAVE_OPENSSL_PROTOTYPE_SSL_CTX_GET_CIPHERS) + CHECK_FUNCTIONWITHHEADER_EXISTS("X509_STORE_get0_param" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_STORE_GET0_PARAM) + CHECK_FUNCTIONWITHHEADER_EXISTS("X509_get_signature_nid" "openssl/x509.h" HAVE_OPENSSL_PROTOTYPE_X509_GET_SIGNATURE_NID) +endif() + + +# "DCMTK_ENABLE_CHARSET_CONVERSION" is defined by "osconfig.h.in" +if (NOT DEFINED DCMTK_LOCALE_BACKEND OR # This is the case if locale support is disabled (e.g. in Stone) + DCMTK_LOCALE_BACKEND STREQUAL "gcc") + set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_STDLIBC_ICONV" CACHE STRING "" FORCE) +elseif (DCMTK_LOCALE_BACKEND STREQUAL "libiconv") + set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_ICONV" CACHE STRING "" FORCE) +elseif (DCMTK_LOCALE_BACKEND STREQUAL "icu") + message(FATAL_ERROR "Support for ICU has been removed since DCMTK 3.6.9") +elseif (DCMTK_LOCALE_BACKEND STREQUAL "oficonv") + set(DCMTK_ENABLE_CHARSET_CONVERSION "DCMTK_CHARSET_CONVERSION_OFICONV" CACHE STRING "" FORCE) +else() + message(FATAL_ERROR "Invalid value for DCMTK_LOCALE_BACKEND: ${DCMTK_LOCALE_BACKEND}") +endif() + + +# Enable support of the 1.2.840.10008.1.2.1.99 transfer syntax in +# static builds of Orthanc (Deflated Explicit VR Little +# Endian). Defining "WITH_ZLIB" is always OK, as zlib is part of the +# core dependencies of the Orthanc framework. +# https://discourse.orthanc-server.org/t/transcoding-to-deflated-transfer-syntax-fails/ +set(WITH_ZLIB ON) + +CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) + + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(netapi32) # For NetWkstaUserGetInfo@12 + link_libraries(iphlpapi) # For GetAdaptersInfo@8 + + # Configure Wine if cross-compiling for Windows + if (CMAKE_COMPILER_IS_GNUCXX) + include(${DCMTK_SOURCES_DIR}/CMake/dcmtkUseWine.cmake) + FIND_PROGRAM(WINE_WINE_PROGRAM wine) + FIND_PROGRAM(WINE_WINEPATH_PROGRAM winepath) + list(APPEND DCMTK_TRY_COMPILE_REQUIRED_CMAKE_FLAGS "-DCMAKE_EXE_LINKER_FLAGS=-static") + endif() +endif() + +# This step must be after the generation of "osconfig.h" => Removed since DCMTK 3.6.9 +#if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") +# INSPECT_FUNDAMENTAL_ARITHMETIC_TYPES() +#endif() + + +# Source for the logging facility of DCMTK +AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc + ) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc + ${DCMTK_SOURCES_DIR}/oflog/libsrc/clfsap.cc + ) +endif() + + +# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by +# default since this does not seem to be appropriate (anymore) for +# most modern operating systems. In order to change this default, the +# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt +# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can +# be defined to change this setting at compilation time (see +# macros.txt for details). +# https://forum.dcmtk.org/viewtopic.php?t=4632 +add_definitions( + -DDISABLE_NAGLE_ALGORITHM=1 + ) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # For compatibility with Windows XP, avoid using fiber-local-storage + # in log4cplus, but use thread-local-storage instead. Otherwise, + # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll + add_definitions( + -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS + ) + + if (CMAKE_COMPILER_IS_GNUCXX OR # MinGW + "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # MSVC for 32bit (*) + + # (*) With multithreaded logging enabled, Visual Studio 2008 fails + # with error: ".\dcmtk-3.6.7\oflog\libsrc\globinit.cc(422) : error + # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot + # convert parameter 1 from 'void (__stdcall *)(void *)' to + # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'" + # None of the functions with this name in scope match the target type + + add_definitions( + -DDCMTK_LOG4CPLUS_SINGLE_THREADED + ) + endif() + + if (CMAKE_COMPILER_IS_GNUCXX) # MinGW + # Necessary since DCMTK 3.6.9 + add_definitions( + -DENABLE_OLD_OFSTD_FTOA_IMPLEMENTATION + -DENABLE_OLD_OFSTD_ATOF_IMPLEMENTATION + ) + endif() +endif() + + +if (DCMTK_LOCALE_BACKEND STREQUAL "oficonv") + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oficonv/libsrc DCMTK_SOURCES) +endif() diff --git a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake new file mode 100644 index 0000000..756f589 --- /dev/null +++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake @@ -0,0 +1,592 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + + +## +## Check whether the parent script sets the mandatory variables +## + +if (NOT DEFINED ORTHANC_FRAMEWORK_SOURCE OR + (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "web" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "path")) + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_SOURCE must be set to \"system\", \"hg\", \"web\", \"archive\" or \"path\"") +endif() + + +## +## Detection of the requested version +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if (NOT DEFINED ORTHANC_FRAMEWORK_VERSION) + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_VERSION must be set") + endif() + + if (DEFINED ORTHANC_FRAMEWORK_MAJOR OR + DEFINED ORTHANC_FRAMEWORK_MINOR OR + DEFINED ORTHANC_FRAMEWORK_REVISION OR + DEFINED ORTHANC_FRAMEWORK_MD5) + message(FATAL_ERROR "Some internal variable has been set") + endif() + + set(ORTHANC_FRAMEWORK_MD5 "") + + if (NOT DEFINED ORTHANC_FRAMEWORK_BRANCH) + if (ORTHANC_FRAMEWORK_VERSION STREQUAL "mainline") + set(ORTHANC_FRAMEWORK_BRANCH "default") + set(ORTHANC_FRAMEWORK_MAJOR 999) + set(ORTHANC_FRAMEWORK_MINOR 999) + set(ORTHANC_FRAMEWORK_REVISION 999) + + else() + set(ORTHANC_FRAMEWORK_BRANCH "Orthanc-${ORTHANC_FRAMEWORK_VERSION}") + + set(RE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$") + string(REGEX REPLACE ${RE} "\\1" ORTHANC_FRAMEWORK_MAJOR ${ORTHANC_FRAMEWORK_VERSION}) + string(REGEX REPLACE ${RE} "\\2" ORTHANC_FRAMEWORK_MINOR ${ORTHANC_FRAMEWORK_VERSION}) + string(REGEX REPLACE ${RE} "\\3" ORTHANC_FRAMEWORK_REVISION ${ORTHANC_FRAMEWORK_VERSION}) + + if (NOT ORTHANC_FRAMEWORK_MAJOR MATCHES "^[0-9]+$" OR + NOT ORTHANC_FRAMEWORK_MINOR MATCHES "^[0-9]+$" OR + NOT ORTHANC_FRAMEWORK_REVISION MATCHES "^[0-9]+$") + message("Bad version of the Orthanc framework, assuming a pre-release: ${ORTHANC_FRAMEWORK_VERSION}") + set(ORTHANC_FRAMEWORK_MAJOR 999) + set(ORTHANC_FRAMEWORK_MINOR 999) + set(ORTHANC_FRAMEWORK_REVISION 999) + endif() + + if (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.1") + set(ORTHANC_FRAMEWORK_MD5 "dac95bd6cf86fb19deaf4e612961f378") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.2") + set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0") + set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1") + set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2") + set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.0") + set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1") + set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2") + set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3") + set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.4") + set(ORTHANC_FRAMEWORK_MD5 "404baef5d4c43e7c5d9410edda8ef5a5") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.5") + set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6") + set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7") + set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8") + set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0") + set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1") + set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.0") + set(ORTHANC_FRAMEWORK_MD5 "ce5f689e852b01d3672bd3d2f952a5ef") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.1") + set(ORTHANC_FRAMEWORK_MD5 "3c171217f930abe80246997bdbcaf7cc") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.2") + set(ORTHANC_FRAMEWORK_MD5 "328f94dcbd78c169655a13f7ad58a2c2") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.3") + set(ORTHANC_FRAMEWORK_MD5 "3f1ba9502ec7c5449971d3b56087bcde") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.4") + set(ORTHANC_FRAMEWORK_MD5 "19fcb7c21876af86546baa048a22c6c0") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.0") + set(ORTHANC_FRAMEWORK_MD5 "f8ec7554ef5d23ea4ce474b1e8214de9") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.1") + set(ORTHANC_FRAMEWORK_MD5 "db094f96399cbe8b9bbdbce34884c220") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.2") + set(ORTHANC_FRAMEWORK_MD5 "8bfa10e66c9931e74111be0bfb1f4548") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.0") + set(ORTHANC_FRAMEWORK_MD5 "cea0b02ce184671eaf1bd668beefbf28") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.1") + set(ORTHANC_FRAMEWORK_MD5 "08eebc66ef93c3b40115c38501db5fbd") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.2") + set(ORTHANC_FRAMEWORK_MD5 "3ea66c09f64aca990016683b6375734e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.3") + set(ORTHANC_FRAMEWORK_MD5 "9b86e6f00e03278293cd15643cc0233f") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.4") + set(ORTHANC_FRAMEWORK_MD5 "6d5ca4a73ac7d42445041ca79de1624d") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.5") + set(ORTHANC_FRAMEWORK_MD5 "10fc64de1254a095e5d3ed3931f0cfbb") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.6") + set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7") + set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.0") + set(ORTHANC_FRAMEWORK_MD5 "8610c82d9153f22e929f2110f8f60279") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.1") + set(ORTHANC_FRAMEWORK_MD5 "caf667fc5ea452b3d0c2f70bfd02599c") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.0") + set(ORTHANC_FRAMEWORK_MD5 "962c4a4a706a2ef28b390d8515dd7091") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.1") + set(ORTHANC_FRAMEWORK_MD5 "a39661c406adf22cf574fde290cf4bbf") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.2") + set(ORTHANC_FRAMEWORK_MD5 "ede3de356493a8868545f8cb4b8bc8b5") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.3") + set(ORTHANC_FRAMEWORK_MD5 "e48fc0cb09c4856803791a1be28c07dc") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.0") + set(ORTHANC_FRAMEWORK_MD5 "d32a0cde03b6eb603d8dd2b33d38bf1b") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.1") + set(ORTHANC_FRAMEWORK_MD5 "8a435140efc8ff4a01d8242f092f21de") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.2") + set(ORTHANC_FRAMEWORK_MD5 "d2476b9e796e339ac320b5333489bdb3") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.3") + set(ORTHANC_FRAMEWORK_MD5 "975f5bf2142c22cb1777b4f6a0a614c5") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.4") + set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.5") + set(ORTHANC_FRAMEWORK_MD5 "5bb69f092981fdcfc11dec0a0f9a7db3") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.6") + set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7") + set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a") + + # Below this point are development snapshots that were used to + # release some plugin, before an official release of the Orthanc + # framework was available. Here is the command to be used to + # generate a proper archive: + # + # $ hg archive /tmp/Orthanc-`hg id -i | sed 's/\+//'`.tar.gz + # + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "ae0e3fd609df") + # DICOMweb 1.1 (framework pre-1.6.0) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "7e09e9b530a2f527854f0b782d7e0645") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "82652c5fc04f") + # Stone Web viewer 1.0 (framework pre-1.8.1) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "d77331d68917e66a3f4f9b807bbdab7f") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "4a3ba4bf4ba7") + # PostgreSQL 3.3 (framework pre-1.8.2) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "2d82bddf06f9cfe82095495cb3b8abde") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "23ad1b9c7800") + # For "Toolbox::ReadJson()" and "Toolbox::Write{...}Json()" (pre-1.9.0) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "9af92080e57c60dd288eba46ce606c00") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "b2e08d83e21d") + # WSI 1.1 (framework pre-1.10.0), to remove "-std=c++11" + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "2eaa073cbb4b44ffba199ad93393b2b1") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "daf4807631c5") + # DICOMweb 1.15 (framework pre-1.12.2) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "ebe8bdf388319f1c9536b2b680451848") + endif() + endif() + endif() + +elseif (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path") + message("Using the Orthanc framework from a path of the filesystem. Assuming mainline version.") + set(ORTHANC_FRAMEWORK_MAJOR 999) + set(ORTHANC_FRAMEWORK_MINOR 999) + set(ORTHANC_FRAMEWORK_REVISION 999) +endif() + + + +## +## Detection of the third-party software +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg") + find_program(ORTHANC_FRAMEWORK_HG hg) + + if (${ORTHANC_FRAMEWORK_HG} MATCHES "ORTHANC_FRAMEWORK_HG-NOTFOUND") + message(FATAL_ERROR "Please install Mercurial") + endif() +endif() + + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + find_program(ORTHANC_FRAMEWORK_7ZIP 7z + PATHS + "$ENV{ProgramFiles}/7-Zip" + "$ENV{ProgramW6432}/7-Zip" + ) + + if (${ORTHANC_FRAMEWORK_7ZIP} MATCHES "ORTHANC_FRAMEWORK_7ZIP-NOTFOUND") + message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)") + endif() + + else() + find_program(ORTHANC_FRAMEWORK_TAR tar) + if (${ORTHANC_FRAMEWORK_TAR} MATCHES "ORTHANC_FRAMEWORK_TAR-NOTFOUND") + message(FATAL_ERROR "Please install the 'tar' package") + endif() + endif() +endif() + + + +## +## Case of the Orthanc framework specified as a path on the filesystem +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path") + if (NOT DEFINED ORTHANC_FRAMEWORK_ROOT OR + ORTHANC_FRAMEWORK_ROOT STREQUAL "") + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ROOT must provide the path to the sources of Orthanc") + endif() + + if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}) + message(FATAL_ERROR "Non-existing directory: ${ORTHANC_FRAMEWORK_ROOT}") + endif() +endif() + + + +## +## Case of the Orthanc framework cloned using Mercurial +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg") + if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS) + message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON") + endif() + + set(ORTHANC_ROOT ${CMAKE_BINARY_DIR}/orthanc) + + if (EXISTS ${ORTHANC_ROOT}) + message("Updating the Orthanc source repository using Mercurial") + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_HG} pull + WORKING_DIRECTORY ${ORTHANC_ROOT} + RESULT_VARIABLE Failure + ) + else() + message("Forking the Orthanc source repository using Mercurial") + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://orthanc.uclouvain.be/hg/orthanc/" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + endif() + + if (Failure OR NOT EXISTS ${ORTHANC_ROOT}) + message(FATAL_ERROR "Cannot fork the Orthanc repository") + endif() + + message("Setting branch of the Orthanc repository to: ${ORTHANC_FRAMEWORK_BRANCH}") + + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_HG} update -c ${ORTHANC_FRAMEWORK_BRANCH} + WORKING_DIRECTORY ${ORTHANC_ROOT} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while running Mercurial") + endif() +endif() + + + +## +## Case of the Orthanc framework provided as a source archive on the +## filesystem +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive") + if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR + ORTHANC_FRAMEWORK_ARCHIVE STREQUAL "") + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ARCHIVE must provide the path to the sources of Orthanc") + endif() +endif() + + + +## +## Case of the Orthanc framework downloaded from the Web +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if (DEFINED ORTHANC_FRAMEWORK_URL) + string(REGEX REPLACE "^.*/" "" ORTHANC_FRAMEMORK_FILENAME "${ORTHANC_FRAMEWORK_URL}") + else() + # Default case: Download from the official Web site + set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz) + if (ORTHANC_FRAMEWORK_PRE_RELEASE) + set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}") + else() + set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/sources/orthanc/${ORTHANC_FRAMEMORK_FILENAME}") + endif() + endif() + + set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}") + + if (NOT EXISTS "${ORTHANC_FRAMEWORK_ARCHIVE}") + if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS) + message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON") + endif() + + message("Downloading: ${ORTHANC_FRAMEWORK_URL}") + + file(DOWNLOAD + "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}" + SHOW_PROGRESS EXPECTED_MD5 "${ORTHANC_FRAMEWORK_MD5}" + TIMEOUT 60 + INACTIVITY_TIMEOUT 60 + ) + else() + message("Using local copy of: ${ORTHANC_FRAMEWORK_URL}") + endif() +endif() + + + + +## +## Uncompressing the Orthanc framework, if it was retrieved from a +## source archive on the filesystem, or from the official Web site +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + + if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR + NOT DEFINED ORTHANC_FRAMEWORK_VERSION OR + NOT DEFINED ORTHANC_FRAMEWORK_MD5) + message(FATAL_ERROR "Internal error") + endif() + + if (ORTHANC_FRAMEWORK_MD5 STREQUAL "") + message(FATAL_ERROR "Unknown release of Orthanc: ${ORTHANC_FRAMEWORK_VERSION}") + endif() + + file(MD5 ${ORTHANC_FRAMEWORK_ARCHIVE} ActualMD5) + + if (NOT "${ActualMD5}" STREQUAL "${ORTHANC_FRAMEWORK_MD5}") + message(FATAL_ERROR "The MD5 hash of the Orthanc archive is invalid: ${ORTHANC_FRAMEWORK_ARCHIVE}") + endif() + + set(ORTHANC_ROOT "${CMAKE_BINARY_DIR}/Orthanc-${ORTHANC_FRAMEWORK_VERSION}") + + if (NOT IS_DIRECTORY "${ORTHANC_ROOT}") + if (NOT ORTHANC_FRAMEWORK_ARCHIVE MATCHES ".tar.gz$") + message(FATAL_ERROR "Archive should have the \".tar.gz\" extension: ${ORTHANC_FRAMEWORK_ARCHIVE}") + endif() + + message("Uncompressing: ${ORTHANC_FRAMEWORK_ARCHIVE}") + + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + # How to silently extract files using 7-zip + # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly + + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_7ZIP} e -y ${ORTHANC_FRAMEWORK_ARCHIVE} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + get_filename_component(TMP_FILENAME "${ORTHANC_FRAMEWORK_ARCHIVE}" NAME) + string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}") + + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_7ZIP} x -y ${TMP_FILENAME2} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + else() + execute_process( + COMMAND sh -c "${ORTHANC_FRAMEWORK_TAR} xfz ${ORTHANC_FRAMEWORK_ARCHIVE}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + endif() + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if (NOT IS_DIRECTORY "${ORTHANC_ROOT}") + message(FATAL_ERROR "The Orthanc framework was not uncompressed at the proper location. Check the CMake instructions.") + endif() + endif() +endif() + + + +## +## Determine the path to the sources of the Orthanc framework +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if (NOT DEFINED ORTHANC_ROOT OR + NOT DEFINED ORTHANC_FRAMEWORK_MAJOR OR + NOT DEFINED ORTHANC_FRAMEWORK_MINOR OR + NOT DEFINED ORTHANC_FRAMEWORK_REVISION) + message(FATAL_ERROR "Internal error in the DownloadOrthancFramework.cmake file") + endif() + + unset(ORTHANC_FRAMEWORK_ROOT CACHE) + + if ("${ORTHANC_FRAMEWORK_MAJOR}.${ORTHANC_FRAMEWORK_MINOR}.${ORTHANC_FRAMEWORK_REVISION}" VERSION_LESS "1.7.2") + set(ORTHANC_FRAMEWORK_ROOT "${ORTHANC_ROOT}/Core" CACHE + STRING "Path to the Orthanc framework source directory") + set(ENABLE_PLUGINS_VERSION_SCRIPT OFF) + else() + set(ORTHANC_FRAMEWORK_ROOT "${ORTHANC_ROOT}/OrthancFramework/Sources" CACHE + STRING "Path to the Orthanc framework source directory") + endif() + + unset(ORTHANC_ROOT) +endif() + +if (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system") + if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/OrthancException.h OR + NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake) + message(FATAL_ERROR "Directory not containing the source code of the Orthanc framework: ${ORTHANC_FRAMEWORK_ROOT}") + endif() +endif() + + + +## +## Case of the Orthanc framework installed as a shared library in a +## GNU/Linux distribution (typically Debian). New in Orthanc 1.7.2. +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system") + set(ORTHANC_FRAMEWORK_LIBDIR "" CACHE PATH "") + set(ORTHANC_FRAMEWORK_USE_SHARED ON CACHE BOOL "Whether to use the shared library or the static library") + set(ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES "" CACHE STRING "Additional libraries to link against, separated by whitespaces, typically needed if using the static library (a common minimal value is \"boost_filesystem boost_iostreams boost_locale boost_regex boost_thread jsoncpp pugixml uuid\")") + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND + CMAKE_COMPILER_IS_GNUCXX) # MinGW + set(DYNAMIC_MINGW_STDLIB ON) # Disable static linking against libc (to throw exceptions) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libstdc++") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++") + endif() + + include(CheckIncludeFile) + include(CheckIncludeFileCXX) + + if(CMAKE_VERSION VERSION_GREATER "3.11") + find_package(Python REQUIRED COMPONENTS Interpreter) + set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) + else() + include(FindPythonInterp) + find_package(PythonInterp REQUIRED) + endif() + + include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake) + include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake) + include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake) + set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py) + + if (ORTHANC_FRAMEWORK_USE_SHARED) + list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) + list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix) + else() + list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) + list(GET CMAKE_FIND_LIBRARY_SUFFIXES 1 Suffix) + endif() + + # The "OrthancFramework" library must be the first one to be included + if ("${ORTHANC_FRAMEWORK_LIBDIR}" STREQUAL "") + set(ORTHANC_FRAMEWORK_LIBRARIES ${Prefix}OrthancFramework${Suffix}) + else () + set(ORTHANC_FRAMEWORK_LIBRARIES ${ORTHANC_FRAMEWORK_LIBDIR}/${Prefix}OrthancFramework${Suffix}) + endif() + + if (NOT ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES STREQUAL "") + # https://stackoverflow.com/a/5272993/881731 + string(REPLACE " " ";" tmp ${ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES}) + list(APPEND ORTHANC_FRAMEWORK_LIBRARIES ${tmp}) + endif() + + # Look for the version of the mandatory dependency JsonCpp (cf. JsonCppConfiguration.cmake) + if (CMAKE_CROSSCOMPILING) + set(JSONCPP_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT}/..) + else() + find_path(JSONCPP_INCLUDE_DIR json/reader.h + ${ORTHANC_FRAMEWORK_ROOT}/.. + /usr/include/jsoncpp + /usr/local/include/jsoncpp + ) + endif() + + message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}") + include_directories(${JSONCPP_INCLUDE_DIR}) + + CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H) + if (NOT HAVE_JSONCPP_H) + message(FATAL_ERROR "Please install the libjsoncpp-dev package") + endif() + + # Look for Orthanc framework shared library + include(CheckCXXSymbolExists) + + if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows") + set(ORTHANC_FRAMEWORK_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT}) + else() + find_path(ORTHANC_FRAMEWORK_INCLUDE_DIR OrthancFramework.h + /usr/include/orthanc-framework + /usr/local/include/orthanc-framework + ${ORTHANC_FRAMEWORK_ROOT} + ) + endif() + + if (${ORTHANC_FRAMEWORK_INCLUDE_DIR} STREQUAL "ORTHANC_FRAMEWORK_INCLUDE_DIR-NOTFOUND") + message(FATAL_ERROR "Cannot locate the OrthancFramework.h header") + endif() + + message("Orthanc framework include dir: ${ORTHANC_FRAMEWORK_INCLUDE_DIR}") + include_directories(${ORTHANC_FRAMEWORK_INCLUDE_DIR}) + + if (ORTHANC_FRAMEWORK_USE_SHARED) + set(CMAKE_REQUIRED_INCLUDES "${ORTHANC_FRAMEWORK_INCLUDE_DIR}") + set(CMAKE_REQUIRED_LIBRARIES "${ORTHANC_FRAMEWORK_LIBRARIES}") + + check_cxx_symbol_exists("Orthanc::InitializeFramework" "OrthancFramework.h" HAVE_ORTHANC_FRAMEWORK) + if (NOT HAVE_ORTHANC_FRAMEWORK) + message(FATAL_ERROR "Cannot find the Orthanc framework") + endif() + + unset(CMAKE_REQUIRED_INCLUDES) + unset(CMAKE_REQUIRED_LIBRARIES) + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/DownloadPackage.cmake b/OrthancFramework/Resources/CMake/DownloadPackage.cmake new file mode 100644 index 0000000..cba2c98 --- /dev/null +++ b/OrthancFramework/Resources/CMake/DownloadPackage.cmake @@ -0,0 +1,287 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +macro(GetUrlFilename TargetVariable Url) + string(REGEX REPLACE "^.*/" "" ${TargetVariable} "${Url}") +endmacro() + + +macro(GetUrlExtension TargetVariable Url) + #string(REGEX REPLACE "^.*/[^.]*\\." "" TMP "${Url}") + string(REGEX REPLACE "^.*\\." "" TMP "${Url}") + string(TOLOWER "${TMP}" "${TargetVariable}") +endmacro() + + + +## +## Setup the patch command-line tool +## + +if (NOT ORTHANC_DISABLE_PATCH) + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + set(PATCH_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/patch/patch.exe) + if (NOT EXISTS ${PATCH_EXECUTABLE}) + message(FATAL_ERROR "Unable to find the patch.exe tool that is shipped with Orthanc") + endif() + + else () + find_program(PATCH_EXECUTABLE patch) + if (${PATCH_EXECUTABLE} MATCHES "PATCH_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'patch' standard command-line tool") + endif() + endif() +endif() + + + +## +## Check the existence of the required decompression tools +## + +if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + find_program(ZIP_EXECUTABLE 7z + PATHS + "$ENV{ProgramFiles}/7-Zip" + "$ENV{ProgramW6432}/7-Zip" + ) + + if (${ZIP_EXECUTABLE} MATCHES "ZIP_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)") + endif() + +else() + find_program(UNZIP_EXECUTABLE unzip) + if (${UNZIP_EXECUTABLE} MATCHES "UNZIP_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'unzip' package") + endif() + + find_program(TAR_EXECUTABLE tar) + if (${TAR_EXECUTABLE} MATCHES "TAR_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'tar' package") + endif() + + find_program(GUNZIP_EXECUTABLE gunzip) + if (${GUNZIP_EXECUTABLE} MATCHES "GUNZIP_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'gzip' package") + endif() +endif() + + +macro(DownloadFile MD5 Url) + GetUrlFilename(TMP_FILENAME "${Url}") + + set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}") + if (NOT EXISTS "${TMP_PATH}") + message("Downloading ${Url}") + + # This fixes issue 6: "I think cmake shouldn't download the + # packages which are not in the system, it should stop and let + # user know." + # https://code.google.com/p/orthanc/issues/detail?id=6 + if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS) + message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON") + endif() + + foreach (retry RANGE 1 5) # Retries 5 times + if ("${MD5}" STREQUAL "no-check") + message(WARNING "Not checking the MD5 of: ${Url}") + file(DOWNLOAD "${Url}" "${TMP_PATH}" + SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10 + STATUS Failure) + else() + file(DOWNLOAD "${Url}" "${TMP_PATH}" + SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10 + EXPECTED_MD5 "${MD5}" STATUS Failure) + endif() + + list(GET Failure 0 Status) + if (Status EQUAL 0) + break() # Successful download + endif() + endforeach() + + if (NOT Status EQUAL 0) + file(REMOVE ${TMP_PATH}) + message(FATAL_ERROR "Cannot download file: ${Url}") + endif() + + else() + message("Using local copy of ${Url}") + + if ("${MD5}" STREQUAL "no-check") + message(WARNING "Not checking the MD5 of: ${Url}") + else() + file(MD5 ${TMP_PATH} ActualMD5) + if (NOT "${ActualMD5}" STREQUAL "${MD5}") + message(FATAL_ERROR "The MD5 hash of a previously download file is invalid: ${TMP_PATH}") + endif() + endif() + endif() +endmacro() + + +macro(DownloadPackage MD5 Url TargetDirectory) + if (NOT IS_DIRECTORY "${TargetDirectory}") + DownloadFile("${MD5}" "${Url}") + + GetUrlExtension(TMP_EXTENSION "${Url}") + #message(${TMP_EXTENSION}) + message("Uncompressing ${TMP_FILENAME}") + + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + # How to silently extract files using 7-zip + # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly + + if (("${TMP_EXTENSION}" STREQUAL "gz") OR + ("${TMP_EXTENSION}" STREQUAL "tgz") OR + ("${TMP_EXTENSION}" STREQUAL "xz")) + execute_process( + COMMAND ${ZIP_EXECUTABLE} e -y ${TMP_PATH} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if ("${TMP_EXTENSION}" STREQUAL "tgz") + string(REGEX REPLACE ".tgz$" ".tar" TMP_FILENAME2 "${TMP_FILENAME}") + elseif ("${TMP_EXTENSION}" STREQUAL "gz") + string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}") + elseif ("${TMP_EXTENSION}" STREQUAL "xz") + string(REGEX REPLACE ".xz" "" TMP_FILENAME2 "${TMP_FILENAME}") + endif() + + execute_process( + COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_FILENAME2} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + elseif ("${TMP_EXTENSION}" STREQUAL "zip") + execute_process( + COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_PATH} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + else() + message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}") + endif() + + else() + if ("${TMP_EXTENSION}" STREQUAL "zip") + execute_process( + COMMAND sh -c "${UNZIP_EXECUTABLE} -q ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + elseif (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz")) + #message("tar xvfz ${TMP_PATH}") + execute_process( + COMMAND sh -c "${TAR_EXECUTABLE} xfz ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + elseif ("${TMP_EXTENSION}" STREQUAL "bz2") + execute_process( + COMMAND sh -c "${TAR_EXECUTABLE} xfj ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + elseif ("${TMP_EXTENSION}" STREQUAL "xz") + execute_process( + COMMAND sh -c "${TAR_EXECUTABLE} xf ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + else() + message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}") + endif() + endif() + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if (NOT IS_DIRECTORY "${TargetDirectory}") + message(FATAL_ERROR "The package was not uncompressed at the proper location. Check the CMake instructions.") + endif() + endif() +endmacro() + + + +macro(DownloadCompressedFile MD5 Url TargetFile) + if (NOT EXISTS "${TargetFile}") + DownloadFile("${MD5}" "${Url}") + + GetUrlExtension(TMP_EXTENSION "${Url}") + #message(${TMP_EXTENSION}) + message("Uncompressing ${TMP_FILENAME}") + + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + # How to silently extract files using 7-zip + # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly + + if ("${TMP_EXTENSION}" STREQUAL "gz") + execute_process( + # "-so" writes uncompressed file to stdout + COMMAND ${ZIP_EXECUTABLE} e -so -y ${TMP_PATH} + OUTPUT_FILE "${TargetFile}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + else() + message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}") + endif() + + else() + if ("${TMP_EXTENSION}" STREQUAL "gz") + execute_process( + COMMAND sh -c "${GUNZIP_EXECUTABLE} -c ${TMP_PATH}" + OUTPUT_FILE "${TargetFile}" + RESULT_VARIABLE Failure + ) + else() + message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}") + endif() + endif() + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if (NOT EXISTS "${TargetFile}") + message(FATAL_ERROR "The file was not uncompressed at the proper location. Check the CMake instructions.") + endif() + endif() +endmacro() diff --git a/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake b/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake new file mode 100644 index 0000000..7181d2e --- /dev/null +++ b/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake @@ -0,0 +1,65 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +# https://github.com/emscripten-core/emscripten/blob/master/src/settings.js + +if (NOT "${EMSCRIPTEN_TRAP_MODE}" STREQUAL "") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s BINARYEN_TRAP_MODE='\"${EMSCRIPTEN_TRAP_MODE}\"'") +endif() + +# If "-O3" is used (the default in "Release" mode), this results in a +# too large memory consumption in "wasm-opt", at least in Emscripten +# 3.1.7, which ultimately crashes the compiler. So we force "-O2" +# (this also has the advantage of speeding up the build): +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG") + +# "DISABLE_EXCEPTION_CATCHING" is a "compile+link" option. HOWEVER, +# setting it inside "WASM_FLAGS" creates link errors, at least with +# side modules. TODO: Understand why +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") + +# "-Wno-unused-command-line-argument" is used to avoid annoying +# warnings about setting WASM, FETCH and ASSERTIONS, which was +# required for earlier versions of emsdk: +# https://groups.google.com/g/emscripten-discuss/c/VX4enWfadUE +set(WASM_FLAGS "${WASM_FLAGS} -Wno-unused-command-line-argument") + +#set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") + +if (EMSCRIPTEN_TARGET_MODE STREQUAL "wasm") + # WebAssembly + set(WASM_FLAGS "${WASM_FLAGS} -s WASM=1") + +elseif (EMSCRIPTEN_TARGET_MODE STREQUAL "asm.js") + # asm.js targeting IE 11 + set(WASM_FLAGS "-s WASM=0 -s ASM_JS=2 -s LEGACY_VM_SUPPORT=1") + +else() + message(FATAL_ERROR "Bad value for EMSCRIPTEN_TARGET_MODE: ${EMSCRIPTEN_TARGET_MODE}") +endif() + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(WASM_FLAGS "${WASM_FLAGS} -s SAFE_HEAP=1 -s ASSERTIONS=1") +endif() + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}") diff --git a/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake b/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake new file mode 100644 index 0000000..aa4fe4e --- /dev/null +++ b/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake @@ -0,0 +1,91 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (USE_GOOGLE_TEST_DEBIAN_PACKAGE) + find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR + NAMES src/gtest-all.cc + PATHS + ${CROSSTOOL_NG_IMAGE}/usr/src/gtest + ${CROSSTOOL_NG_IMAGE}/usr/src/googletest/googletest + PATH_SUFFIXES src + ) + + find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR + NAMES gtest.h + PATHS + ${CROSSTOOL_NG_IMAGE}/usr/include/gtest + ) + + message("Path to the Debian Google Test sources: ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}") + message("Path to the Debian Google Test includes: ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}") + + set(GOOGLE_TEST_SOURCES + ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}/src/gtest-all.cc + ) + + include_directories(${GOOGLE_TEST_DEBIAN_SOURCES_DIR}) + + if (NOT EXISTS ${GOOGLE_TEST_SOURCES} OR + NOT EXISTS ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}/gtest.h) + message(FATAL_ERROR "Please install the libgtest-dev package") + endif() + +elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST) + set(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/googletest-release-1.8.1) + set(GOOGLE_TEST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/gtest-1.8.1.tar.gz") + set(GOOGLE_TEST_MD5 "2e6fbeb6a91310a16efe181886c59596") + + DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}") + + include_directories( + ${GOOGLE_TEST_SOURCES_DIR}/googletest + ${GOOGLE_TEST_SOURCES_DIR}/googletest/include + ${GOOGLE_TEST_SOURCES_DIR} + ) + + set(GOOGLE_TEST_SOURCES + ${GOOGLE_TEST_SOURCES_DIR}/googletest/src/gtest-all.cc + ) + + # https://code.google.com/p/googletest/issues/detail?id=412 + if (MSVC) # VS2012 does not support tuples correctly yet + add_definitions(/D _VARIADIC_MAX=10) + endif() + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + add_definitions(-DGTEST_HAS_CLONE=0) + endif() + + source_group(ThirdParty\\GoogleTest REGULAR_EXPRESSION ${GOOGLE_TEST_SOURCES_DIR}/.*) + +else() + include(FindGTest) + if (NOT GTEST_FOUND) + message(FATAL_ERROR "Unable to find GoogleTest") + endif() + + include_directories(${GTEST_INCLUDE_DIRS}) + + # The variable GTEST_LIBRARIES contains the shared library of + # Google Test, create an alias for more uniformity + set(GOOGLE_TEST_LIBRARIES ${GTEST_LIBRARIES}) +endif() diff --git a/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake b/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake new file mode 100644 index 0000000..aa0a3fa --- /dev/null +++ b/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake @@ -0,0 +1,102 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +set(JSONCPP_CXX11 OFF) + +if (STATIC_BUILD OR NOT USE_SYSTEM_JSONCPP) + if (USE_LEGACY_JSONCPP) + set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-0.10.7) + set(JSONCPP_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/jsoncpp-0.10.7.tar.gz") + set(JSONCPP_MD5 "3a8072ca6a1fa9cbaf7715ae625f134f") + add_definitions(-DORTHANC_LEGACY_JSONCPP=1) + else() + set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-1.9.5) + set(JSONCPP_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/jsoncpp-1.9.5.tar.gz") + set(JSONCPP_MD5 "d6c8c609f2162eff373db62b90a051c7") + add_definitions(-DORTHANC_LEGACY_JSONCPP=0) + set(JSONCPP_CXX11 ON) + endif() + + DownloadPackage(${JSONCPP_MD5} ${JSONCPP_URL} "${JSONCPP_SOURCES_DIR}") + + set(JSONCPP_SOURCES + ${JSONCPP_SOURCES_DIR}/src/lib_json/json_reader.cpp + ${JSONCPP_SOURCES_DIR}/src/lib_json/json_value.cpp + ${JSONCPP_SOURCES_DIR}/src/lib_json/json_writer.cpp + ) + + include_directories( + ${JSONCPP_SOURCES_DIR}/include + ) + + if (NOT ENABLE_LOCALE) + add_definitions(-DJSONCPP_NO_LOCALE_SUPPORT=1) + endif() + + source_group(ThirdParty\\JsonCpp REGULAR_EXPRESSION ${JSONCPP_SOURCES_DIR}/.*) + +else() + find_path(JSONCPP_INCLUDE_DIR json/reader.h + /usr/include/jsoncpp + /usr/local/include/jsoncpp + ) + + message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}") + include_directories(${JSONCPP_INCLUDE_DIR}) + link_libraries(jsoncpp) + + CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H) + if (NOT HAVE_JSONCPP_H) + message(FATAL_ERROR "Please install the libjsoncpp-dev package") + endif() + + # Detect if the version of JsonCpp is >= 1.0.0 + if (EXISTS ${JSONCPP_INCLUDE_DIR}/json/version.h) + file(STRINGS + "${JSONCPP_INCLUDE_DIR}/json/version.h" + JSONCPP_VERSION_MAJOR1 REGEX + ".*define JSONCPP_VERSION_MAJOR.*") + + if (NOT JSONCPP_VERSION_MAJOR1) + message(FATAL_ERROR "Unable to extract the major version of JsonCpp") + endif() + + string(REGEX REPLACE + ".*JSONCPP_VERSION_MAJOR.*([0-9]+)$" "\\1" + JSONCPP_VERSION_MAJOR ${JSONCPP_VERSION_MAJOR1}) + message("JsonCpp major version: ${JSONCPP_VERSION_MAJOR}") + + if (JSONCPP_VERSION_MAJOR GREATER 0) + set(JSONCPP_CXX11 ON) + endif() + else() + message("Unable to detect the major version of JsonCpp, assuming < 1.0.0") + endif() +endif() + + +if (JSONCPP_CXX11) + # Osimis has encountered problems when this macro is left at its + # default value (1000), so we increase this limit + # https://gitlab.kitware.com/third-party/jsoncpp/commit/56df2068470241f9043b676bfae415ed62a0c172 + add_definitions(-DJSONCPP_DEPRECATED_STACK_LIMIT=5000) +endif() diff --git a/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake new file mode 100644 index 0000000..e9ddb24 --- /dev/null +++ b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake @@ -0,0 +1,364 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_CURL) + SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-8.9.0) + SET(CURL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/curl-8.9.0.tar.gz") + SET(CURL_MD5 "f9bca5d4d5bac1f04e6c5eb4d0418618") + + if (IS_DIRECTORY "${CURL_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + + DownloadPackage(${CURL_MD5} ${CURL_URL} "${CURL_SOURCES_DIR}") + + if (FirstRun) + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/curl-8.9.0.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + endif() + + include_directories( + ${CURL_SOURCES_DIR}/include + ) + + AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib CURL_SOURCES) + AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vauth CURL_SOURCES) + AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vssh CURL_SOURCES) + AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vtls CURL_SOURCES) + AUX_SOURCE_DIRECTORY(${CURL_SOURCES_DIR}/lib/vquic CURL_SOURCES) + source_group(ThirdParty\\LibCurl REGULAR_EXPRESSION ${CURL_SOURCES_DIR}/.*) + + add_definitions( + -DBUILDING_LIBCURL=1 + -DCURL_STATICLIB=1 + -DCURL_DISABLE_LDAPS=1 + -DCURL_DISABLE_LDAP=1 + -DCURL_DISABLE_DICT=1 + -DCURL_DISABLE_FILE=1 + -DCURL_DISABLE_FTP=1 + -DCURL_DISABLE_GOPHER=1 + -DCURL_DISABLE_LDAP=1 + -DCURL_DISABLE_LDAPS=1 + -DCURL_DISABLE_POP3=1 + #-DCURL_DISABLE_PROXY=1 + -DCURL_DISABLE_RTSP=1 + -DCURL_DISABLE_TELNET=1 + -DCURL_DISABLE_TFTP=1 + ) + + if (ENABLE_SSL) + add_definitions( + #-DHAVE_LIBSSL=1 + -DUSE_OPENSSL=1 + -DHAVE_OPENSSL_ENGINE_H=1 + -DUSE_SSLEAY=1 + ) + endif() + + if (NOT EXISTS "${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.h") + file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/digest.h "#include \"../digest.h\"\n") + file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/ntlm.h "#include \"../ntlm.h\"\n") + file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.h "#include \"../vauth.h\"\n") + file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/vtls/vtls.h "#include \"../../vtls/vtls.h\"\n") + file(WRITE ${CURL_SOURCES_DIR}/lib/vssh/curl_setup.h "#include \"../curl_setup.h\"\n") + file(WRITE ${CURL_SOURCES_DIR}/lib/vtls/vauth/vauth.h "#include \"../../vauth/vauth.h\"\n") + + file(GLOB CURL_LIBS_HEADERS ${CURL_SOURCES_DIR}/lib/*.h) + foreach (header IN LISTS CURL_LIBS_HEADERS) + get_filename_component(filename ${header} NAME) + file(WRITE ${CURL_SOURCES_DIR}/lib/vauth/${filename} "#include \"../${filename}\"\n") + file(WRITE ${CURL_SOURCES_DIR}/lib/vquic/${filename} "#include \"../${filename}\"\n") + file(WRITE ${CURL_SOURCES_DIR}/lib/vtls/${filename} "#include \"../${filename}\"\n") + endforeach() + endif() + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + SET(TMP_OS "x86_64") + else() + SET(TMP_OS "x86") + endif() + + set_property( + SOURCE ${CURL_SOURCES} APPEND + PROPERTY COMPILE_DEFINITIONS "HAVE_CONFIG_H=1;OS=\"${TMP_OS}\"" + ) + + include(${CURL_SOURCES_DIR}/CMake/Macros.cmake) + + # WARNING: Do *not* reorder the "check_include_file_concat()" below! + check_include_file_concat("stdio.h" HAVE_STDIO_H) + check_include_file_concat("inttypes.h" HAVE_INTTYPES_H) + check_include_file_concat("sys/filio.h" HAVE_SYS_FILIO_H) + check_include_file_concat("sys/ioctl.h" HAVE_SYS_IOCTL_H) + check_include_file_concat("sys/param.h" HAVE_SYS_PARAM_H) + check_include_file_concat("sys/poll.h" HAVE_SYS_POLL_H) + check_include_file_concat("sys/resource.h" HAVE_SYS_RESOURCE_H) + check_include_file_concat("sys/select.h" HAVE_SYS_SELECT_H) + check_include_file_concat("sys/socket.h" HAVE_SYS_SOCKET_H) + check_include_file_concat("sys/sockio.h" HAVE_SYS_SOCKIO_H) + check_include_file_concat("sys/stat.h" HAVE_SYS_STAT_H) + check_include_file_concat("sys/time.h" HAVE_SYS_TIME_H) + check_include_file_concat("sys/types.h" HAVE_SYS_TYPES_H) + check_include_file_concat("sys/uio.h" HAVE_SYS_UIO_H) + check_include_file_concat("sys/un.h" HAVE_SYS_UN_H) + check_include_file_concat("sys/utime.h" HAVE_SYS_UTIME_H) + check_include_file_concat("sys/xattr.h" HAVE_SYS_XATTR_H) + check_include_file_concat("alloca.h" HAVE_ALLOCA_H) + check_include_file_concat("arpa/inet.h" HAVE_ARPA_INET_H) + check_include_file_concat("arpa/tftp.h" HAVE_ARPA_TFTP_H) + check_include_file_concat("assert.h" HAVE_ASSERT_H) + check_include_file_concat("crypto.h" HAVE_CRYPTO_H) + check_include_file_concat("des.h" HAVE_DES_H) + check_include_file_concat("err.h" HAVE_ERR_H) + check_include_file_concat("errno.h" HAVE_ERRNO_H) + check_include_file_concat("fcntl.h" HAVE_FCNTL_H) + check_include_file_concat("idn2.h" HAVE_IDN2_H) + check_include_file_concat("ifaddrs.h" HAVE_IFADDRS_H) + check_include_file_concat("io.h" HAVE_IO_H) + check_include_file_concat("krb.h" HAVE_KRB_H) + check_include_file_concat("libgen.h" HAVE_LIBGEN_H) + check_include_file_concat("limits.h" HAVE_LIMITS_H) + check_include_file_concat("locale.h" HAVE_LOCALE_H) + check_include_file_concat("net/if.h" HAVE_NET_IF_H) + check_include_file_concat("netdb.h" HAVE_NETDB_H) + check_include_file_concat("netinet/in.h" HAVE_NETINET_IN_H) + check_include_file_concat("netinet/tcp.h" HAVE_NETINET_TCP_H) + + check_include_file_concat("pem.h" HAVE_PEM_H) + check_include_file_concat("poll.h" HAVE_POLL_H) + check_include_file_concat("pwd.h" HAVE_PWD_H) + check_include_file_concat("rsa.h" HAVE_RSA_H) + check_include_file_concat("setjmp.h" HAVE_SETJMP_H) + check_include_file_concat("sgtty.h" HAVE_SGTTY_H) + check_include_file_concat("signal.h" HAVE_SIGNAL_H) + check_include_file_concat("ssl.h" HAVE_SSL_H) + check_include_file_concat("stdbool.h" HAVE_STDBOOL_H) + check_include_file_concat("stdint.h" HAVE_STDINT_H) + check_include_file_concat("stdio.h" HAVE_STDIO_H) + check_include_file_concat("stdlib.h" HAVE_STDLIB_H) + check_include_file_concat("string.h" HAVE_STRING_H) + check_include_file_concat("strings.h" HAVE_STRINGS_H) + check_include_file_concat("stropts.h" HAVE_STROPTS_H) + check_include_file_concat("termio.h" HAVE_TERMIO_H) + check_include_file_concat("termios.h" HAVE_TERMIOS_H) + check_include_file_concat("time.h" HAVE_TIME_H) + check_include_file_concat("unistd.h" HAVE_UNISTD_H) + check_include_file_concat("utime.h" HAVE_UTIME_H) + check_include_file_concat("x509.h" HAVE_X509_H) + + check_include_file_concat("process.h" HAVE_PROCESS_H) + check_include_file_concat("stddef.h" HAVE_STDDEF_H) + check_include_file_concat("dlfcn.h" HAVE_DLFCN_H) + check_include_file_concat("malloc.h" HAVE_MALLOC_H) + check_include_file_concat("memory.h" HAVE_MEMORY_H) + check_include_file_concat("netinet/if_ether.h" HAVE_NETINET_IF_ETHER_H) + check_include_file_concat("stdint.h" HAVE_STDINT_H) + check_include_file_concat("sockio.h" HAVE_SOCKIO_H) + check_include_file_concat("sys/utsname.h" HAVE_SYS_UTSNAME_H) + + check_type_size("size_t" SIZEOF_SIZE_T) + check_type_size("ssize_t" SIZEOF_SSIZE_T) + check_type_size("long long" SIZEOF_LONG_LONG) + check_type_size("long" SIZEOF_LONG) + check_type_size("short" SIZEOF_SHORT) + check_type_size("int" SIZEOF_INT) + check_type_size("__int64" SIZEOF___INT64) + check_type_size("long double" SIZEOF_LONG_DOUBLE) + check_type_size("time_t" SIZEOF_TIME_T) + check_type_size("off_t" SIZEOF_OFF_T) + check_type_size("socklen_t" CURL_SIZEOF_CURL_SOCKLEN_T) + + check_symbol_exists(basename "${CURL_INCLUDES}" HAVE_BASENAME) + check_symbol_exists(socket "${CURL_INCLUDES}" HAVE_SOCKET) + # poll on macOS is unreliable, it first did not exist, then was broken until + # fixed in 10.9 only to break again in 10.12. + if(NOT APPLE) + check_symbol_exists(poll "${CURL_INCLUDES}" HAVE_POLL) + endif() + check_symbol_exists(select "${CURL_INCLUDES}" HAVE_SELECT) + check_symbol_exists(strdup "${CURL_INCLUDES}" HAVE_STRDUP) + check_symbol_exists(strstr "${CURL_INCLUDES}" HAVE_STRSTR) + check_symbol_exists(strtok_r "${CURL_INCLUDES}" HAVE_STRTOK_R) + check_symbol_exists(strftime "${CURL_INCLUDES}" HAVE_STRFTIME) + check_symbol_exists(uname "${CURL_INCLUDES}" HAVE_UNAME) + check_symbol_exists(strcasecmp "${CURL_INCLUDES}" HAVE_STRCASECMP) + check_symbol_exists(stricmp "${CURL_INCLUDES}" HAVE_STRICMP) + check_symbol_exists(strcmpi "${CURL_INCLUDES}" HAVE_STRCMPI) + check_symbol_exists(strncmpi "${CURL_INCLUDES}" HAVE_STRNCMPI) + check_symbol_exists(alarm "${CURL_INCLUDES}" HAVE_ALARM) + if(NOT HAVE_STRNCMPI) + set(HAVE_STRCMPI) + endif(NOT HAVE_STRNCMPI) + + check_symbol_exists(gethostbyaddr "${CURL_INCLUDES}" HAVE_GETHOSTBYADDR) + check_symbol_exists(gethostbyaddr_r "${CURL_INCLUDES}" HAVE_GETHOSTBYADDR_R) + check_symbol_exists(gettimeofday "${CURL_INCLUDES}" HAVE_GETTIMEOFDAY) + check_symbol_exists(inet_addr "${CURL_INCLUDES}" HAVE_INET_ADDR) + check_symbol_exists(inet_ntoa "${CURL_INCLUDES}" HAVE_INET_NTOA) + check_symbol_exists(inet_ntoa_r "${CURL_INCLUDES}" HAVE_INET_NTOA_R) + check_symbol_exists(tcsetattr "${CURL_INCLUDES}" HAVE_TCSETATTR) + check_symbol_exists(tcgetattr "${CURL_INCLUDES}" HAVE_TCGETATTR) + check_symbol_exists(perror "${CURL_INCLUDES}" HAVE_PERROR) + check_symbol_exists(closesocket "${CURL_INCLUDES}" HAVE_CLOSESOCKET) + check_symbol_exists(setvbuf "${CURL_INCLUDES}" HAVE_SETVBUF) + check_symbol_exists(sigsetjmp "${CURL_INCLUDES}" HAVE_SIGSETJMP) + check_symbol_exists(getpass_r "${CURL_INCLUDES}" HAVE_GETPASS_R) + check_symbol_exists(strlcat "${CURL_INCLUDES}" HAVE_STRLCAT) + check_symbol_exists(getpwuid "${CURL_INCLUDES}" HAVE_GETPWUID) + check_symbol_exists(geteuid "${CURL_INCLUDES}" HAVE_GETEUID) + check_symbol_exists(utime "${CURL_INCLUDES}" HAVE_UTIME) + check_symbol_exists(gmtime_r "${CURL_INCLUDES}" HAVE_GMTIME_R) + check_symbol_exists(localtime_r "${CURL_INCLUDES}" HAVE_LOCALTIME_R) + + check_symbol_exists(gethostbyname "${CURL_INCLUDES}" HAVE_GETHOSTBYNAME) + check_symbol_exists(gethostbyname_r "${CURL_INCLUDES}" HAVE_GETHOSTBYNAME_R) + + check_symbol_exists(signal "${CURL_INCLUDES}" HAVE_SIGNAL_FUNC) + check_symbol_exists(SIGALRM "${CURL_INCLUDES}" HAVE_SIGNAL_MACRO) + if(HAVE_SIGNAL_FUNC AND HAVE_SIGNAL_MACRO) + set(HAVE_SIGNAL 1) + endif(HAVE_SIGNAL_FUNC AND HAVE_SIGNAL_MACRO) + check_symbol_exists(uname "${CURL_INCLUDES}" HAVE_UNAME) + check_symbol_exists(strtoll "${CURL_INCLUDES}" HAVE_STRTOLL) + check_symbol_exists(_strtoi64 "${CURL_INCLUDES}" HAVE__STRTOI64) + check_symbol_exists(strerror_r "${CURL_INCLUDES}" HAVE_STRERROR_R) + check_symbol_exists(siginterrupt "${CURL_INCLUDES}" HAVE_SIGINTERRUPT) + check_symbol_exists(perror "${CURL_INCLUDES}" HAVE_PERROR) + check_symbol_exists(fork "${CURL_INCLUDES}" HAVE_FORK) + check_symbol_exists(getaddrinfo "${CURL_INCLUDES}" HAVE_GETADDRINFO) + check_symbol_exists(freeaddrinfo "${CURL_INCLUDES}" HAVE_FREEADDRINFO) + check_symbol_exists(freeifaddrs "${CURL_INCLUDES}" HAVE_FREEIFADDRS) + check_symbol_exists(pipe "${CURL_INCLUDES}" HAVE_PIPE) + check_symbol_exists(ftruncate "${CURL_INCLUDES}" HAVE_FTRUNCATE) + check_symbol_exists(getprotobyname "${CURL_INCLUDES}" HAVE_GETPROTOBYNAME) + check_symbol_exists(getrlimit "${CURL_INCLUDES}" HAVE_GETRLIMIT) + check_symbol_exists(setlocale "${CURL_INCLUDES}" HAVE_SETLOCALE) + check_symbol_exists(setmode "${CURL_INCLUDES}" HAVE_SETMODE) + check_symbol_exists(setrlimit "${CURL_INCLUDES}" HAVE_SETRLIMIT) + check_symbol_exists(fcntl "${CURL_INCLUDES}" HAVE_FCNTL) + check_symbol_exists(ioctl "${CURL_INCLUDES}" HAVE_IOCTL) + check_symbol_exists(setsockopt "${CURL_INCLUDES}" HAVE_SETSOCKOPT) + + if(HAVE_SIZEOF_LONG_LONG) + set(HAVE_LONGLONG 1) + set(HAVE_LL 1) + endif(HAVE_SIZEOF_LONG_LONG) + + check_function_exists(mach_absolute_time HAVE_MACH_ABSOLUTE_TIME) + check_function_exists(gethostname HAVE_GETHOSTNAME) + + check_include_file_concat("pthread.h" HAVE_PTHREAD_H) + check_symbol_exists(recv "sys/socket.h" HAVE_RECV) + check_symbol_exists(send "sys/socket.h" HAVE_SEND) + + check_struct_has_member("struct sockaddr_un" sun_path "sys/un.h" USE_UNIX_SOCKETS) + + list(APPEND CMAKE_REQUIRED_INCLUDES "${CURL_SOURCES_DIR}/include") + set(CMAKE_EXTRA_INCLUDE_FILES "curl/system.h") + check_type_size("curl_off_t" SIZEOF_CURL_OFF_T) + + add_definitions(-DHAVE_GLIBC_STRERROR_R=1) + + include(${CURL_SOURCES_DIR}/CMake/OtherTests.cmake) + + foreach(CURL_TEST + HAVE_FCNTL_O_NONBLOCK + HAVE_IOCTLSOCKET + HAVE_IOCTLSOCKET_CAMEL + HAVE_IOCTLSOCKET_CAMEL_FIONBIO + HAVE_IOCTLSOCKET_FIONBIO + HAVE_IOCTL_FIONBIO + HAVE_IOCTL_SIOCGIFADDR + HAVE_SETSOCKOPT_SO_NONBLOCK + HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID + TIME_WITH_SYS_TIME + HAVE_O_NONBLOCK + HAVE_GETHOSTBYADDR_R_5 + HAVE_GETHOSTBYADDR_R_7 + HAVE_GETHOSTBYADDR_R_8 + HAVE_GETHOSTBYADDR_R_5_REENTRANT + HAVE_GETHOSTBYADDR_R_7_REENTRANT + HAVE_GETHOSTBYADDR_R_8_REENTRANT + HAVE_GETHOSTBYNAME_R_3 + HAVE_GETHOSTBYNAME_R_5 + HAVE_GETHOSTBYNAME_R_6 + HAVE_GETHOSTBYNAME_R_3_REENTRANT + HAVE_GETHOSTBYNAME_R_5_REENTRANT + HAVE_GETHOSTBYNAME_R_6_REENTRANT + HAVE_SOCKLEN_T + HAVE_IN_ADDR_T + HAVE_BOOL_T + STDC_HEADERS + RETSIGTYPE_TEST + HAVE_INET_NTOA_R_DECL + HAVE_INET_NTOA_R_DECL_REENTRANT + HAVE_GETADDRINFO + HAVE_FILE_OFFSET_BITS + ) + curl_internal_test(${CURL_TEST}) + endforeach(CURL_TEST) + + configure_file( + ${CURL_SOURCES_DIR}/lib/curl_config.h.cmake + ${CURL_SOURCES_DIR}/lib/curl_config.h + ) + endif() + +elseif (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + + CHECK_INCLUDE_FILE_CXX(curl/curl.h HAVE_CURL_H) + if (NOT HAVE_CURL_H) + message(FATAL_ERROR "Please install the libcurl-dev package") + endif() + + CHECK_LIBRARY_EXISTS(curl "curl_easy_init" "" HAVE_CURL_LIB) + if (NOT HAVE_CURL_LIB) + message(FATAL_ERROR "Please install the libcurl package") + endif() + + link_libraries(curl) + +else() + include(FindCURL) + include_directories(${CURL_INCLUDE_DIRS}) + link_libraries(${CURL_LIBRARIES}) + + if (NOT ${CURL_FOUND}) + message(FATAL_ERROR "Unable to find LibCurl") + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake b/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake new file mode 100644 index 0000000..a4a0ff3 --- /dev/null +++ b/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake @@ -0,0 +1,112 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +message("Using libiconv") + +if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICONV) + set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.15) + set(LIBICONV_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/libiconv-1.15.tar.gz") + set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808") + + DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}") + + # Disable the support of libiconv that is shipped by default with + # the C standard library on Linux. Setting this macro redirects + # calls from "iconv*()" to "libiconv*()" by defining macros in the + # C headers of "libiconv-1.15". + add_definitions(-DLIBICONV_PLUG=1) + + # https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ + add_definitions( + -DBUILDING_LIBICONV=1 + -DIN_LIBRARY=1 + -DLIBDIR="" + -DICONV_CONST= + #-DENABLE_EXTRA=1 + ) + + configure_file( + ${LIBICONV_SOURCES_DIR}/srclib/localcharset.h + ${LIBICONV_SOURCES_DIR}/include + COPYONLY) + + set(HAVE_VISIBILITY 0) + set(ICONV_CONST ${ICONV_CONST}) + set(USE_MBSTATE_T 1) + set(BROKEN_WCHAR_H 0) + set(EILSEQ) + set(HAVE_WCHAR_T 1) + configure_file( + ${LIBICONV_SOURCES_DIR}/include/iconv.h.build.in + ${LIBICONV_SOURCES_DIR}/include/iconv.h + ) + unset(HAVE_VISIBILITY) + unset(ICONV_CONST) + unset(USE_MBSTATE_T) + unset(BROKEN_WCHAR_H) + unset(EILSEQ) + unset(HAVE_WCHAR_T) + + if (NOT EXISTS ${LIBICONV_SOURCES_DIR}/include/config.h) + # Create an empty "config.h" for libiconv + file(WRITE ${LIBICONV_SOURCES_DIR}/include/config.h "") + endif() + + include_directories( + ${LIBICONV_SOURCES_DIR}/include + ) + + set(LIBICONV_SOURCES + ${LIBICONV_SOURCES_DIR}/lib/iconv.c + ${LIBICONV_SOURCES_DIR}/lib/relocatable.c + ${LIBICONV_SOURCES_DIR}/libcharset/lib/localcharset.c + ${LIBICONV_SOURCES_DIR}/libcharset/lib/relocatable.c + ) + + source_group(ThirdParty\\libiconv REGULAR_EXPRESSION ${LIBICONV_SOURCES_DIR}/.*) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_definitions(-DHAVE_WORKING_O_NOFOLLOW=0) + else() + add_definitions(-DHAVE_WORKING_O_NOFOLLOW=1) + endif() + +else() + CHECK_INCLUDE_FILE_CXX(iconv.h HAVE_ICONV_H) + if (NOT HAVE_ICONV_H) + message(FATAL_ERROR "Please install the libiconv-dev package") + endif() + + # Check whether the support for libiconv is bundled within the + # standard C library + CHECK_FUNCTION_EXISTS(iconv_open HAVE_ICONV_LIB) + if (NOT HAVE_ICONV_LIB) + # No builtin support for libiconv, try and find an external library. + # Open question: Does this make sense on any platform? + CHECK_LIBRARY_EXISTS(iconv iconv_open "" HAVE_ICONV_LIB_2) + if (NOT HAVE_ICONV_LIB_2) + message(FATAL_ERROR "Please install the libiconv-dev package") + else() + link_libraries(iconv) + endif() + endif() +endif() diff --git a/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake b/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake new file mode 100644 index 0000000..d8dc4b7 --- /dev/null +++ b/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake @@ -0,0 +1,102 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + + +# Check out: ../ThirdParty/icu/README.txt + +# http://userguide.icu-project.org/packaging +# http://userguide.icu-project.org/howtouseicu + +message("Using libicu") + +if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICU) + include(${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/icu/Version.cmake) + DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}") + + # Use the gzip-compressed data + DownloadFile(${LIBICU_DATA_COMPRESSED_MD5} ${LIBICU_DATA_URL}) + set(LIBICU_RESOURCES + LIBICU_DATA ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${LIBICU_DATA} + ) + + set_source_files_properties( + ${CMAKE_BINARY_DIR}/${LIBICU_DATA} + PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t" + ) + + include_directories(BEFORE + ${LIBICU_SOURCES_DIR}/source/common + ${LIBICU_SOURCES_DIR}/source/i18n + ) + + aux_source_directory(${LIBICU_SOURCES_DIR}/source/common LIBICU_SOURCES) + aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n LIBICU_SOURCES) + + add_definitions( + #-DU_COMBINED_IMPLEMENTATION + #-DU_DEF_ICUDATA_ENTRY_POINT=icudt63l_dat + #-DU_LIB_SUFFIX_C_NAME=l + + #-DUCONFIG_NO_SERVICE=1 + -DU_COMMON_IMPLEMENTATION + -DU_STATIC_IMPLEMENTATION + -DU_ENABLE_DYLOAD=0 + -DU_HAVE_STD_STRING=1 + -DU_I18N_IMPLEMENTATION + -DU_IO_IMPLEMENTATION + -DU_STATIC_IMPLEMENTATION=1 + #-DU_CHARSET_IS_UTF8 + -DUNISTR_FROM_STRING_EXPLICIT= + + -DORTHANC_STATIC_ICU=1 + -DORTHANC_ICU_DATA_MD5="${LIBICU_DATA_UNCOMPRESSED_MD5}" + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + set_source_files_properties( + ${LIBICU_SOURCES_DIR}/source/common/locmap.c + PROPERTIES COMPILE_DEFINITIONS "LOCALE_SNAME=0x0000005c" + ) + endif() + + source_group(ThirdParty\\libicu REGULAR_EXPRESSION ${LIBICU_SOURCES_DIR}/.*) + +else() + CHECK_INCLUDE_FILE_CXX(unicode/uvernum.h HAVE_ICU_H) + if (NOT HAVE_ICU_H) + message(FATAL_ERROR "Please install the libicu-dev package") + endif() + + find_library(LIBICU_PATH_1 NAMES icuuc) + find_library(LIBICU_PATH_2 NAMES icui18n) + + if (NOT LIBICU_PATH_1 OR + NOT LIBICU_PATH_2) + message(FATAL_ERROR "Please install the libicu-dev package") + else() + link_libraries(${LIBICU_PATH_1} ${LIBICU_PATH_2}) + endif() + + add_definitions( + -DORTHANC_STATIC_ICU=0 + ) +endif() diff --git a/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake b/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake new file mode 100644 index 0000000..285ad6b --- /dev/null +++ b/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake @@ -0,0 +1,117 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_LIBJPEG) + set(LIBJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/jpeg-9f) + DownloadPackage( + "9ca58d68febb0fa9c1c087045b9a5483" + "https://orthanc.uclouvain.be/downloads/third-party-downloads/jpegsrc.v9f.tar.gz" + "${LIBJPEG_SOURCES_DIR}") + + include_directories( + ${LIBJPEG_SOURCES_DIR} + ) + + list(APPEND LIBJPEG_SOURCES + ${LIBJPEG_SOURCES_DIR}/jaricom.c + ${LIBJPEG_SOURCES_DIR}/jcapimin.c + ${LIBJPEG_SOURCES_DIR}/jcapistd.c + ${LIBJPEG_SOURCES_DIR}/jcarith.c + ${LIBJPEG_SOURCES_DIR}/jccoefct.c + ${LIBJPEG_SOURCES_DIR}/jccolor.c + ${LIBJPEG_SOURCES_DIR}/jcdctmgr.c + ${LIBJPEG_SOURCES_DIR}/jchuff.c + ${LIBJPEG_SOURCES_DIR}/jcinit.c + ${LIBJPEG_SOURCES_DIR}/jcmarker.c + ${LIBJPEG_SOURCES_DIR}/jcmaster.c + ${LIBJPEG_SOURCES_DIR}/jcomapi.c + ${LIBJPEG_SOURCES_DIR}/jcparam.c + ${LIBJPEG_SOURCES_DIR}/jcprepct.c + ${LIBJPEG_SOURCES_DIR}/jcsample.c + ${LIBJPEG_SOURCES_DIR}/jctrans.c + ${LIBJPEG_SOURCES_DIR}/jdapimin.c + ${LIBJPEG_SOURCES_DIR}/jdapistd.c + ${LIBJPEG_SOURCES_DIR}/jdarith.c + ${LIBJPEG_SOURCES_DIR}/jdatadst.c + ${LIBJPEG_SOURCES_DIR}/jdatasrc.c + ${LIBJPEG_SOURCES_DIR}/jdcoefct.c + ${LIBJPEG_SOURCES_DIR}/jdcolor.c + ${LIBJPEG_SOURCES_DIR}/jddctmgr.c + ${LIBJPEG_SOURCES_DIR}/jdhuff.c + ${LIBJPEG_SOURCES_DIR}/jdinput.c + ${LIBJPEG_SOURCES_DIR}/jcmainct.c + ${LIBJPEG_SOURCES_DIR}/jdmainct.c + ${LIBJPEG_SOURCES_DIR}/jdmarker.c + ${LIBJPEG_SOURCES_DIR}/jdmaster.c + ${LIBJPEG_SOURCES_DIR}/jdmerge.c + ${LIBJPEG_SOURCES_DIR}/jdpostct.c + ${LIBJPEG_SOURCES_DIR}/jdsample.c + ${LIBJPEG_SOURCES_DIR}/jdtrans.c + ${LIBJPEG_SOURCES_DIR}/jerror.c + ${LIBJPEG_SOURCES_DIR}/jfdctflt.c + ${LIBJPEG_SOURCES_DIR}/jfdctfst.c + ${LIBJPEG_SOURCES_DIR}/jfdctint.c + ${LIBJPEG_SOURCES_DIR}/jidctflt.c + ${LIBJPEG_SOURCES_DIR}/jidctfst.c + ${LIBJPEG_SOURCES_DIR}/jidctint.c + #${LIBJPEG_SOURCES_DIR}/jmemansi.c + #${LIBJPEG_SOURCES_DIR}/jmemdos.c + #${LIBJPEG_SOURCES_DIR}/jmemmac.c + ${LIBJPEG_SOURCES_DIR}/jmemmgr.c + #${LIBJPEG_SOURCES_DIR}/jmemname.c + ${LIBJPEG_SOURCES_DIR}/jmemnobs.c + ${LIBJPEG_SOURCES_DIR}/jquant1.c + ${LIBJPEG_SOURCES_DIR}/jquant2.c + ${LIBJPEG_SOURCES_DIR}/jutils.c + + # ${LIBJPEG_SOURCES_DIR}/rdbmp.c + # ${LIBJPEG_SOURCES_DIR}/rdcolmap.c + # ${LIBJPEG_SOURCES_DIR}/rdgif.c + # ${LIBJPEG_SOURCES_DIR}/rdppm.c + # ${LIBJPEG_SOURCES_DIR}/rdrle.c + # ${LIBJPEG_SOURCES_DIR}/rdswitch.c + # ${LIBJPEG_SOURCES_DIR}/rdtarga.c + # ${LIBJPEG_SOURCES_DIR}/transupp.c + # ${LIBJPEG_SOURCES_DIR}/wrbmp.c + # ${LIBJPEG_SOURCES_DIR}/wrgif.c + # ${LIBJPEG_SOURCES_DIR}/wrppm.c + # ${LIBJPEG_SOURCES_DIR}/wrrle.c + # ${LIBJPEG_SOURCES_DIR}/wrtarga.c + ) + + configure_file( + ${LIBJPEG_SOURCES_DIR}/jconfig.txt + ${LIBJPEG_SOURCES_DIR}/jconfig.h COPYONLY + ) + + source_group(ThirdParty\\libjpeg REGULAR_EXPRESSION ${LIBJPEG_SOURCES_DIR}/.*) + +else() + include(FindJPEG) + + if (NOT JPEG_FOUND) + message(FATAL_ERROR "Unable to find libjpeg") + endif() + + include_directories(${JPEG_INCLUDE_DIR}) + link_libraries(${JPEG_LIBRARIES}) +endif() diff --git a/OrthancFramework/Resources/CMake/LibP11Configuration.cmake b/OrthancFramework/Resources/CMake/LibP11Configuration.cmake new file mode 100644 index 0000000..54662fb --- /dev/null +++ b/OrthancFramework/Resources/CMake/LibP11Configuration.cmake @@ -0,0 +1,100 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_LIBP11) + if (NOT STATIC_BUILD AND USE_SYSTEM_OPENSSL) + message(FATAL_ERROR "If statically linking against libp11, one must also statically link against OpenSSL") + endif() + + SET(LIBP11_SOURCES_DIR ${CMAKE_BINARY_DIR}/libp11-0.4.0) + SET(LIBP11_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/libp11-0.4.0.tar.gz") + SET(LIBP11_MD5 "00b3e41db5be840d822bda12f3ab2ca7") + + if (IS_DIRECTORY "${LIBP11_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + + DownloadPackage(${LIBP11_MD5} ${LIBP11_URL} "${LIBP11_SOURCES_DIR}") + + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i ${CMAKE_CURRENT_LIST_DIR}/../Patches/libp11-0.4.0.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure AND FirstRun) + message(FATAL_ERROR "Error while patching libp11") + endif() + + # This command MUST be after applying the patch + file(COPY + ${LIBP11_SOURCES_DIR}/src/engine.h + ${LIBP11_SOURCES_DIR}/src/libp11.h + DESTINATION ${AUTOGENERATED_DIR}/libp11) + + file(WRITE ${AUTOGENERATED_DIR}/libp11/config.h "") + + set(LIBP11_SOURCES + #${LIBP11_SOURCES_DIR}/src/eng_front.c + ${LIBP11_SOURCES_DIR}/src/eng_back.c + ${LIBP11_SOURCES_DIR}/src/eng_parse.c + ${LIBP11_SOURCES_DIR}/src/libpkcs11.c + ${LIBP11_SOURCES_DIR}/src/p11_attr.c + ${LIBP11_SOURCES_DIR}/src/p11_cert.c + ${LIBP11_SOURCES_DIR}/src/p11_ec.c + ${LIBP11_SOURCES_DIR}/src/p11_err.c + ${LIBP11_SOURCES_DIR}/src/p11_front.c + ${LIBP11_SOURCES_DIR}/src/p11_key.c + ${LIBP11_SOURCES_DIR}/src/p11_load.c + ${LIBP11_SOURCES_DIR}/src/p11_misc.c + ${LIBP11_SOURCES_DIR}/src/p11_rsa.c + ${LIBP11_SOURCES_DIR}/src/p11_slot.c + ) + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + list(APPEND LIBP11_SOURCES + ${LIBP11_SOURCES_DIR}/src/atfork.c + ) + endif() + + source_group(ThirdParty\\libp11 REGULAR_EXPRESSION ${LIBP11_SOURCES_DIR}/.*) + +else() + check_include_file_cxx(libp11.h HAVE_LIBP11_H) + if (NOT HAVE_LIBP11_H) + message(FATAL_ERROR "Please install the libp11-dev package") + endif() + + check_library_exists(p11 PKCS11_login "" HAVE_LIBP11_LIB) + if (NOT HAVE_LIBP11_LIB) + message(FATAL_ERROR "Please install the libp11-dev package") + endif() + + link_libraries(p11) +endif() diff --git a/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake b/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake new file mode 100644 index 0000000..0963347 --- /dev/null +++ b/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake @@ -0,0 +1,82 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_LIBPNG) + SET(LIBPNG_SOURCES_DIR ${CMAKE_BINARY_DIR}/libpng-1.6.40) + SET(LIBPNG_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/libpng-1.6.40.tar.gz") + SET(LIBPNG_MD5 "ec4b597c3a9b1f8d2826575f530367b7") + + DownloadPackage(${LIBPNG_MD5} ${LIBPNG_URL} "${LIBPNG_SOURCES_DIR}") + + include_directories( + ${LIBPNG_SOURCES_DIR} + ) + + configure_file( + ${LIBPNG_SOURCES_DIR}/scripts/pnglibconf.h.prebuilt + ${LIBPNG_SOURCES_DIR}/pnglibconf.h + ) + + set(LIBPNG_SOURCES + #${LIBPNG_SOURCES_DIR}/example.c + ${LIBPNG_SOURCES_DIR}/png.c + ${LIBPNG_SOURCES_DIR}/pngerror.c + ${LIBPNG_SOURCES_DIR}/pngget.c + ${LIBPNG_SOURCES_DIR}/pngmem.c + ${LIBPNG_SOURCES_DIR}/pngpread.c + ${LIBPNG_SOURCES_DIR}/pngread.c + ${LIBPNG_SOURCES_DIR}/pngrio.c + ${LIBPNG_SOURCES_DIR}/pngrtran.c + ${LIBPNG_SOURCES_DIR}/pngrutil.c + ${LIBPNG_SOURCES_DIR}/pngset.c + #${LIBPNG_SOURCES_DIR}/pngtest.c + ${LIBPNG_SOURCES_DIR}/pngtrans.c + ${LIBPNG_SOURCES_DIR}/pngwio.c + ${LIBPNG_SOURCES_DIR}/pngwrite.c + ${LIBPNG_SOURCES_DIR}/pngwtran.c + ${LIBPNG_SOURCES_DIR}/pngwutil.c + ) + + add_definitions( + -DPNG_NO_CONFIG_H=1 + -DPNG_NO_CONSOLE_IO=1 + -DPNG_NO_STDIO=1 + # disable ARM neon optimization for Apple M1 builds (TODO: try adding arm/filter_neon_intrinscis.c ... ) + -DPNG_ARM_NEON_OPT=0 + # The following declaration avoids "__declspec(dllexport)" in + # libpng to prevent publicly exposing its symbols by the DLLs + -DPNG_IMPEXP= + ) + + source_group(ThirdParty\\libpng REGULAR_EXPRESSION ${LIBPNG_SOURCES_DIR}/.*) + +else() + include(FindPNG) + + if (NOT PNG_FOUND) + message(FATAL_ERROR "Unable to find libpng") + endif() + + include_directories(${PNG_INCLUDE_DIRS}) + link_libraries(${PNG_LIBRARIES}) + add_definitions(${PNG_DEFINITIONS}) +endif() diff --git a/OrthancFramework/Resources/CMake/LuaConfiguration.cmake b/OrthancFramework/Resources/CMake/LuaConfiguration.cmake new file mode 100644 index 0000000..8058ab3 --- /dev/null +++ b/OrthancFramework/Resources/CMake/LuaConfiguration.cmake @@ -0,0 +1,180 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_LUA) + SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.4.7) + SET(LUA_MD5 "fc3f3291353bbe6ee6dec85ee61331e8") + SET(LUA_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/lua-5.4.7.tar.gz") + + DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}") + + if (ENABLE_LUA_MODULES) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # Enable loading of shared libraries (for UNIX-like) + add_definitions(-DLUA_USE_DLOPEN=1) + + # Publish the functions of the Lua engine (that are built within + # the Orthanc binary) as global symbols, so that the external + # shared libraries can call them + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--export-dynamic") + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") + add_definitions(-DLUA_USE_LINUX=1) + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + add_definitions( + -DLUA_USE_LINUX=1 + -DLUA_USE_READLINE=1 + ) + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + add_definitions(-DLUA_USE_POSIX=1) + endif() + + elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + add_definitions( + -DLUA_DL_DLL=1 # Enable loading of shared libraries (for Microsoft Windows) + ) + + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + add_definitions( + -DLUA_USE_MACOSX=1 + -DLUA_DL_DYLD=1 # Enable loading of shared libraries (for Apple OS X) + ) + + else() + message(FATAL_ERROR "Support your platform here") + endif() + endif() + + add_definitions( + -DLUA_COMPAT_5_2=1 + ) + + include_directories( + ${LUA_SOURCES_DIR}/src + ) + + set(LUA_SOURCES + # Don't compile the Lua command-line + #${LUA_SOURCES_DIR}/src/lua.c + #${LUA_SOURCES_DIR}/src/luac.c + + # Core Lua + ${LUA_SOURCES_DIR}/src/lapi.c + ${LUA_SOURCES_DIR}/src/lcode.c + ${LUA_SOURCES_DIR}/src/lctype.c + ${LUA_SOURCES_DIR}/src/ldebug.c + ${LUA_SOURCES_DIR}/src/ldo.c + ${LUA_SOURCES_DIR}/src/ldump.c + ${LUA_SOURCES_DIR}/src/lfunc.c + ${LUA_SOURCES_DIR}/src/lgc.c + ${LUA_SOURCES_DIR}/src/llex.c + ${LUA_SOURCES_DIR}/src/lmem.c + ${LUA_SOURCES_DIR}/src/lobject.c + ${LUA_SOURCES_DIR}/src/lopcodes.c + ${LUA_SOURCES_DIR}/src/lparser.c + ${LUA_SOURCES_DIR}/src/lstate.c + ${LUA_SOURCES_DIR}/src/lstring.c + ${LUA_SOURCES_DIR}/src/ltable.c + ${LUA_SOURCES_DIR}/src/ltm.c + ${LUA_SOURCES_DIR}/src/lundump.c + ${LUA_SOURCES_DIR}/src/lvm.c + ${LUA_SOURCES_DIR}/src/lzio.c + + # Base Lua modules + ${LUA_SOURCES_DIR}/src/lauxlib.c + ${LUA_SOURCES_DIR}/src/lbaselib.c + ${LUA_SOURCES_DIR}/src/lcorolib.c + ${LUA_SOURCES_DIR}/src/ldblib.c + ${LUA_SOURCES_DIR}/src/liolib.c + ${LUA_SOURCES_DIR}/src/lmathlib.c + ${LUA_SOURCES_DIR}/src/loadlib.c + ${LUA_SOURCES_DIR}/src/loslib.c + ${LUA_SOURCES_DIR}/src/lstrlib.c + ${LUA_SOURCES_DIR}/src/ltablib.c + ${LUA_SOURCES_DIR}/src/lutf8lib.c + + ${LUA_SOURCES_DIR}/src/linit.c + ) + + source_group(ThirdParty\\Lua REGULAR_EXPRESSION ${LUA_SOURCES_DIR}/.*) + +elseif ((CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR + NOT "${ORTHANC_LUA_VERSION}" STREQUAL "") + + if ("${ORTHANC_LUA_VERSION}" STREQUAL "") + set(LUA_VERSIONS 5.3 5.2 5.1) + else() + # New in Orthanc 1.9.3 + set(LUA_VERSIONS ${ORTHANC_LUA_VERSION}) + endif() + + unset(LUA_VERSION) + foreach(version IN ITEMS ${LUA_VERSIONS}) + CHECK_INCLUDE_FILE(lua${version}/lua.h HAVE_LUA${version}_H) + if (HAVE_LUA${version}_H) + set(LUA_VERSION ${version}) + break() + endif() + endforeach() + + if (NOT LUA_VERSION) + message(FATAL_ERROR "Please install the liblua-dev package") + endif() + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + set(LUA_INCLUDE_DIR ${CROSSTOOL_NG_IMAGE}/usr/include/lua${LUA_VERSION}) + else() + # New in Orthanc 1.9.3 + find_path(LUA_INCLUDE_DIR + NAMES lua.h + PATHS + /usr/include/lua${LUA_VERSION} + /usr/local/include/lua${LUA_VERSION} + ) + endif() + + message("Lua include dir: ${LUA_INCLUDE_DIR}") + include_directories(${LUA_INCLUDE_DIR}) + + CHECK_LIBRARY_EXISTS(lua${LUA_VERSION} "lua_call" "${LUA_LIB_DIR}" HAVE_LUA_LIB_1) # Lua 5.1 + CHECK_LIBRARY_EXISTS(lua${LUA_VERSION} "lua_callk" "${LUA_LIB_DIR}" HAVE_LUA_LIB_2) # Lua 5.3 + if (NOT HAVE_LUA_LIB_1 AND NOT HAVE_LUA_LIB_2) + message(FATAL_ERROR "Please install the liblua package") + endif() + + link_libraries(lua${LUA_VERSION}) + +else() + include(FindLua) + + if (NOT LUA_FOUND) + message(FATAL_ERROR "Please install the liblua-dev package") + endif() + + include_directories(${LUA_INCLUDE_DIR}) + link_libraries(${LUA_LIBRARIES}) +endif() diff --git a/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake b/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake new file mode 100644 index 0000000..df09500 --- /dev/null +++ b/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake @@ -0,0 +1,117 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_MONGOOSE) + SET(MONGOOSE_SOURCES_DIR ${CMAKE_BINARY_DIR}/mongoose) + + if (IS_DIRECTORY "${MONGOOSE_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + + if (0) + # Use Mongoose 3.1 + DownloadPackage( + "e718fc287b4eb1bd523be3fa00942bb0" + "https://orthanc.uclouvain.be/downloads/third-party-downloads/mongoose-3.1.tgz" + "${MONGOOSE_SOURCES_DIR}") + + add_definitions(-DMONGOOSE_USE_CALLBACKS=0) + set(MONGOOSE_PATCH ${CMAKE_CURRENT_LIST_DIR}/../Patches/mongoose-3.1-patch.diff) + + else() + # Use Mongoose 3.8 + DownloadPackage( + "7e3296295072792cdc3c633f9404e0c3" + "https://orthanc.uclouvain.be/downloads/third-party-downloads/mongoose-3.8.tgz" + "${MONGOOSE_SOURCES_DIR}") + + add_definitions(-DMONGOOSE_USE_CALLBACKS=1) + set(MONGOOSE_PATCH ${CMAKE_CURRENT_LIST_DIR}/../Patches/mongoose-3.8-patch.diff) + endif() + + # Patch mongoose + execute_process( + COMMAND ${PATCH_EXECUTABLE} -N mongoose.c + INPUT_FILE ${MONGOOSE_PATCH} + WORKING_DIRECTORY ${MONGOOSE_SOURCES_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure AND FirstRun) + message(FATAL_ERROR "Error while patching a file") + endif() + + include_directories( + ${MONGOOSE_SOURCES_DIR} + ) + + set(MONGOOSE_SOURCES + ${MONGOOSE_SOURCES_DIR}/mongoose.c + ) + + + if (ENABLE_SSL) + add_definitions( + -DNO_SSL_DL=1 + ) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") + link_libraries(dl) + endif() + + else() + add_definitions( + -DNO_SSL=1 # Remove SSL support from mongoose + ) + endif() + + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + if (CMAKE_COMPILER_IS_GNUCXX) + # This is a patch for MinGW64 + add_definitions(-D_TIMESPEC_DEFINED=1) + endif() + endif() + + source_group(ThirdParty\\Mongoose REGULAR_EXPRESSION ${MONGOOSE_SOURCES_DIR}/.*) + +else() + CHECK_INCLUDE_FILE_CXX(mongoose.h HAVE_MONGOOSE_H) + if (NOT HAVE_MONGOOSE_H) + message(FATAL_ERROR "Please install the mongoose-devel package") + endif() + + CHECK_LIBRARY_EXISTS(mongoose mg_start "" HAVE_MONGOOSE_LIB) + if (NOT HAVE_MONGOOSE_LIB) + message(FATAL_ERROR "Please install the mongoose-devel package") + endif() + + if (SYSTEM_MONGOOSE_USE_CALLBACKS) + add_definitions(-DMONGOOSE_USE_CALLBACKS=1) + else() + add_definitions(-DMONGOOSE_USE_CALLBACKS=0) + endif() + + link_libraries(mongoose) +endif() diff --git a/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake b/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake new file mode 100644 index 0000000..239f46b --- /dev/null +++ b/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake @@ -0,0 +1,69 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL) + if (OPENSSL_STATIC_VERSION STREQUAL "1.1.1") + # Still used by orthanc-gcp (Google Cloud Platform) as of its release 1.0 + include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfigurationStatic-1.1.1.cmake) + elseif (OPENSSL_STATIC_VERSION STREQUAL "3.0") + include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfigurationStatic-3.0.cmake) + else() + message(FATAL_ERROR "Unsupported version of OpenSSL: ${OPENSSL_STATIC_VERSION}") + endif() + + source_group(ThirdParty\\OpenSSL REGULAR_EXPRESSION ${OPENSSL_SOURCES_DIR}/.*) + +elseif (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + + CHECK_INCLUDE_FILE_CXX(openssl/opensslv.h HAVE_OPENSSL_H) + if (NOT HAVE_OPENSSL_H) + message(FATAL_ERROR "Please install the libopenssl-dev package") + endif() + + CHECK_LIBRARY_EXISTS(crypto "OPENSSL_init" "" HAVE_OPENSSL_CRYPTO_LIB) + if (NOT HAVE_OPENSSL_CRYPTO_LIB) + message(FATAL_ERROR "Please install the libopenssl package") + endif() + + # The "SSL_library_init" is for OpenSSL <= 1.0.2, whereas + # "OPENSSL_init_ssl" is for OpenSSL >= 1.1.0 + CHECK_LIBRARY_EXISTS(ssl "SSL_library_init" "" HAVE_OPENSSL_SSL_LIB) + if (NOT HAVE_OPENSSL_SSL_LIB) + CHECK_LIBRARY_EXISTS(ssl "OPENSSL_init_ssl" "" HAVE_OPENSSL_SSL_LIB_2) + if (NOT HAVE_OPENSSL_SSL_LIB_2) + message(FATAL_ERROR "Please install the libopenssl package") + endif() + endif() + + link_libraries(crypto ssl) + +else() + include(FindOpenSSL) + + if (NOT OPENSSL_FOUND) + message(FATAL_ERROR "Unable to find OpenSSL") + endif() + + include_directories(${OPENSSL_INCLUDE_DIR}) + link_libraries(${OPENSSL_LIBRARIES}) +endif() diff --git a/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake new file mode 100644 index 0000000..f1c1968 --- /dev/null +++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake @@ -0,0 +1,310 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.1.1k) +SET(OPENSSL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/openssl-1.1.1k.tar.gz") +SET(OPENSSL_MD5 "c4e7d95f782b08116afa27b30393dd27") + +if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${OPENSSL_MD5} ${OPENSSL_URL} "${OPENSSL_SOURCES_DIR}") + +if (FirstRun) + file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/buildinf.h " +#define DATE \"\" +#define PLATFORM \"\" +#define compiler_flags \"\" +") + file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/bn_conf.h "") + file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/dso_conf.h "") + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Patches/openssl-1.1.1-conf.h.in + ${OPENSSL_SOURCES_DIR}/include/openssl/opensslconf.h + ) + + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/openssl-1.1.1k.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + file(RENAME + ${OPENSSL_SOURCES_DIR}/include/openssl/e_os2.h + ${OPENSSL_SOURCES_DIR}/include/openssl/e_os2_source.h) + + # The following patch of "e_os2.h" prevents from building OpenSSL + # as a DLL under Windows. Otherwise, symbols have inconsistent + # linkage if ${OPENSSL_SOURCES} is used to create a DLL (notably + # if building an Orthanc plugin such as PostgreSQL or MySQL). + file(WRITE ${OPENSSL_SOURCES_DIR}/include/openssl/e_os2.h " +#include \"e_os2_source.h\" +#if defined(_WIN32) +# undef OPENSSL_EXPORT +# undef OPENSSL_IMPORT +# undef OPENSSL_EXTERN +# undef OPENSSL_GLOBAL +# define OPENSSL_EXPORT +# define OPENSSL_IMPORT +# define OPENSSL_EXTERN extern +# define OPENSSL_GLOBAL +#endif +") + +else() + message("The patches for OpenSSL have already been applied") +endif() + +add_definitions( + -DOPENSSL_THREADS + -DOPENSSL_IA32_SSE2 + -DOPENSSL_NO_ASM + -DOPENSSL_NO_DYNAMIC_ENGINE + -DOPENSSL_NO_DEVCRYPTOENG + + -DOPENSSL_NO_BF + -DOPENSSL_NO_CAMELLIA + -DOPENSSL_NO_CAST + -DOPENSSL_NO_EC_NISTP_64_GCC_128 + -DOPENSSL_NO_GMP + -DOPENSSL_NO_GOST + -DOPENSSL_NO_HW + -DOPENSSL_NO_JPAKE + -DOPENSSL_NO_IDEA + -DOPENSSL_NO_KRB5 + -DOPENSSL_NO_MD2 + -DOPENSSL_NO_MDC2 + #-DOPENSSL_NO_MD4 # MD4 is necessary for MariaDB/MySQL client + -DOPENSSL_NO_RC2 + -DOPENSSL_NO_RC4 + -DOPENSSL_NO_RC5 + -DOPENSSL_NO_RFC3779 + -DOPENSSL_NO_SCTP + -DOPENSSL_NO_STORE + -DOPENSSL_NO_SEED + -DOPENSSL_NO_WHIRLPOOL + -DOPENSSL_NO_RIPEMD + -DOPENSSL_NO_AFALGENG + + -DOPENSSLDIR="/usr/local/ssl" + ) + + +include_directories( + ${OPENSSL_SOURCES_DIR} + ${OPENSSL_SOURCES_DIR}/crypto + ${OPENSSL_SOURCES_DIR}/crypto/asn1 + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448 + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32 + ${OPENSSL_SOURCES_DIR}/crypto/evp + ${OPENSSL_SOURCES_DIR}/crypto/include + ${OPENSSL_SOURCES_DIR}/crypto/modes + ${OPENSSL_SOURCES_DIR}/include + ) + + +set(OPENSSL_SOURCES_SUBDIRS + ${OPENSSL_SOURCES_DIR}/crypto + ${OPENSSL_SOURCES_DIR}/crypto/aes + ${OPENSSL_SOURCES_DIR}/crypto/aria + ${OPENSSL_SOURCES_DIR}/crypto/asn1 + ${OPENSSL_SOURCES_DIR}/crypto/async + ${OPENSSL_SOURCES_DIR}/crypto/async/arch + ${OPENSSL_SOURCES_DIR}/crypto/bio + ${OPENSSL_SOURCES_DIR}/crypto/blake2 + ${OPENSSL_SOURCES_DIR}/crypto/bn + ${OPENSSL_SOURCES_DIR}/crypto/buffer + ${OPENSSL_SOURCES_DIR}/crypto/chacha + ${OPENSSL_SOURCES_DIR}/crypto/cmac + ${OPENSSL_SOURCES_DIR}/crypto/cms + ${OPENSSL_SOURCES_DIR}/crypto/comp + ${OPENSSL_SOURCES_DIR}/crypto/conf + ${OPENSSL_SOURCES_DIR}/crypto/ct + ${OPENSSL_SOURCES_DIR}/crypto/des + ${OPENSSL_SOURCES_DIR}/crypto/dh + ${OPENSSL_SOURCES_DIR}/crypto/dsa + ${OPENSSL_SOURCES_DIR}/crypto/dso + ${OPENSSL_SOURCES_DIR}/crypto/ec + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448 + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32 + ${OPENSSL_SOURCES_DIR}/crypto/err + ${OPENSSL_SOURCES_DIR}/crypto/evp + ${OPENSSL_SOURCES_DIR}/crypto/hmac + ${OPENSSL_SOURCES_DIR}/crypto/kdf + ${OPENSSL_SOURCES_DIR}/crypto/lhash + ${OPENSSL_SOURCES_DIR}/crypto/md4 + ${OPENSSL_SOURCES_DIR}/crypto/md5 + ${OPENSSL_SOURCES_DIR}/crypto/modes + ${OPENSSL_SOURCES_DIR}/crypto/objects + ${OPENSSL_SOURCES_DIR}/crypto/ocsp + ${OPENSSL_SOURCES_DIR}/crypto/pem + ${OPENSSL_SOURCES_DIR}/crypto/pkcs12 + ${OPENSSL_SOURCES_DIR}/crypto/pkcs7 + ${OPENSSL_SOURCES_DIR}/crypto/poly1305 + ${OPENSSL_SOURCES_DIR}/crypto/pqueue + ${OPENSSL_SOURCES_DIR}/crypto/rand + ${OPENSSL_SOURCES_DIR}/crypto/ripemd + ${OPENSSL_SOURCES_DIR}/crypto/rsa + ${OPENSSL_SOURCES_DIR}/crypto/sha + ${OPENSSL_SOURCES_DIR}/crypto/siphash + ${OPENSSL_SOURCES_DIR}/crypto/sm2 + ${OPENSSL_SOURCES_DIR}/crypto/sm3 + ${OPENSSL_SOURCES_DIR}/crypto/sm4 + ${OPENSSL_SOURCES_DIR}/crypto/srp + ${OPENSSL_SOURCES_DIR}/crypto/stack + ${OPENSSL_SOURCES_DIR}/crypto/store + ${OPENSSL_SOURCES_DIR}/crypto/ts + ${OPENSSL_SOURCES_DIR}/crypto/txt_db + ${OPENSSL_SOURCES_DIR}/crypto/ui + ${OPENSSL_SOURCES_DIR}/crypto/x509 + ${OPENSSL_SOURCES_DIR}/crypto/x509v3 + ${OPENSSL_SOURCES_DIR}/ssl + ${OPENSSL_SOURCES_DIR}/ssl/record + ${OPENSSL_SOURCES_DIR}/ssl/statem + ) + +if (ENABLE_OPENSSL_ENGINES) + add_definitions( + #-DENGINESDIR="/usr/local/lib/engines-1.1" # On GNU/Linux + -DENGINESDIR="." + ) + + list(APPEND OPENSSL_SOURCES_SUBDIRS + ${OPENSSL_SOURCES_DIR}/engines + ${OPENSSL_SOURCES_DIR}/crypto/engine + ) +else() + add_definitions(-DOPENSSL_NO_ENGINE) +endif() + +list(APPEND OPENSSL_SOURCES_SUBDIRS + # EC, ECDH and ECDSA are necessary for PKCS11, and for contacting + # HTTPS servers that use TLS certificate encrypted with ECDSA + # (check the output of a recent version of the "sslscan" + # command). Until Orthanc <= 1.4.1, these features were only + # enabled if ENABLE_PKCS11 support was set to "ON". + # https://groups.google.com/d/msg/orthanc-users/2l-bhYIMEWg/oMmK33bYBgAJ + ${OPENSSL_SOURCES_DIR}/crypto/ec + ${OPENSSL_SOURCES_DIR}/crypto/ecdh + ${OPENSSL_SOURCES_DIR}/crypto/ecdsa + ) + +foreach(d ${OPENSSL_SOURCES_SUBDIRS}) + AUX_SOURCE_DIRECTORY(${d} OPENSSL_SOURCES) +endforeach() + +list(REMOVE_ITEM OPENSSL_SOURCES + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_nyi.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_unix.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_vms.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win32.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_wince.c + ${OPENSSL_SOURCES_DIR}/crypto/aes/aes_x86core.c + ${OPENSSL_SOURCES_DIR}/crypto/armcap.c + ${OPENSSL_SOURCES_DIR}/crypto/bio/bss_dgram.c + ${OPENSSL_SOURCES_DIR}/crypto/des/ncbc_enc.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256_table.c + ${OPENSSL_SOURCES_DIR}/crypto/engine/eng_devcrypto.c + ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_base2_44.c # Cannot be compiled with MinGW + ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_ieee754.c # Cannot be compiled with MinGW + ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c + ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c + ${OPENSSL_SOURCES_DIR}/crypto/sparcv9cap.c + ${OPENSSL_SOURCES_DIR}/engines/e_afalg.c # Cannot be compiled with MinGW + ) + +# Check out "${OPENSSL_SOURCES_DIR}/Configurations/README": "This is +# default if no option is specified, it works on any supported +# system." It is mandatory to define it as a macro, as it is used by +# all the source files that include OpenSSL (e.g. "Core/Toolbox.cpp" +# or curl) +add_definitions(-DTHIRTY_TWO_BIT) + + +if (NOT CMAKE_COMPILER_IS_GNUCXX OR + "${CMAKE_SYSTEM_NAME}" STREQUAL "Windows" OR + "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Disable the use of a gcc extension, that is neither available on + # MinGW, nor on LSB + add_definitions( + -DOPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE + ) +endif() + + +if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows") + set(OPENSSL_DEFINITIONS + "${OPENSSL_DEFINITIONS};OPENSSL_SYSNAME_WIN32;SO_WIN32;WIN32_LEAN_AND_MEAN;L_ENDIAN;NO_WINDOWS_BRAINDEATH") + + if (ENABLE_OPENSSL_ENGINES) + link_libraries(crypt32) + endif() + + add_definitions( + -DOPENSSL_RAND_SEED_OS # ${OPENSSL_SOURCES_DIR}/crypto/rand/rand_win.c + ) + +elseif ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + add_definitions( + # In order for "crypto/mem_sec.c" to compile on LSB + -DOPENSSL_NO_SECURE_MEMORY + + # The "OPENSSL_RAND_SEED_OS" value implies a syscall() to + # "__NR_getrandom" (i.e. system call "getentropy(2)") in + # "rand_unix.c", which is not available in LSB. + -DOPENSSL_RAND_SEED_DEVRANDOM + + # If "OPENSSL_NO_ERR" is not defined, the PostgreSQL plugin + # crashes with segmentation fault in function + # "build_SYS_str_reasons()", that is called from + # "OPENSSL_init_ssl()" + # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=193 + -DOPENSSL_NO_ERR + ) + +else() + # Fixes error "OpenSSL error: error:2406C06E:random number + # generator:RAND_DRBG_instantiate:error retrieving entropy" that was + # present in Orthanc 1.6.0, if statically linking on Ubuntu 18.04 + add_definitions( + -DOPENSSL_RAND_SEED_OS + ) +endif() + + +set_source_files_properties( + ${OPENSSL_SOURCES} + PROPERTIES COMPILE_DEFINITIONS + "${OPENSSL_DEFINITIONS};DSO_NONE" + ) diff --git a/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-3.0.cmake b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-3.0.cmake new file mode 100644 index 0000000..451cc73 --- /dev/null +++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-3.0.cmake @@ -0,0 +1,415 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +set(OPENSSL_VERSION_MAJOR 3) +set(OPENSSL_VERSION_MINOR 1) +set(OPENSSL_VERSION_PATCH 4) +set(OPENSSL_VERSION_PRE_RELEASE "") +set(OPENSSL_VERSION_FULL "${OPENSSL_VERSION_MAJOR}.${OPENSSL_VERSION_MINOR}.${OPENSSL_VERSION_PATCH}${OPENSSL_VERSION_PRE_RELEASE}") +SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-${OPENSSL_VERSION_FULL}) +SET(OPENSSL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/openssl-${OPENSSL_VERSION_FULL}.tar.gz") +SET(OPENSSL_MD5 "653ad58812c751b887e8ec37e02bba70") + +if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${OPENSSL_MD5} ${OPENSSL_URL} "${OPENSSL_SOURCES_DIR}") + + +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/openssl-${OPENSSL_VERSION_FULL}.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + execute_process( + COMMAND ${PYTHON_EXECUTABLE} + ${CMAKE_CURRENT_LIST_DIR}/../Patches/OpenSSL-ConfigureHeaders.py + "${OPENSSL_SOURCES_DIR}" + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while configuring the OpenSSL headers") + endif() + + file(WRITE ${OPENSSL_SOURCES_DIR}/include/openssl/opensslv.h "") + file(WRITE ${OPENSSL_SOURCES_DIR}/include/crypto/bn_conf.h "") + file(WRITE ${OPENSSL_SOURCES_DIR}/include/crypto/dso_conf.h "") + + file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/buildinf.h " +#define DATE \"\" +#define PLATFORM \"\" +#define compiler_flags \"\" +") + +else() + message("The patches for OpenSSL have already been applied") +endif() + + +if (OPENSSL_VERSION_PRE_RELEASE STREQUAL "") + set(VERSION_VERSION_OFFSET 0) +else() + set(VERSION_VERSION_OFFSET 15) +endif() + +math(EXPR OPENSSL_CONFIGURED_API "${OPENSSL_VERSION_MAJOR} * 10000 + ${OPENSSL_VERSION_MINOR} * 100") + +# This macro is normally defined in "opensslv.h.in" +math(EXPR OPENSSL_VERSION_NUMBER "(${OPENSSL_VERSION_MAJOR} << 28) + (${OPENSSL_VERSION_MINOR} << 20) + (${OPENSSL_VERSION_PATCH} << 4) + ${VERSION_VERSION_OFFSET}") + +list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 OPENSSL_DSO_EXTENSION) + +add_definitions( + -DOPENSSL_VERSION_MAJOR=${OPENSSL_VERSION_MAJOR} + -DOPENSSL_VERSION_MINOR=${OPENSSL_VERSION_MINOR} + -DOPENSSL_VERSION_PATCH=${OPENSSL_VERSION_PATCH} + -DOPENSSL_CONFIGURED_API=${OPENSSL_CONFIGURED_API} + -DOPENSSL_VERSION_NUMBER=${OPENSSL_VERSION_NUMBER} + -DOPENSSL_VERSION_PRE_RELEASE="${OPENSSL_VERSION_PRE_RELEASE}" + -DOPENSSL_VERSION_BUILD_METADATA="" + -DOPENSSL_VERSION_TEXT="OpenSSL ${OPENSSL_VERSION_FULL}" + -DOPENSSL_VERSION_STR="${OPENSSL_VERSION_MAJOR}.${OPENSSL_VERSION_MINOR}.${OPENSSL_VERSION_PATCH}" + -DOPENSSL_FULL_VERSION_STR="${OPENSSL_VERSION_FULL}" + -DDSO_EXTENSION="${OPENSSL_DSO_EXTENSION}" + + -DOPENSSLDIR="/usr/local/ssl" + -DMODULESDIR="" # TODO + + -DOPENSSL_BUILDING_OPENSSL + -DOPENSSL_THREADS + -DOPENSSL_IA32_SSE2 + + -DOPENSSL_NO_AFALGENG + -DOPENSSL_NO_ASM + -DOPENSSL_NO_CHACHA # Necessary for VC2015-64 since openssl-3.0.1 + -DOPENSSL_NO_DEVCRYPTOENG + -DOPENSSL_NO_DYNAMIC_ENGINE + -DOPENSSL_NO_EC_NISTP_64_GCC_128 + -DOPENSSL_NO_GOST + -DOPENSSL_NO_RFC3779 + -DOPENSSL_NO_SCTP + + -DOPENSSL_NO_KTLS # TODO ? + ) + + +include_directories( + BEFORE + ${OPENSSL_SOURCES_DIR} + ${OPENSSL_SOURCES_DIR}/crypto/asn1 + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448 + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32 + ${OPENSSL_SOURCES_DIR}/crypto/evp + ${OPENSSL_SOURCES_DIR}/crypto/include + ${OPENSSL_SOURCES_DIR}/crypto/modes + ${OPENSSL_SOURCES_DIR}/include + ${OPENSSL_SOURCES_DIR}/providers/common/include + ${OPENSSL_SOURCES_DIR}/providers/implementations/include + ) + + +set(OPENSSL_SOURCES_SUBDIRS + ## Assembly is disabled + # ${OPENSSL_SOURCES_DIR}/crypto/aes/asm + # ${OPENSSL_SOURCES_DIR}/crypto/bf/asm + # ${OPENSSL_SOURCES_DIR}/crypto/bn/asm + # ${OPENSSL_SOURCES_DIR}/crypto/camellia/asm + # ${OPENSSL_SOURCES_DIR}/crypto/cast/asm + # ${OPENSSL_SOURCES_DIR}/crypto/chacha/asm + # ${OPENSSL_SOURCES_DIR}/crypto/des/asm + # ${OPENSSL_SOURCES_DIR}/crypto/ec/asm + # ${OPENSSL_SOURCES_DIR}/crypto/md5/asm + # ${OPENSSL_SOURCES_DIR}/crypto/modes/asm + # ${OPENSSL_SOURCES_DIR}/crypto/poly1305/asm + # ${OPENSSL_SOURCES_DIR}/crypto/rc4/asm + # ${OPENSSL_SOURCES_DIR}/crypto/rc5/asm + # ${OPENSSL_SOURCES_DIR}/crypto/ripemd/asm + # ${OPENSSL_SOURCES_DIR}/crypto/sha/asm + # ${OPENSSL_SOURCES_DIR}/crypto/whrlpool/asm + + ${OPENSSL_SOURCES_DIR}/crypto + ${OPENSSL_SOURCES_DIR}/crypto/aes + ${OPENSSL_SOURCES_DIR}/crypto/aria + ${OPENSSL_SOURCES_DIR}/crypto/asn1 + ${OPENSSL_SOURCES_DIR}/crypto/async + ${OPENSSL_SOURCES_DIR}/crypto/async/arch + ${OPENSSL_SOURCES_DIR}/crypto/bf + ${OPENSSL_SOURCES_DIR}/crypto/bio + ${OPENSSL_SOURCES_DIR}/crypto/bn + ${OPENSSL_SOURCES_DIR}/crypto/buffer + ${OPENSSL_SOURCES_DIR}/crypto/camellia + ${OPENSSL_SOURCES_DIR}/crypto/cast + ${OPENSSL_SOURCES_DIR}/crypto/chacha + ${OPENSSL_SOURCES_DIR}/crypto/cmac + ${OPENSSL_SOURCES_DIR}/crypto/cmp + ${OPENSSL_SOURCES_DIR}/crypto/cms + ${OPENSSL_SOURCES_DIR}/crypto/comp + ${OPENSSL_SOURCES_DIR}/crypto/conf + ${OPENSSL_SOURCES_DIR}/crypto/crmf + ${OPENSSL_SOURCES_DIR}/crypto/ct + ${OPENSSL_SOURCES_DIR}/crypto/des + ${OPENSSL_SOURCES_DIR}/crypto/dh + ${OPENSSL_SOURCES_DIR}/crypto/dsa + ${OPENSSL_SOURCES_DIR}/crypto/dso + ${OPENSSL_SOURCES_DIR}/crypto/ec + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448 + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32 + ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_64 + ${OPENSSL_SOURCES_DIR}/crypto/encode_decode + ${OPENSSL_SOURCES_DIR}/crypto/engine + ${OPENSSL_SOURCES_DIR}/crypto/err + ${OPENSSL_SOURCES_DIR}/crypto/ess + ${OPENSSL_SOURCES_DIR}/crypto/evp + ${OPENSSL_SOURCES_DIR}/crypto/ffc + ${OPENSSL_SOURCES_DIR}/crypto/hmac + ${OPENSSL_SOURCES_DIR}/crypto/http + ${OPENSSL_SOURCES_DIR}/crypto/idea + ${OPENSSL_SOURCES_DIR}/crypto/kdf + ${OPENSSL_SOURCES_DIR}/crypto/lhash + ${OPENSSL_SOURCES_DIR}/crypto/md2 + ${OPENSSL_SOURCES_DIR}/crypto/md4 + ${OPENSSL_SOURCES_DIR}/crypto/md5 + ${OPENSSL_SOURCES_DIR}/crypto/mdc2 + ${OPENSSL_SOURCES_DIR}/crypto/modes + ${OPENSSL_SOURCES_DIR}/crypto/objects + ${OPENSSL_SOURCES_DIR}/crypto/ocsp + ${OPENSSL_SOURCES_DIR}/crypto/pem + ${OPENSSL_SOURCES_DIR}/crypto/perlasm + ${OPENSSL_SOURCES_DIR}/crypto/pkcs12 + ${OPENSSL_SOURCES_DIR}/crypto/pkcs7 + ${OPENSSL_SOURCES_DIR}/crypto/poly1305 + ${OPENSSL_SOURCES_DIR}/crypto/property + ${OPENSSL_SOURCES_DIR}/crypto/rand + ${OPENSSL_SOURCES_DIR}/crypto/rc2 + ${OPENSSL_SOURCES_DIR}/crypto/rc4 + ${OPENSSL_SOURCES_DIR}/crypto/rc5 + ${OPENSSL_SOURCES_DIR}/crypto/ripemd + ${OPENSSL_SOURCES_DIR}/crypto/rsa + ${OPENSSL_SOURCES_DIR}/crypto/seed + ${OPENSSL_SOURCES_DIR}/crypto/sha + ${OPENSSL_SOURCES_DIR}/crypto/siphash + ${OPENSSL_SOURCES_DIR}/crypto/sm2 + ${OPENSSL_SOURCES_DIR}/crypto/sm3 + ${OPENSSL_SOURCES_DIR}/crypto/sm4 + ${OPENSSL_SOURCES_DIR}/crypto/srp + ${OPENSSL_SOURCES_DIR}/crypto/stack + ${OPENSSL_SOURCES_DIR}/crypto/store + ${OPENSSL_SOURCES_DIR}/crypto/ts + ${OPENSSL_SOURCES_DIR}/crypto/txt_db + ${OPENSSL_SOURCES_DIR}/crypto/ui + ${OPENSSL_SOURCES_DIR}/crypto/whrlpool + ${OPENSSL_SOURCES_DIR}/crypto/x509 + + # ${OPENSSL_SOURCES_DIR}/providers/implementations/rands/seeding # OS-specific + ${OPENSSL_SOURCES_DIR}/providers + ${OPENSSL_SOURCES_DIR}/providers/common + ${OPENSSL_SOURCES_DIR}/providers/common/der + ${OPENSSL_SOURCES_DIR}/providers/implementations/asymciphers + ${OPENSSL_SOURCES_DIR}/providers/implementations/ciphers + ${OPENSSL_SOURCES_DIR}/providers/implementations/digests + ${OPENSSL_SOURCES_DIR}/providers/implementations/encode_decode + ${OPENSSL_SOURCES_DIR}/providers/implementations/exchange + ${OPENSSL_SOURCES_DIR}/providers/implementations/kdfs + ${OPENSSL_SOURCES_DIR}/providers/implementations/kem + ${OPENSSL_SOURCES_DIR}/providers/implementations/keymgmt + ${OPENSSL_SOURCES_DIR}/providers/implementations/macs + ${OPENSSL_SOURCES_DIR}/providers/implementations/rands + ${OPENSSL_SOURCES_DIR}/providers/implementations/signature + ${OPENSSL_SOURCES_DIR}/providers/implementations/storemgmt + + ${OPENSSL_SOURCES_DIR}/ssl + ${OPENSSL_SOURCES_DIR}/ssl/record + ${OPENSSL_SOURCES_DIR}/ssl/statem + ) + +if (ENABLE_OPENSSL_ENGINES) + add_definitions( + #-DENGINESDIR="/usr/local/lib/engines-1.1" # On GNU/Linux + -DENGINESDIR="." + ) + + list(APPEND OPENSSL_SOURCES_SUBDIRS + ${OPENSSL_SOURCES_DIR}/engines + ${OPENSSL_SOURCES_DIR}/crypto/engine + ) +else() + add_definitions(-DOPENSSL_NO_ENGINE) +endif() + +list(APPEND OPENSSL_SOURCES_SUBDIRS + # EC, ECDH and ECDSA are necessary for PKCS11, and for contacting + # HTTPS servers that use TLS certificate encrypted with ECDSA + # (check the output of a recent version of the "sslscan" + # command). Until Orthanc <= 1.4.1, these features were only + # enabled if ENABLE_PKCS11 support was set to "ON". + # https://groups.google.com/d/msg/orthanc-users/2l-bhYIMEWg/oMmK33bYBgAJ + ${OPENSSL_SOURCES_DIR}/crypto/ec + ${OPENSSL_SOURCES_DIR}/crypto/ecdh + ${OPENSSL_SOURCES_DIR}/crypto/ecdsa + ) + +foreach(d ${OPENSSL_SOURCES_SUBDIRS}) + AUX_SOURCE_DIRECTORY(${d} OPENSSL_SOURCES) +endforeach() + + +list(REMOVE_ITEM OPENSSL_SOURCES + # Files below are not part of the "libcrypto.a" and "libssl.a" that + # are created by compiling OpenSSL from sources + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_nyi.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_unix.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_vms.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win32.c + ${OPENSSL_SOURCES_DIR}/crypto/LPdir_wince.c + ${OPENSSL_SOURCES_DIR}/crypto/aes/aes_x86core.c + ${OPENSSL_SOURCES_DIR}/crypto/des/ncbc_enc.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistp224.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistp256.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistp521.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256_table.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_s390x_nistp.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecx_s390x.c + ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_base2_44.c + ${OPENSSL_SOURCES_DIR}/crypto/rsa/rsa_acvp_test_params.c + ${OPENSSL_SOURCES_DIR}/engines/e_devcrypto.c + ${OPENSSL_SOURCES_DIR}/engines/e_loader_attic.c + ${OPENSSL_SOURCES_DIR}/providers/common/securitycheck_fips.c + ${OPENSSL_SOURCES_DIR}/providers/implementations/macs/blake2_mac_impl.c + + ${OPENSSL_SOURCES_DIR}/engines/e_afalg.c # Fails on OS X and Visual Studio + ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_ieee754.c # Fails on Visual Studio + + ${OPENSSL_SOURCES_DIR}/ssl/ktls.c # TODO ? + + # Disable PowerPC sources + ${OPENSSL_SOURCES_DIR}/crypto/bn/bn_ppc.c + ${OPENSSL_SOURCES_DIR}/crypto/chacha/chacha_ppc.c + ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_ppc.c + ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_ppc.c + ${OPENSSL_SOURCES_DIR}/crypto/sha/sha_ppc.c + + # Disable SPARC sources + ${OPENSSL_SOURCES_DIR}/crypto/bn/bn_sparc.c + + # Disable CPUID for non-x86 platforms + ${OPENSSL_SOURCES_DIR}/crypto/armcap.c + ${OPENSSL_SOURCES_DIR}/crypto/loongarchcap.c + ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c + ${OPENSSL_SOURCES_DIR}/crypto/riscvcap.c + ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c + ${OPENSSL_SOURCES_DIR}/crypto/sparcv9cap.c + ) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" OR + APPLE) + list(APPEND OPENSSL_SOURCES + ${OPENSSL_SOURCES_DIR}/providers/implementations/rands/seeding/rand_unix.c + ) +elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows") + list(APPEND OPENSSL_SOURCES + ${OPENSSL_SOURCES_DIR}/providers/implementations/rands/seeding/rand_win.c + ) +endif() + + +# Check out "${OPENSSL_SOURCES_DIR}/Configurations/README.md": "This +# is default if no option is specified, it works on any supported +# system." It is mandatory to define it as a macro, as it is used by +# all the source files that include OpenSSL (e.g. "Core/Toolbox.cpp" +# or curl) +add_definitions(-DTHIRTY_TWO_BIT) + + +if (NOT CMAKE_COMPILER_IS_GNUCXX OR + "${CMAKE_SYSTEM_NAME}" STREQUAL "Windows" OR + "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Disable the use of a gcc extension, that is neither available on + # MinGW, nor on LSB + add_definitions( + -DOPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE + ) +endif() + + +if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows") + set(OPENSSL_DEFINITIONS + "${OPENSSL_DEFINITIONS};OPENSSL_SYSNAME_WIN32;SO_WIN32;WIN32_LEAN_AND_MEAN;L_ENDIAN;NO_WINDOWS_BRAINDEATH") + + if (ENABLE_OPENSSL_ENGINES) + link_libraries(crypt32) + endif() + + add_definitions( + -DOPENSSL_RAND_SEED_OS # ${OPENSSL_SOURCES_DIR}/crypto/rand/rand_win.c + ) + +elseif ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + add_definitions( + # In order for "crypto/mem_sec.c" to compile on LSB + -DOPENSSL_NO_SECURE_MEMORY + + # The "OPENSSL_RAND_SEED_OS" value implies a syscall() to + # "__NR_getrandom" (i.e. system call "getentropy(2)") in + # "rand_unix.c", which is not available in LSB. + -DOPENSSL_RAND_SEED_DEVRANDOM + + # If "OPENSSL_NO_ERR" is not defined, the PostgreSQL plugin + # crashes with segmentation fault in function + # "build_SYS_str_reasons()", that is called from + # "OPENSSL_init_ssl()" + # https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=193 + -DOPENSSL_NO_ERR + ) + +else() + # Fixes error "OpenSSL error: error:2406C06E:random number + # generator:RAND_DRBG_instantiate:error retrieving entropy" that was + # present in Orthanc 1.6.0, if statically linking on Ubuntu 18.04 + add_definitions( + -DOPENSSL_RAND_SEED_OS + ) +endif() + + +set_source_files_properties( + ${OPENSSL_SOURCES} + PROPERTIES COMPILE_DEFINITIONS + "${OPENSSL_DEFINITIONS};DSO_NONE" + ) diff --git a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake new file mode 100644 index 0000000..25a1373 --- /dev/null +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake @@ -0,0 +1,787 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +## +## This is a CMake configuration file that configures the core +## libraries of Orthanc. This file can be used by external projects so +## as to gain access to the Orthanc APIs (the most prominent examples +## are currently "Stone of Orthanc" and "Orthanc for whole-slide +## imaging plugin"). +## + + +##################################################################### +## Configuration of the components +##################################################################### + +# Some basic inclusions +include(CMakePushCheckState) +include(CheckFunctionExists) +include(CheckIncludeFile) +include(CheckIncludeFileCXX) +include(CheckIncludeFiles) +include(CheckLibraryExists) +include(CheckStructHasMember) +include(CheckSymbolExists) +include(CheckTypeSize) + +if(CMAKE_VERSION VERSION_GREATER "3.11") + find_package(Python REQUIRED COMPONENTS Interpreter) + set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) +else() + include(FindPythonInterp) + find_package(PythonInterp REQUIRED) +endif() + +include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake) + + +##################################################################### +## Disable unneeded macros +##################################################################### + +if (NOT ENABLE_SQLITE) + unset(USE_SYSTEM_SQLITE CACHE) + add_definitions(-DORTHANC_ENABLE_SQLITE=0) +endif() + +if (NOT ENABLE_CRYPTO_OPTIONS) + unset(ENABLE_SSL CACHE) + unset(ENABLE_PKCS11 CACHE) + unset(ENABLE_OPENSSL_ENGINES CACHE) + unset(OPENSSL_STATIC_VERSION CACHE) + unset(USE_SYSTEM_OPENSSL CACHE) + unset(USE_SYSTEM_LIBP11 CACHE) + add_definitions( + -DORTHANC_ENABLE_SSL=0 + -DORTHANC_ENABLE_PKCS11=0 + ) +endif() + +if (NOT ENABLE_WEB_CLIENT) + unset(USE_SYSTEM_CURL CACHE) + add_definitions(-DORTHANC_ENABLE_CURL=0) +endif() + +if (NOT ENABLE_WEB_SERVER) + unset(ENABLE_CIVETWEB CACHE) + unset(USE_SYSTEM_CIVETWEB CACHE) + unset(USE_SYSTEM_MONGOOSE CACHE) + add_definitions( + -DORTHANC_ENABLE_CIVETWEB=0 + -DORTHANC_ENABLE_MONGOOSE=0 + ) +endif() + +if (NOT ENABLE_JPEG) + unset(USE_SYSTEM_LIBJPEG CACHE) + add_definitions(-DORTHANC_ENABLE_JPEG=0) +endif() + +if (NOT ENABLE_ZLIB) + unset(USE_SYSTEM_ZLIB CACHE) + add_definitions(-DORTHANC_ENABLE_ZLIB=0) +endif() + +if (NOT ENABLE_PNG) + unset(USE_SYSTEM_LIBPNG CACHE) + add_definitions(-DORTHANC_ENABLE_PNG=0) +endif() + +if (NOT ENABLE_LUA) + unset(USE_SYSTEM_LUA CACHE) + unset(ENABLE_LUA_MODULES CACHE) + unset(ORTHANC_LUA_VERSION) + add_definitions(-DORTHANC_ENABLE_LUA=0) +endif() + +if (NOT ENABLE_PUGIXML) + unset(USE_SYSTEM_PUGIXML CACHE) + add_definitions(-DORTHANC_ENABLE_PUGIXML=0) +endif() + +if (NOT ENABLE_LOCALE) + unset(BOOST_LOCALE_BACKEND CACHE) + add_definitions(-DORTHANC_ENABLE_LOCALE=0) +endif() + +if (NOT ENABLE_GOOGLE_TEST) + unset(USE_SYSTEM_GOOGLE_TEST CACHE) + unset(USE_GOOGLE_TEST_DEBIAN_PACKAGE CACHE) +endif() + +if (NOT ENABLE_DCMTK) + add_definitions( + -DORTHANC_ENABLE_DCMTK=0 + -DORTHANC_ENABLE_DCMTK_JPEG=0 + -DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=0 + -DORTHANC_ENABLE_DCMTK_NETWORKING=0 + -DORTHANC_ENABLE_DCMTK_TRANSCODING=0 + ) + unset(DCMTK_DICTIONARY_DIR CACHE) + unset(DCMTK_VERSION CACHE) + unset(USE_DCMTK_362_PRIVATE_DIC CACHE) + unset(USE_SYSTEM_DCMTK CACHE) + unset(ENABLE_DCMTK_JPEG CACHE) + unset(ENABLE_DCMTK_JPEG_LOSSLESS CACHE) + unset(DCMTK_STATIC_VERSION CACHE) + unset(ENABLE_DCMTK_LOG CACHE) +endif() + +if (NOT ENABLE_PROTOBUF) + unset(USE_SYSTEM_PROTOBUF CACHE) + add_definitions(-DORTHANC_ENABLE_PROTOBUF=0) +endif() + + +##################################################################### +## List of source files +##################################################################### + +set(ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryCache.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryObjectCache.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/ChunkedBuffer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomPath.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomTag.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/Window.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/EnumerationDictionary.h + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Enumerations.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FileInfo.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/MemoryStorageArea.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/PluginStorageAreaAdapter.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/CStringMatcher.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpContentNegociation.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpToolbox.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/MultipartStreamReader.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/StringMatcher.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Logging.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/MallocMemoryBuffer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/OrthancException.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/OrthancFramework.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiHierarchy.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiPath.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SerializationToolbox.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/StringMemoryBuffer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Toolbox.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/WebServiceParameters.cpp + ) + +if (ENABLE_MODULE_IMAGES) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/Font.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/FontRegistry.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/IImageWriter.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/Image.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/ImageAccessor.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/ImageBuffer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/ImageProcessing.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/NumpyWriter.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/PamReader.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/PamWriter.cpp + ) +endif() + +if (ENABLE_MODULE_DICOM) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomArray.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomElement.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomImageInformation.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomInstanceHasher.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomIntegerPixelAccessor.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomMap.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomStreamReader.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/DicomValue.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomFormat/StreamBlockReader.cpp + ) +endif() + +if (ENABLE_MODULE_JOBS) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/GenericJobUnserializer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/JobInfo.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/JobStatus.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/JobStepResult.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/Operations/JobOperationValues.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/Operations/LogJobOperation.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/Operations/NullOperationValue.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/Operations/StringOperationValue.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/SetOfCommandsJob.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/SetOfInstancesJob.cpp + ) +endif() + + + +##################################################################### +## Configuration of optional third-party dependencies +##################################################################### + + +## +## Embedded database: SQLite +## + +if (ENABLE_SQLITE) + include(${CMAKE_CURRENT_LIST_DIR}/SQLiteConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_SQLITE=1) + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SQLite/Connection.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SQLite/FunctionContext.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SQLite/Statement.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SQLite/StatementId.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SQLite/StatementReference.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SQLite/Transaction.cpp + ) +endif() + + +## +## Cryptography: OpenSSL and libp11 +## Must be above "ENABLE_WEB_CLIENT" and "ENABLE_WEB_SERVER" +## + +if (ENABLE_CRYPTO_OPTIONS) + if (ENABLE_SSL) + include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_SSL=1) + else() + unset(ENABLE_OPENSSL_ENGINES CACHE) + unset(USE_SYSTEM_OPENSSL CACHE) + add_definitions(-DORTHANC_ENABLE_SSL=0) + endif() + + if (ENABLE_PKCS11) + if (ENABLE_SSL) + include(${CMAKE_CURRENT_LIST_DIR}/LibP11Configuration.cmake) + + add_definitions(-DORTHANC_ENABLE_PKCS11=1) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Pkcs11.cpp + ) + else() + message(FATAL_ERROR "OpenSSL is required to enable PKCS#11 support") + endif() + else() + add_definitions(-DORTHANC_ENABLE_PKCS11=0) + endif() +endif() + + +## +## HTTP client: libcurl +## + +if (ENABLE_WEB_CLIENT) + include(${CMAKE_CURRENT_LIST_DIR}/LibCurlConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_CURL=1) + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpClient.cpp + ) +endif() + + +## +## HTTP server: Mongoose 3.8 or Civetweb +## + +if (ENABLE_WEB_SERVER) + if (ENABLE_CIVETWEB) + include(${CMAKE_CURRENT_LIST_DIR}/CivetwebConfiguration.cmake) + add_definitions( + -DORTHANC_ENABLE_CIVETWEB=1 + -DORTHANC_ENABLE_MONGOOSE=0 + ) + set(ORTHANC_ENABLE_CIVETWEB 1) + else() + include(${CMAKE_CURRENT_LIST_DIR}/MongooseConfiguration.cmake) + endif() + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/BufferHttpSender.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/FilesystemHttpHandler.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/FilesystemHttpSender.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpFileSender.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpOutput.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpServer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpStreamTranscoder.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/IHttpHandler.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/StringHttpOutput.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApi.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiCall.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiCallDocumentation.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiGetCall.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiOutput.cpp + ) + + if (ENABLE_PUGIXML) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/IWebDavBucket.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/WebDavStorage.cpp + ) + endif() +endif() + +if (ORTHANC_ENABLE_CIVETWEB) + add_definitions(-DORTHANC_ENABLE_CIVETWEB=1) +else() + add_definitions(-DORTHANC_ENABLE_CIVETWEB=0) +endif() + +if (ORTHANC_ENABLE_MONGOOSE) + add_definitions(-DORTHANC_ENABLE_MONGOOSE=1) +else() + add_definitions(-DORTHANC_ENABLE_MONGOOSE=0) +endif() + + + +## +## JPEG support: libjpeg +## + +if (ENABLE_JPEG) + if (NOT ENABLE_MODULE_IMAGES) + message(FATAL_ERROR "Image processing primitives must be enabled if enabling libjpeg support") + endif() + + include(${CMAKE_CURRENT_LIST_DIR}/LibJpegConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_JPEG=1) + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/JpegErrorManager.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/JpegReader.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/JpegWriter.cpp + ) +endif() + + +## +## zlib support +## + +if (ENABLE_ZLIB) + include(${CMAKE_CURRENT_LIST_DIR}/ZlibConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_ZLIB=1) + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/DeflateBaseCompressor.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/GzipCompressor.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/IBufferCompressor.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/ZipReader.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/ZlibCompressor.cpp + ) + + if (NOT ORTHANC_SANDBOXED) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/HierarchicalZipWriter.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/ZipWriter.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageAccessor.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageCache.cpp + ) + endif() +endif() + + +## +## PNG support: libpng (in conjunction with zlib) +## + +if (ENABLE_PNG) + if (NOT ENABLE_ZLIB) + message(FATAL_ERROR "Support for zlib must be enabled if enabling libpng support") + endif() + + if (NOT ENABLE_MODULE_IMAGES) + message(FATAL_ERROR "Image processing primitives must be enabled if enabling libpng support") + endif() + + include(${CMAKE_CURRENT_LIST_DIR}/LibPngConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_PNG=1) + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/PngReader.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Images/PngWriter.cpp + ) +endif() + + +## +## Lua support +## + +if (ENABLE_LUA) + include(${CMAKE_CURRENT_LIST_DIR}/LuaConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_LUA=1) + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Lua/LuaContext.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Lua/LuaFunctionCall.cpp + ) +endif() + + +## +## XML support: pugixml +## + +if (ENABLE_PUGIXML) + include(${CMAKE_CURRENT_LIST_DIR}/PugixmlConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_PUGIXML=1) +endif() + + +## +## Locale support +## + +if (ENABLE_LOCALE) + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # In WebAssembly or asm.js, we rely on the version of iconv that + # is shipped with the stdlib + unset(BOOST_LOCALE_BACKEND CACHE) + else() + if (BOOST_LOCALE_BACKEND STREQUAL "gcc" OR + BOOST_LOCALE_BACKEND STREQUAL "oficonv") + elseif (BOOST_LOCALE_BACKEND STREQUAL "libiconv") + include(${CMAKE_CURRENT_LIST_DIR}/LibIconvConfiguration.cmake) + elseif (BOOST_LOCALE_BACKEND STREQUAL "icu") + include(${CMAKE_CURRENT_LIST_DIR}/LibIcuConfiguration.cmake) + elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv") + message("Using Microsoft Window's wconv") + else() + message(FATAL_ERROR "Invalid value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}") + endif() + endif() + + add_definitions(-DORTHANC_ENABLE_LOCALE=1) +endif() + + +## +## Google Test for unit testing +## + +if (ENABLE_GOOGLE_TEST) + include(${CMAKE_CURRENT_LIST_DIR}/GoogleTestConfiguration.cmake) +endif() + + +## +## Google Protocol Buffers +## + +if (ENABLE_PROTOBUF) + include(${CMAKE_CURRENT_LIST_DIR}/ProtobufConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_PROTOBUF=1) +endif() + + + +##################################################################### +## Inclusion of mandatory third-party dependencies +##################################################################### + +include(${CMAKE_CURRENT_LIST_DIR}/JsonCppConfiguration.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/UuidConfiguration.cmake) + +# We put Boost as the last dependency, as it is the heaviest to +# configure, which allows one to quickly spot problems when configuring +# static builds in other dependencies +include(${CMAKE_CURRENT_LIST_DIR}/BoostConfiguration.cmake) + + +##################################################################### +## Optional configuration of DCMTK +##################################################################### + +if (ENABLE_DCMTK) + if (NOT ENABLE_LOCALE) + message(FATAL_ERROR "Support for locales must be enabled if enabling DCMTK support") + endif() + + if (NOT ENABLE_MODULE_DICOM) + message(FATAL_ERROR "DICOM module must be enabled if enabling DCMTK support") + endif() + + # WARNING - MUST be after "OpenSslConfiguration.cmake", otherwise + # DICOM TLS will not be corrected detected + include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfiguration.cmake) + + add_definitions(-DORTHANC_ENABLE_DCMTK=1) + + if (ENABLE_DCMTK_JPEG) + add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG=1) + else() + add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG=0) + endif() + + if (ENABLE_DCMTK_JPEG_LOSSLESS) + add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=1) + else() + add_definitions(-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=0) + endif() + + set(ORTHANC_DICOM_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/DicomFindAnswers.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DicomModification.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DicomWebJsonVisitor.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/FromDcmtkBridge.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/ParsedDicomCache.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/ParsedDicomDir.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/ParsedDicomFile.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/ToDcmtkBridge.cpp + + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/Internals/DicomFrameIndex.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/Internals/DicomImageDecoder.cpp + ) + + if (NOT ORTHANC_SANDBOXED) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DicomDirWriter.cpp + ) + endif() + + if (ENABLE_DCMTK_NETWORKING) + add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=1) + list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/DicomAssociation.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/DicomAssociationParameters.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/DicomControlUserConnection.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/DicomServer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/DicomStoreUserConnection.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/Internals/CommandDispatcher.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/Internals/FindScp.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/Internals/MoveScp.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/Internals/GetScp.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/Internals/StoreScp.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/RemoteModalityParameters.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp + ) + + if (ENABLE_SSL) + list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomNetworking/Internals/DicomTls.cpp + ) + endif() + else() + add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=0) + endif() + + # New in Orthanc 1.6.0 + if (ENABLE_DCMTK_TRANSCODING) + add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=1) + list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DcmtkTranscoder.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/IDicomTranscoder.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/MemoryBufferTranscoder.cpp + ) + else() + add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=0) + endif() +endif() + + +##################################################################### +## Configuration of the C/C++ macros +##################################################################### + +add_definitions( + -DORTHANC_API_VERSION=${ORTHANC_API_VERSION} + -DORTHANC_DATABASE_VERSION=${ORTHANC_DATABASE_VERSION} + -DORTHANC_DEFAULT_DICOM_ENCODING=Encoding_Latin1 + -DORTHANC_ENABLE_BASE64=1 + -DORTHANC_ENABLE_MD5=1 + -DORTHANC_MAXIMUM_TAG_LENGTH=256 + -DORTHANC_VERSION="${ORTHANC_VERSION}" + ) + + +if (ORTHANC_BUILDING_FRAMEWORK_LIBRARY) + add_definitions(-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=1) +else() + add_definitions(-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0) +endif() + + +if (ORTHANC_SANDBOXED) + add_definitions( + -DORTHANC_SANDBOXED=1 + ) + + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(ORTHANC_ENABLE_LOGGING ON) + set(ORTHANC_ENABLE_LOGGING_STDIO ON) + else() + set(ORTHANC_ENABLE_LOGGING OFF) + endif() + +else() + set(ORTHANC_ENABLE_LOGGING ON) + set(ORTHANC_ENABLE_LOGGING_STDIO OFF) + + add_definitions( + -DORTHANC_SANDBOXED=0 + ) + + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/MemoryStringCache.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Cache/SharedArchive.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileBuffer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FilesystemStorage.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/MetricsRegistry.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/MultiThreading/RunnableWorkersPool.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/MultiThreading/Semaphore.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/MultiThreading/SharedMessageQueue.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SharedLibrary.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/SystemToolbox.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/TemporaryFile.cpp + ) + + if (ENABLE_MODULE_JOBS) + list(APPEND ORTHANC_CORE_SOURCES_INTERNAL + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/JobsEngine.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/JobsEngine/JobsRegistry.cpp + ) + endif() +endif() + + + +if (ORTHANC_ENABLE_LOGGING) + add_definitions(-DORTHANC_ENABLE_LOGGING=1) +else() + add_definitions(-DORTHANC_ENABLE_LOGGING=0) +endif() + +if (ORTHANC_ENABLE_LOGGING_STDIO) + add_definitions(-DORTHANC_ENABLE_LOGGING_STDIO=1) +else() + add_definitions(-DORTHANC_ENABLE_LOGGING_STDIO=0) +endif() + + + +##################################################################### +## Configuration of Orthanc versioning macros (new in Orthanc 1.5.0) +##################################################################### + +if (ORTHANC_VERSION STREQUAL "mainline") + set(ORTHANC_VERSION_MAJOR "999") + set(ORTHANC_VERSION_MINOR "999") + set(ORTHANC_VERSION_REVISION "999") +else() + string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\1" ORTHANC_VERSION_MAJOR ${ORTHANC_VERSION}) + string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\2" ORTHANC_VERSION_MINOR ${ORTHANC_VERSION}) + string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\3" ORTHANC_VERSION_REVISION ${ORTHANC_VERSION}) + + if (NOT ORTHANC_VERSION STREQUAL + "${ORTHANC_VERSION_MAJOR}.${ORTHANC_VERSION_MINOR}.${ORTHANC_VERSION_REVISION}") + message(FATAL_ERROR "Error in the (x.y.z) format of the Orthanc version: ${ORTHANC_VERSION}") + endif() +endif() + +add_definitions( + -DORTHANC_VERSION_MAJOR=${ORTHANC_VERSION_MAJOR} + -DORTHANC_VERSION_MINOR=${ORTHANC_VERSION_MINOR} + -DORTHANC_VERSION_REVISION=${ORTHANC_VERSION_REVISION} + ) + + + +##################################################################### +## Gathering of all the source code +##################################################################### + +# The "xxx_INTERNAL" variables list the source code that belongs to +# the Orthanc project. It can be used to configure precompiled headers +# if using Microsoft Visual Studio. + +# The "xxx_DEPENDENCIES" variables list the source code coming from +# third-party dependencies. + + +set(ORTHANC_CORE_SOURCES_DEPENDENCIES + ${BOOST_SOURCES} + ${CIVETWEB_SOURCES} + ${CURL_SOURCES} + ${JSONCPP_SOURCES} + ${LIBICONV_SOURCES} + ${LIBICU_SOURCES} + ${LIBJPEG_SOURCES} + ${LIBP11_SOURCES} + ${LIBPNG_SOURCES} + ${LUA_SOURCES} + ${MONGOOSE_SOURCES} + ${OPENSSL_SOURCES} + ${PROTOBUF_LIBRARY_SOURCES} + ${PUGIXML_SOURCES} + ${SQLITE_SOURCES} + ${UUID_SOURCES} + ${ZLIB_SOURCES} + + ${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/md5/md5.c + ${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/base64/base64.cpp + ) + +if (ENABLE_ZLIB AND NOT ORTHANC_SANDBOXED) + list(APPEND ORTHANC_CORE_SOURCES_DEPENDENCIES + # This is the minizip distribution to create/decode ZIP files using zlib + ${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/minizip/ioapi.c + ${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/minizip/unzip.c + ${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/minizip/zip.c + ) +endif() + + +if (NOT "${LIBICU_RESOURCES}" STREQUAL "" OR + NOT "${DCMTK_DICTIONARIES}" STREQUAL "") + EmbedResources( + --namespace=Orthanc.FrameworkResources + --target=OrthancFrameworkResources + --framework-path=${CMAKE_CURRENT_LIST_DIR}/../../Sources + ${LIBICU_RESOURCES} + ${DCMTK_DICTIONARIES} + ) +endif() + + +set(ORTHANC_CORE_SOURCES + ${ORTHANC_CORE_SOURCES_INTERNAL} + ${ORTHANC_CORE_SOURCES_DEPENDENCIES} + ) + +if (ENABLE_DCMTK) + list(APPEND ORTHANC_DICOM_SOURCES_DEPENDENCIES + ${DCMTK_SOURCES} + ) + + set(ORTHANC_DICOM_SOURCES + ${ORTHANC_DICOM_SOURCES_INTERNAL} + ${ORTHANC_DICOM_SOURCES_DEPENDENCIES} + ) +endif() diff --git a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake new file mode 100644 index 0000000..0924fd9 --- /dev/null +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake @@ -0,0 +1,155 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +##################################################################### +## Versioning information +##################################################################### + +# Version of the build, should always be "mainline" except in release branches +set(ORTHANC_VERSION "1.12.8") + +# Version of the database schema. History: +# * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning +# * Orthanc 0.3.1 = version 2 +# * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3 +# * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4 +# * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5 +# * Orthanc 0.9.5 -> mainline = version 6 +set(ORTHANC_DATABASE_VERSION 6) + +# Version of the Orthanc API, can be retrieved from "/system" URI in +# order to check whether new URI endpoints are available even if using +# the mainline version of Orthanc +set(ORTHANC_API_VERSION "29") + + +##################################################################### +## CMake parameters tunable by the user +##################################################################### + +# Support of static compilation +set(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") +set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") + +# Generic parameters of the build +set(ENABLE_CIVETWEB ON CACHE BOOL "Use Civetweb instead of Mongoose (Mongoose was the default embedded HTTP server in Orthanc <= 1.5.1)") +set(ENABLE_PKCS11 OFF CACHE BOOL "Enable PKCS#11 for HTTPS client authentication using hardware security modules and smart cards") +set(ENABLE_PROFILING OFF CACHE BOOL "Whether to enable the generation of profiling information with gprof") +set(ENABLE_SSL ON CACHE BOOL "Include support for SSL") +set(ENABLE_LUA_MODULES OFF CACHE BOOL "Enable support for loading external Lua modules (only meaningful if using static version of the Lua engine)") + +# Parameters to fine-tune linking against system libraries +set(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") +set(USE_SYSTEM_CIVETWEB ON CACHE BOOL "Use the system version of Civetweb (experimental)") +set(USE_SYSTEM_CURL ON CACHE BOOL "Use the system version of LibCurl") +set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test") +set(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +set(USE_SYSTEM_LIBICONV ON CACHE BOOL "Use the system version of libiconv") +set(USE_SYSTEM_LIBICU ON CACHE BOOL "Use the system version of libicu") +set(USE_SYSTEM_LIBJPEG ON CACHE BOOL "Use the system version of libjpeg") +set(USE_SYSTEM_LIBP11 OFF CACHE BOOL "Use the system version of libp11 (PKCS#11 wrapper library)") +set(USE_SYSTEM_LIBPNG ON CACHE BOOL "Use the system version of libpng") +set(USE_SYSTEM_LUA ON CACHE BOOL "Use the system version of Lua") +set(USE_SYSTEM_MONGOOSE ON CACHE BOOL "Use the system version of Mongoose") +set(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL") +set(USE_SYSTEM_PROTOBUF ON CACHE BOOL "Use the system version of Google Protocol Buffers") +set(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml") +set(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite") +set(USE_SYSTEM_UUID ON CACHE BOOL "Use the system version of the uuid library from e2fsprogs") +set(USE_SYSTEM_ZLIB ON CACHE BOOL "Use the system version of ZLib") + +# Parameters specific to DCMTK +set(DCMTK_DICTIONARY_DIR "" CACHE PATH "Directory containing the DCMTK dictionaries \"dicom.dic\" and \"private.dic\" (only when using system version of DCMTK)") +set(DCMTK_STATIC_VERSION "3.6.9" CACHE STRING "Version of DCMTK to be used in static builds (can be \"3.6.0\", \"3.6.2\", \"3.6.4\", \"3.6.5\", \"3.6.6\", \"3.6.7\", \"3.6.8\", or \"3.6.9\")") +set(USE_DCMTK_362_PRIVATE_DIC ON CACHE BOOL "Use the dictionary of private tags from DCMTK 3.6.2 if using DCMTK 3.6.0") +set(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK") +set(ENABLE_DCMTK_LOG ON CACHE BOOL "Enable logging internal to DCMTK") +set(ENABLE_DCMTK_JPEG ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression") +set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression") + +# Advanced and distribution-specific parameters +set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)") +set(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)") +set(BOOST_LOCALE_BACKEND "libiconv" CACHE STRING "Back-end for locales that is used by Boost (can be \"gcc\", \"libiconv\", \"icu\", or \"wconv\" on Windows)") +set(DCMTK_LOCALE_BACKEND "oficonv" CACHE STRING "Back-end for locales that is used by DCMTK (can be \"gcc\", \"libiconv\", \"icu\" (only up to DCMTK 3.6.8), \"oficonv\")") +set(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)") +set(USE_LEGACY_JSONCPP OFF CACHE BOOL "Use the old branch 0.x.y of JsonCpp, that does not require a C++11 compiler (for LSB and old versions of Visual Studio)") +set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)") +set(USE_LEGACY_BOOST OFF CACHE BOOL "Use boost 1.69.0, latest version to be compatible with LSB") +set(MSVC_MULTIPLE_PROCESSES OFF CACHE BOOL "Add the /MP option to build with multiple processes if using Visual Studio") +set(EMSCRIPTEN_TARGET_MODE "wasm" CACHE STRING "Sets the target mode for Emscripten (can be \"wasm\" or \"asm.js\")") +set(EMSCRIPTEN_TRAP_MODE "" CACHE STRING "Sets the trap mode for Emscripten for numeric errors (can notably be empty, or \"clamp\")") +set(OPENSSL_STATIC_VERSION "3.0" CACHE STRING "Version of OpenSSL to be used in static builds (can be \"1.1.1\" or \"3.0\")") +set(CIVETWEB_OPENSSL_API "1.1" CACHE STRING "Version of the OpenSSL API to be used in civetweb in static builds (can be \"1.0\" or \"1.1\"") +set(ORTHANC_LUA_VERSION "" CACHE STRING "Force the version of Lua to be used by Orthanc (for instance \"5.3\"), if empty, this will be autodetected") + +mark_as_advanced(CIVETWEB_OPENSSL_API) +mark_as_advanced(EMSCRIPTEN_TARGET_MODE) +mark_as_advanced(EMSCRIPTEN_TRAP_MODE) +mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS) +mark_as_advanced(USE_DCMTK_362_PRIVATE_DIC) +mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE) +mark_as_advanced(USE_PUGIXML) + + +##################################################################### +## Internal CMake parameters to enable the optional subcomponents of +## the Orthanc framework +##################################################################### + +# These options must be set to "ON" if compiling Orthanc, but might be +# set to "OFF" by third-party projects if their associated features +# are not required + +set(ENABLE_CRYPTO_OPTIONS OFF CACHE INTERNAL "Show options related to cryptography") +set(ENABLE_JPEG OFF CACHE INTERNAL "Enable support of JPEG") +set(ENABLE_GOOGLE_TEST OFF CACHE INTERNAL "Enable support of Google Test") +set(ENABLE_LOCALE OFF CACHE INTERNAL "Enable support for locales (notably in Boost)") +set(ENABLE_LUA OFF CACHE INTERNAL "Enable support of Lua scripting") +set(ENABLE_PNG OFF CACHE INTERNAL "Enable support of PNG") +set(ENABLE_PROTOBUF OFF CACHE INTERNAL "Enable support for Google Protocol Buffers' library") +set(ENABLE_PROTOBUF_COMPILER OFF CACHE INTERNAL "Enable support for Google Protocol Buffers' compiler") +set(ENABLE_PUGIXML OFF CACHE INTERNAL "Enable support of XML through Pugixml") +set(ENABLE_SQLITE OFF CACHE INTERNAL "Enable support of SQLite databases") +set(ENABLE_ZLIB OFF CACHE INTERNAL "Enable support of zlib") +set(ENABLE_WEB_CLIENT OFF CACHE INTERNAL "Enable Web client") +set(ENABLE_WEB_SERVER OFF CACHE INTERNAL "Enable embedded Web server") +set(ENABLE_DCMTK OFF CACHE INTERNAL "Enable DCMTK") +set(ENABLE_DCMTK_NETWORKING OFF CACHE INTERNAL "Enable DICOM networking in DCMTK") +set(ENABLE_DCMTK_TRANSCODING OFF CACHE INTERNAL "Enable DICOM transcoding in DCMTK") +set(ENABLE_OPENSSL_ENGINES OFF CACHE INTERNAL "Enable support of engines in OpenSSL") + +set(ORTHANC_SANDBOXED OFF CACHE INTERNAL + "Whether Orthanc runs inside a sandboxed environment (such as Google NaCl or WebAssembly)") + +set(ORTHANC_BUILDING_FRAMEWORK_LIBRARY OFF CACHE INTERNAL + "Whether we are in the process of building the Orthanc Framework shared library") + +# +# These options can be used to turn off some modules of the Orthanc +# framework, in order to speed up the compilation time of third-party +# projects. +# + +set(ENABLE_MODULE_IMAGES ON CACHE INTERNAL "Enable module for image processing") +set(ENABLE_MODULE_JOBS ON CACHE INTERNAL "Enable module for jobs") +set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "Enable module for DICOM handling") diff --git a/OrthancFramework/Resources/CMake/ProtobufConfiguration.cmake b/OrthancFramework/Resources/CMake/ProtobufConfiguration.cmake new file mode 100644 index 0000000..0043f40 --- /dev/null +++ b/OrthancFramework/Resources/CMake/ProtobufConfiguration.cmake @@ -0,0 +1,86 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_PROTOBUF) + if (ENABLE_PROTOBUF_COMPILER) + include(ExternalProject) + externalproject_add(ProtobufCompiler + SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../ProtocolBuffers" + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ProtobufCompiler-build" + # this helps triggering build when changing the external project + BUILD_ALWAYS 1 + CMAKE_ARGS + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR} + ) + + # The "protoc" compiler is built using "externalproject_add", + # which builds for the host platform, not for the target platform + if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + set(Suffix ".exe") + else() + set(Suffix "") + endif() + + set(PROTOC_EXECUTABLE ${CMAKE_CURRENT_BINARY_DIR}/protoc${Suffix}) + endif() + + include(${CMAKE_CURRENT_LIST_DIR}/../ProtocolBuffers/ProtobufLibrary.cmake) + source_group(ThirdParty\\Protobuf REGULAR_EXPRESSION ${PROTOBUF_SOURCE_DIR}/.*) + +else() + if (CMAKE_CROSSCOMPILING) + message(FATAL_ERROR "If cross-compiling, the static version of Protocol Buffers should be used to avoid version mismatch") + endif() + + if (ENABLE_PROTOBUF_COMPILER) + find_program(PROTOC_EXECUTABLE protoc) + if (${PROTOC_EXECUTABLE} MATCHES "PROTOC_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'protoc' compiler for Protocol Buffers (package 'protobuf-compiler' on Debian/Ubuntu)") + endif() + add_custom_target(ProtobufCompiler) + endif() + + check_include_file_cxx(google/protobuf/any.h HAVE_PROTOBUF_H) + if (NOT HAVE_PROTOBUF_H) + message(FATAL_ERROR "Please install the libprotobuf-dev package") + endif() + + set(CMAKE_REQUIRED_LIBRARIES "protobuf") + + include(CheckCXXSourceCompiles) + check_cxx_source_compiles( + " +#include +int main() +{ + google::protobuf::FieldDescriptor::TypeName(google::protobuf::FieldDescriptor::TYPE_FLOAT); +} +" HAVE_PROTOBUF_LIB) + if (NOT HAVE_PROTOBUF_LIB) + message(FATAL_ERROR "Cannot find the protobuf library") + endif() + + unset(CMAKE_REQUIRED_LIBRARIES) + + link_libraries(protobuf) +endif() diff --git a/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake b/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake new file mode 100644 index 0000000..a4a51e7 --- /dev/null +++ b/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake @@ -0,0 +1,48 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML) + set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.14) + set(PUGIXML_MD5 "06e4242ee2352ee63c2b6627c6e3addb") + set(PUGIXML_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/pugixml-1.14.tar.gz") + + DownloadPackage(${PUGIXML_MD5} ${PUGIXML_URL} "${PUGIXML_SOURCES_DIR}") + + include_directories( + ${PUGIXML_SOURCES_DIR}/src + ) + + set(PUGIXML_SOURCES + #${PUGIXML_SOURCES_DIR}/src/vlog_is_on.cc + ${PUGIXML_SOURCES_DIR}/src/pugixml.cpp + ) + + source_group(ThirdParty\\pugixml REGULAR_EXPRESSION ${PUGIXML_SOURCES_DIR}/.*) + +else() + CHECK_INCLUDE_FILE_CXX(pugixml.hpp HAVE_PUGIXML_H) + if (NOT HAVE_PUGIXML_H) + message(FATAL_ERROR "Please install the libpugixml-dev package") + endif() + + link_libraries(pugixml) +endif() diff --git a/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake new file mode 100644 index 0000000..3c8b965 --- /dev/null +++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake @@ -0,0 +1,90 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_SQLITE) + set(SQLITE_STATIC ON) +else() + set(SQLITE_STATIC OFF) +endif() + + +if (SQLITE_STATIC) + SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3460100) + SET(SQLITE_MD5 "1fb0f7ebbee45752098cf453b6dffff3") + SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3460100.zip") + + set(ORTHANC_SQLITE_VERSION 3046001) + + DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}") + + set(SQLITE_SOURCES + ${SQLITE_SOURCES_DIR}/sqlite3.c + ) + + add_definitions( + # For SQLite to run in the "Serialized" thread-safe mode + # http://www.sqlite.org/threadsafe.html + -DSQLITE_THREADSAFE=1 + -DSQLITE_OMIT_LOAD_EXTENSION # Disable SQLite plugins + ) + + include_directories( + ${SQLITE_SOURCES_DIR} + ) + + source_group(ThirdParty\\SQLite REGULAR_EXPRESSION ${SQLITE_SOURCES_DIR}/.*) + +else() + CHECK_INCLUDE_FILE(sqlite3.h HAVE_SQLITE_H) + if (NOT HAVE_SQLITE_H) + message(FATAL_ERROR "Please install the libsqlite3-dev package") + endif() + + find_path(SQLITE_INCLUDE_DIR + NAMES sqlite3.h + PATHS + /usr/include + /usr/local/include + ) + message("SQLite include dir: ${SQLITE_INCLUDE_DIR}") + + # Autodetection of the version of SQLite + file(STRINGS "${SQLITE_INCLUDE_DIR}/sqlite3.h" SQLITE_VERSION_NUMBER1 REGEX "#define SQLITE_VERSION_NUMBER.*$") + string(REGEX REPLACE "#define SQLITE_VERSION_NUMBER(.*)$" "\\1" SQLITE_VERSION_NUMBER2 ${SQLITE_VERSION_NUMBER1}) + + # Remove the trailing spaces to convert the string to a proper integer + string(STRIP ${SQLITE_VERSION_NUMBER2} ORTHANC_SQLITE_VERSION) + + message("Detected version of SQLite: ${ORTHANC_SQLITE_VERSION}") + + IF (${ORTHANC_SQLITE_VERSION} LESS 3007000) + # "sqlite3_create_function_v2" is not defined in SQLite < 3.7.0 + message(FATAL_ERROR "SQLite version must be above 3.7.0. Please set the CMake variable USE_SYSTEM_SQLITE to OFF.") + ENDIF() + + link_libraries(sqlite3) +endif() + + +add_definitions( + -DORTHANC_SQLITE_VERSION=${ORTHANC_SQLITE_VERSION} + ) diff --git a/OrthancFramework/Resources/CMake/Uninstall.cmake.in b/OrthancFramework/Resources/CMake/Uninstall.cmake.in new file mode 100644 index 0000000..2c49a83 --- /dev/null +++ b/OrthancFramework/Resources/CMake/Uninstall.cmake.in @@ -0,0 +1,25 @@ +# Code taken from the CMake FAQ +# http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F + +if (NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + message(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"") +endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + +file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) +string(REGEX REPLACE "\n" ";" files "${files}") +list(REVERSE files) +foreach (file ${files}) + message(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") + if (EXISTS "$ENV{DESTDIR}${file}") + execute_process( + COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}" + OUTPUT_VARIABLE rm_out + RESULT_VARIABLE rm_retval + ) + if(NOT ${rm_retval} EQUAL 0) + message(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") + endif (NOT ${rm_retval} EQUAL 0) + else (EXISTS "$ENV{DESTDIR}${file}") + message(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.") + endif (EXISTS "$ENV{DESTDIR}${file}") +endforeach(file) diff --git a/OrthancFramework/Resources/CMake/UuidConfiguration.cmake b/OrthancFramework/Resources/CMake/UuidConfiguration.cmake new file mode 100644 index 0000000..c3300c1 --- /dev/null +++ b/OrthancFramework/Resources/CMake/UuidConfiguration.cmake @@ -0,0 +1,154 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + + if (STATIC_BUILD OR NOT USE_SYSTEM_UUID) + SET(E2FSPROGS_SOURCES_DIR ${CMAKE_BINARY_DIR}/e2fsprogs-1.44.5) + SET(E2FSPROGS_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/e2fsprogs-1.44.5.tar.gz") + SET(E2FSPROGS_MD5 "8d78b11d04d26c0b2dd149529441fa80") + + if (IS_DIRECTORY "${E2FSPROGS_SOURCES_DIR}") + set(FirstRun OFF) + else() + set(FirstRun ON) + endif() + + DownloadPackage(${E2FSPROGS_MD5} ${E2FSPROGS_URL} "${E2FSPROGS_SOURCES_DIR}") + + + ## + ## Patch for OS X, in order to be compatible with Cocoa, and for + ## WebAssembly (used in Stone) + ## + + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/../Patches/e2fsprogs-1.44.5.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (FirstRun AND Failure) + message(FATAL_ERROR "Error while patching a file") + endif() + + + include_directories( + BEFORE ${E2FSPROGS_SOURCES_DIR}/lib + ) + + set(UUID_SOURCES + #${E2FSPROGS_SOURCES_DIR}/lib/uuid/tst_uuid.c + #${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid_time.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/clear.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/compare.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/copy.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/gen_uuid.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/isnull.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/pack.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/parse.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/unpack.c + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/unparse.c + ) + + check_include_file("net/if.h" HAVE_NET_IF_H) + check_include_file("net/if_dl.h" HAVE_NET_IF_DL_H) + check_include_file("netinet/in.h" HAVE_NETINET_IN_H) + check_include_file("stdlib.h" HAVE_STDLIB_H) + check_include_file("sys/file.h" HAVE_SYS_FILE_H) + check_include_file("sys/ioctl.h" HAVE_SYS_IOCTL_H) + check_include_file("sys/resource.h" HAVE_SYS_RESOURCE_H) + check_include_file("sys/socket.h" HAVE_SYS_SOCKET_H) + check_include_file("sys/sockio.h" HAVE_SYS_SOCKIO_H) + check_include_file("sys/syscall.h" HAVE_SYS_SYSCALL_H) + check_include_file("sys/time.h" HAVE_SYS_TIME_H) + check_include_file("sys/un.h" HAVE_SYS_UN_H) + check_include_file("unistd.h" HAVE_UNISTD_H) + + if (NOT HAVE_NET_IF_H) # This is the case of OpenBSD + unset(HAVE_NET_IF_H CACHE) + check_include_files("sys/socket.h;net/if.h" HAVE_NET_IF_H) + endif() + + if (NOT HAVE_NETINET_TCP_H) # This is the case of OpenBSD + unset(HAVE_NETINET_TCP_H CACHE) + check_include_files("sys/socket.h;netinet/tcp.h" HAVE_NETINET_TCP_H) + endif() + + if (NOT EXISTS ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h) + file(WRITE ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h.cmake " +#cmakedefine HAVE_NET_IF_H \@HAVE_NET_IF_H\@ +#cmakedefine HAVE_NET_IF_DL_H \@HAVE_NET_IF_DL_H\@ +#cmakedefine HAVE_NETINET_IN_H \@HAVE_NETINET_IN_H\@ +#cmakedefine HAVE_STDLIB_H \@HAVE_STDLIB_H \@ +#cmakedefine HAVE_SYS_FILE_H \@HAVE_SYS_FILE_H\@ +#cmakedefine HAVE_SYS_IOCTL_H \@HAVE_SYS_IOCTL_H\@ +#cmakedefine HAVE_SYS_RESOURCE_H \@HAVE_SYS_RESOURCE_H\@ +#cmakedefine HAVE_SYS_SOCKET_H \@HAVE_SYS_SOCKET_H\@ +#cmakedefine HAVE_SYS_SOCKIO_H \@HAVE_SYS_SOCKIO_H\@ +#cmakedefine HAVE_SYS_SYSCALL_H \@HAVE_SYS_SYSCALL_H\@ +#cmakedefine HAVE_SYS_TIME_H \@HAVE_SYS_TIME_H\@ +#cmakedefine HAVE_SYS_UN_H \@HAVE_SYS_UN_H\@ +#cmakedefine HAVE_UNISTD_H \@HAVE_UNISTD_H\@ +") + endif() + + configure_file( + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h.cmake + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/config.h + ) + + configure_file( + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid.h.in + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid.h + ) + + if (NOT EXISTS ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid_types.h) + file(WRITE + ${E2FSPROGS_SOURCES_DIR}/lib/uuid/uuid_types.h + "#include \n") + endif() + + source_group(ThirdParty\\uuid REGULAR_EXPRESSION ${E2FSPROGS_SOURCES_DIR}/.*) + + else() + CHECK_INCLUDE_FILE(uuid/uuid.h HAVE_UUID_H) + if (NOT HAVE_UUID_H) + message(FATAL_ERROR "Please install uuid-dev, e2fsprogs (OpenBSD) or e2fsprogs-libuuid (FreeBSD)") + endif() + + find_library(LIBUUID uuid + PATHS + /usr/lib + /usr/local/lib + ) + + check_library_exists(${LIBUUID} uuid_generate_random "" HAVE_LIBUUID) + if (NOT HAVE_LIBUUID) + message(FATAL_ERROR "Unable to find the uuid library") + endif() + + link_libraries(${LIBUUID}) + endif() + +endif() diff --git a/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake b/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake new file mode 100644 index 0000000..b4bc001 --- /dev/null +++ b/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake @@ -0,0 +1,36 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources Target) + get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE) + set(PrecompiledBinary "${PrecompiledBasename}_${CMAKE_BUILD_TYPE}_${CMAKE_GENERATOR_PLATFORM}.pch") + + set_source_files_properties(${PrecompiledSource} + PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\"" + OBJECT_OUTPUTS "${PrecompiledBinary}") + + set_source_files_properties(${${Sources}} + PROPERTIES COMPILE_FLAGS "/Yu\"${PrecompiledHeaders}\" /FI\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\"" + OBJECT_DEPENDS "${PrecompiledBinary}") + + set(${Target} ${PrecompiledSource}) +endmacro() diff --git a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt new file mode 100644 index 0000000..69c5bf5 --- /dev/null +++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt @@ -0,0 +1,122 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +# source ~/Downloads/emsdk/emsdk_env.sh +# cmake .. -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/tmp/wasm-install/ +# make install +# cd /tmp/wasm-install +# python -m SimpleHTTPServer 8000 +# firefox http://localhost:8000/ +# -> Copy the result as "../arith.h" + + +cmake_minimum_required(VERSION 2.8.3...4.0) + + +##################################################################### +## Configuration of the Emscripten compiler for WebAssembly target +##################################################################### + +set(WASM_FLAGS "-s WASM=1 -s DISABLE_EXCEPTION_CATCHING=0") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}") + +# Turn on support for debug exceptions +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") + + +##################################################################### +## Prepare DCMTK 3.6.2 +##################################################################### + +include(${CMAKE_SOURCE_DIR}/../../Compiler.cmake) +include(${CMAKE_SOURCE_DIR}/../../DownloadPackage.cmake) + +set(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.2) +set(DCMTK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dcmtk-3.6.2.tar.gz") +set(DCMTK_MD5 "d219a4152772985191c9b89d75302d12") + +if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + +DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}") + +if (FirstRun) + message("Patching file") + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_SOURCE_DIR}/arith.patch + WORKING_DIRECTORY ${DCMTK_SOURCES_DIR}/config/tests + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() +endif() + + +##################################################################### +## Build the DCMTK tests for arithmetics +##################################################################### + +# https://github.com/kripken/emscripten/wiki/WebAssembly#web-server-setup +file(WRITE ${CMAKE_BINARY_DIR}/.htaccess " +AddType application/wasm .wasm +AddOutputFilterByType DEFLATE application/wasm +") + +file(WRITE ${CMAKE_BINARY_DIR}/dcmtk/config/osconfig.h " +#pragma once +#define HAVE_CMATH 1 +#define HAVE_MATH_H 1 +#define HAVE_PROTOTYPE_FINITE 1 +#define HAVE_PROTOTYPE_STD__ISINF 1 +#define HAVE_PROTOTYPE_STD__ISNAN 1 +#define HAVE_STD_NAMESPACE 1 +#define HAVE_STRSTREAM 1 +#define SIZEOF_VOID_P 4 +#define USE_STD_CXX_INCLUDES +") + +include_directories( + ${DCMTK_SOURCES_DIR}/ofstd/include + ${CMAKE_BINARY_DIR} + ) + +add_executable(dcmtk + ${DCMTK_SOURCES_DIR}/config/tests/arith.cc + ${CMAKE_SOURCE_DIR}/Run2.cpp + ) + +install(TARGETS dcmtk DESTINATION .) + +install(FILES + ${CMAKE_BINARY_DIR}/.htaccess + ${CMAKE_BINARY_DIR}/dcmtk.wasm + ${CMAKE_SOURCE_DIR}/app.js + ${CMAKE_SOURCE_DIR}/index.html + DESTINATION . + ) diff --git a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/Run2.cpp b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/Run2.cpp new file mode 100644 index 0000000..0ca6fb9 --- /dev/null +++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/Run2.cpp @@ -0,0 +1,22 @@ +#include +#include + +extern "C" +{ + void EMSCRIPTEN_KEEPALIVE Run2() + { + // This stuff is not properly discovered by DCMTK 3.6.2 configuration scripts + std::cerr << std::endl << std::endl; + std::cerr << "/**" << std::endl; + std::cerr << "#define SIZEOF_CHAR " << sizeof(char) << std::endl; + std::cerr << "#define SIZEOF_DOUBLE " << sizeof(double) << std::endl; + std::cerr << "#define SIZEOF_FLOAT " << sizeof(float) << std::endl; + std::cerr << "#define SIZEOF_INT " << sizeof(int) << std::endl; + std::cerr << "#define SIZEOF_LONG " << sizeof(long) << std::endl; + std::cerr << "#define SIZEOF_SHORT " << sizeof(short) << std::endl; + std::cerr << "#define SIZEOF_VOID_P " << sizeof(void*) << std::endl; + std::cerr << "#define C_CHAR_UNSIGNED " << (!std::is_signed()) << std::endl; + std::cerr << "**/" << std::endl; + std::cerr << std::endl << std::endl; + } +} diff --git a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/app.js b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/app.js new file mode 100644 index 0000000..141a0da --- /dev/null +++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/app.js @@ -0,0 +1,34 @@ +function Initialize() +{ + Module.ccall('Run2', // name of C function + null, // return type + [], // argument types + []); + + Module.ccall('Run', // name of C function + 'number', // return type + [], // argument types + []); +} + + +var Module = { + preRun: [], + postRun: [ Initialize ], + print: function(text) { + console.log(text); + }, + printErr: function(text) { + if (text != 'Calling stub instead of signal()') + { + document.getElementById("stderr").textContent += text + '\n'; + } + }, + totalDependencies: 0 +}; + + +if (!('WebAssembly' in window)) { + alert('Sorry, your browser does not support WebAssembly :('); +} else { +} diff --git a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/arith.patch b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/arith.patch new file mode 100644 index 0000000..0457402 --- /dev/null +++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/arith.patch @@ -0,0 +1,46 @@ +--- /home/jodogne/Subversion/orthanc/Resources/WebAssembly/ArithmeticTests/i/dcmtk-3.6.2/config/tests/arith.cc 2017-07-14 17:41:11.000000000 +0200 ++++ arith.cc 2018-03-28 13:53:34.242234303 +0200 +@@ -19,6 +19,8 @@ + * for being used within oflimits.h. + */ + ++#include ++ + // Note: This depends on some files of ofstd and osconfig.h, + // although it is part of configure testing itself. + // Therefore, ensure osconfig.h has already been generated +@@ -514,7 +516,9 @@ + } + #endif + +-int main( int argc, char** argv ) ++extern "C" ++{ ++int EMSCRIPTEN_KEEPALIVE Run() + { + #ifdef HAVE_WINDOWS_H + // Activate the fallback workaround, it will only be used +@@ -524,6 +528,8 @@ + #endif + + COUT << "Inspecting fundamental arithmetic types... " << OFendl; ++ ++#if 0 + if( argc != 2 ) + { + STD_NAMESPACE cerr << "-- " << "Error: missing destination file " +@@ -532,6 +538,9 @@ + } + + STD_NAMESPACE ofstream out( argv[1] ); ++#else ++ std::ostream& out = std::cerr; ++#endif + + out << "#ifndef CONFIG_ARITH_H" << '\n'; + out << "#define CONFIG_ARITH_H" << '\n'; +@@ -619,3 +628,4 @@ + + return 0; + } ++} diff --git a/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/index.html b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/index.html new file mode 100644 index 0000000..bae8d08 --- /dev/null +++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/index.html @@ -0,0 +1,13 @@ + + + + + + DCMTK - Inspect arithmetic types + + +

+    
+    
+  
+
diff --git a/OrthancFramework/Resources/CMake/WebAssembly/arith.h b/OrthancFramework/Resources/CMake/WebAssembly/arith.h
new file mode 100644
index 0000000..1e7be18
--- /dev/null
+++ b/OrthancFramework/Resources/CMake/WebAssembly/arith.h
@@ -0,0 +1,70 @@
+/**
+#define SIZEOF_CHAR 1
+#define SIZEOF_DOUBLE 8
+#define SIZEOF_FLOAT 4
+#define SIZEOF_INT 4
+#define SIZEOF_LONG 4
+#define SIZEOF_SHORT 2
+#define SIZEOF_VOID_P 4
+#define C_CHAR_UNSIGNED 0
+**/
+
+
+#ifndef CONFIG_ARITH_H
+#define CONFIG_ARITH_H
+
+#define DCMTK_SIGNED_CHAR_DIGITS10 2
+#define DCMTK_UNSIGNED_CHAR_DIGITS10 2
+#define DCMTK_SIGNED_SHORT_DIGITS10 4
+#define DCMTK_UNSIGNED_SHORT_DIGITS10 4
+#define DCMTK_SIGNED_INT_DIGITS10 9
+#define DCMTK_UNSIGNED_INT_DIGITS10 9
+#define DCMTK_SIGNED_LONG_DIGITS10 9
+#define DCMTK_UNSIGNED_LONG_DIGITS10 9
+#define DCMTK_FLOAT_MAX_DIGITS10 9
+#define DCMTK_DOUBLE_MAX_DIGITS10 17
+#define DCMTK_CHAR_TRAPS OFFalse
+#define DCMTK_CHAR_MODULO OFTrue
+#define DCMTK_SIGNED_CHAR_TRAPS OFFalse
+#define DCMTK_SIGNED_CHAR_MODULO OFTrue
+#define DCMTK_UNSIGNED_CHAR_TRAPS OFFalse
+#define DCMTK_UNSIGNED_CHAR_MODULO OFTrue
+#define DCMTK_SIGNED_SHORT_TRAPS OFFalse
+#define DCMTK_SIGNED_SHORT_MODULO OFTrue
+#define DCMTK_UNSIGNED_SHORT_TRAPS OFFalse
+#define DCMTK_UNSIGNED_SHORT_MODULO OFTrue
+#define DCMTK_SIGNED_INT_TRAPS OFFalse
+#define DCMTK_SIGNED_INT_MODULO OFTrue
+#define DCMTK_UNSIGNED_INT_TRAPS OFFalse
+#define DCMTK_UNSIGNED_INT_MODULO OFTrue
+#define DCMTK_SIGNED_LONG_TRAPS OFFalse
+#define DCMTK_SIGNED_LONG_MODULO OFTrue
+#define DCMTK_UNSIGNED_LONG_TRAPS OFFalse
+#define DCMTK_UNSIGNED_LONG_MODULO OFTrue
+#define DCMTK_FLOAT_TRAPS OFFalse
+#define DCMTK_DOUBLE_TRAPS OFFalse
+#define DCMTK_FLOAT_HAS_INFINITY OFTrue
+#define DCMTK_FLOAT_INFINITY *OFreinterpret_cast( const float*, "\000\000\200\177" )
+#define DCMTK_DOUBLE_HAS_INFINITY OFTrue
+#define DCMTK_DOUBLE_INFINITY *OFreinterpret_cast( const double*, "\000\000\000\000\000\000\360\177" )
+#define DCMTK_FLOAT_HAS_QUIET_NAN OFTrue
+#define DCMTK_FLOAT_QUIET_NAN *OFreinterpret_cast( const float*, "\000\000\300\177" )
+#define DCMTK_DOUBLE_HAS_QUIET_NAN OFTrue
+#define DCMTK_DOUBLE_QUIET_NAN *OFreinterpret_cast( const double*, "\000\000\000\000\000\000\370\177" )
+#define DCMTK_FLOAT_HAS_SIGNALING_NAN OFFalse
+#define DCMTK_FLOAT_SIGNALING_NAN *OFreinterpret_cast( const float*, "\001\000\200\177" )
+#define DCMTK_DOUBLE_HAS_SIGNALING_NAN OFFalse
+#define DCMTK_DOUBLE_SIGNALING_NAN *OFreinterpret_cast( const double*, "\001\000\000\000\000\000\360\177" )
+#define DCMTK_FLOAT_IS_IEC559 OFFalse
+#define DCMTK_DOUBLE_IS_IEC559 OFFalse
+#define DCMTK_FLOAT_HAS_DENORM OFdenorm_present
+#define DCMTK_FLOAT_DENORM_MIN *OFreinterpret_cast( const float*, "\001\000\000\000" )
+#define DCMTK_DOUBLE_HAS_DENORM OFdenorm_present
+#define DCMTK_DOUBLE_DENORM_MIN *OFreinterpret_cast( const double*, "\001\000\000\000\000\000\000\000" )
+#define DCMTK_FLOAT_TINYNESS_BEFORE OFFalse
+#define DCMTK_DOUBLE_TINYNESS_BEFORE OFFalse
+#define DCMTK_FLOAT_HAS_DENORM_LOSS OFFalse
+#define DCMTK_DOUBLE_HAS_DENORM_LOSS OFFalse
+#define DCMTK_ROUND_STYLE 1
+
+#endif // CONFIG_ARITH_H
diff --git a/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake b/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake
new file mode 100644
index 0000000..effed4c
--- /dev/null
+++ b/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake
@@ -0,0 +1,73 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_ZLIB)
+  SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.3.1)
+  SET(ZLIB_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/zlib-1.3.1.tar.gz")
+  SET(ZLIB_MD5 "9855b6d802d7fe5b7bd5b196a2271655")
+
+  DownloadPackage(${ZLIB_MD5} ${ZLIB_URL} "${ZLIB_SOURCES_DIR}")
+
+  include_directories(
+    ${ZLIB_SOURCES_DIR}
+    )
+
+  list(APPEND ZLIB_SOURCES 
+    ${ZLIB_SOURCES_DIR}/adler32.c
+    ${ZLIB_SOURCES_DIR}/compress.c
+    ${ZLIB_SOURCES_DIR}/crc32.c 
+    ${ZLIB_SOURCES_DIR}/deflate.c 
+    ${ZLIB_SOURCES_DIR}/infback.c 
+    ${ZLIB_SOURCES_DIR}/inffast.c 
+    ${ZLIB_SOURCES_DIR}/inflate.c 
+    ${ZLIB_SOURCES_DIR}/inftrees.c 
+    ${ZLIB_SOURCES_DIR}/trees.c 
+    ${ZLIB_SOURCES_DIR}/uncompr.c 
+    ${ZLIB_SOURCES_DIR}/zutil.c
+    )
+
+  if (NOT ORTHANC_SANDBOXED)
+    # The source files below require access to the filesystem
+    list(APPEND ZLIB_SOURCES
+      ${ZLIB_SOURCES_DIR}/gzlib.c 
+      ${ZLIB_SOURCES_DIR}/gzclose.c 
+      ${ZLIB_SOURCES_DIR}/gzread.c 
+      ${ZLIB_SOURCES_DIR}/gzwrite.c 
+      )
+  endif()
+
+  source_group(ThirdParty\\zlib REGULAR_EXPRESSION ${ZLIB_SOURCES_DIR}/.*)
+
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    # "ioapi.c" from zlib (minizip) expects the "IOAPI_NO_64" macro to be set to "true"
+    # https://ohse.de/uwe/articles/lfs.html
+    add_definitions(
+      -DIOAPI_NO_64=1
+      )
+  endif()
+
+else()
+  include(FindZLIB)
+  include_directories(${ZLIB_INCLUDE_DIRS})
+  link_libraries(${ZLIB_LIBRARIES})
+endif()
diff --git a/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py b/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py
new file mode 100755
index 0000000..e147471
--- /dev/null
+++ b/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py
@@ -0,0 +1,351 @@
+#!/usr/bin/env python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+##
+## This maintenance script detects all the public methods in the
+## Orthanc framework that come with an inlined implementation in the
+## header file. Such methods can break the ABI of the shared library,
+## as the actual implementation might change over versions.
+##
+
+
+# Ubuntu 20.04:
+# sudo apt-get install python-clang-6.0
+# ./ParseWebAssemblyExports.py --libclang=libclang-6.0.so.1 ./Test.cpp
+
+# Ubuntu 18.04:
+# sudo apt-get install python-clang-4.0
+# ./ParseWebAssemblyExports.py --libclang=libclang-4.0.so.1 ./Test.cpp
+
+# Ubuntu 14.04:
+# ./ParseWebAssemblyExports.py --libclang=libclang-3.6.so.1 ./Test.cpp
+
+
+import os
+import sys
+import clang.cindex
+import argparse
+
+##
+## Parse the command-line arguments
+##
+
+parser = argparse.ArgumentParser(description = 'Parse WebAssembly C++ source file, and create a basic JavaScript wrapper.')
+parser.add_argument('--libclang',
+                    default = '',
+                    help = 'manually provides the path to the libclang shared library')
+parser.add_argument('--target-cpp-size',
+                    default = '',
+                    help = 'where to store C++ source to display the size of each public class')
+
+args = parser.parse_args()
+
+
+if len(args.libclang) != 0:
+    clang.cindex.Config.set_library_file(args.libclang)
+
+index = clang.cindex.Index.create()
+
+
+ROOT = os.path.abspath(os.path.dirname(sys.argv[0]))
+SOURCES = []
+
+for root, dirs, files in os.walk(os.path.join(ROOT, '..', 'Sources')):
+    for name in files:
+        if (os.path.splitext(name)[1] == '.h' and
+            not name.endswith('.impl.h')):
+            SOURCES.append(os.path.join(root, name))
+
+AMALGAMATION = '/tmp/CheckOrthancFrameworkSymbols.cpp'
+            
+with open(AMALGAMATION, 'w') as f:
+    f.write('#include "%s"\n' % os.path.join(ROOT, '..', 'Sources', 'OrthancFramework.h'))
+    for source in SOURCES:
+        f.write('#include "%s"\n' % source)
+            
+
+tu = index.parse(AMALGAMATION, [
+    '--std=c++11',
+    '-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=1',
+    '-DORTHANC_BUILD_UNIT_TESTS=0',
+    '-DORTHANC_ENABLE_BASE64=1',
+    '-DORTHANC_ENABLE_CIVETWEB=1',
+    '-DORTHANC_ENABLE_CURL=1',
+    '-DORTHANC_ENABLE_DCMTK=1',
+    '-DORTHANC_ENABLE_DCMTK_JPEG=1',
+    '-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=1',
+    '-DORTHANC_ENABLE_DCMTK_NETWORKING=1',
+    '-DORTHANC_ENABLE_DCMTK_TRANSCODING=1',
+    '-DORTHANC_ENABLE_JPEG=1',
+    '-DORTHANC_ENABLE_LOCALE=1',
+    '-DORTHANC_ENABLE_LOGGING=1',
+    '-DORTHANC_ENABLE_LOGGING_STDIO=0',
+    '-DORTHANC_ENABLE_LUA=1',
+    '-DORTHANC_ENABLE_MD5=1',
+    '-DORTHANC_ENABLE_MONGOOSE=1',
+    '-DORTHANC_ENABLE_PKCS11=1',
+    '-DORTHANC_ENABLE_PNG=1',
+    '-DORTHANC_ENABLE_PUGIXML=1',
+    '-DORTHANC_ENABLE_SQLITE=1',
+    '-DORTHANC_ENABLE_SSL=1',
+    '-DORTHANC_ENABLE_ZLIB=1',
+    '-DORTHANC_SANDBOXED=0',
+    '-DORTHANC_SQLITE_STANDALONE=0',
+    '-DORTHANC_SQLITE_VERSION=3027001',
+    '-I/usr/include/jsoncpp',  # On Ubuntu 18.04
+    '-I/usr/include/lua5.3',   # On Ubuntu 18.04
+])
+
+
+if len(tu.diagnostics) != 0:
+    for d in tu.diagnostics:
+        print('  ** %s' % d)
+    print('')
+    raise Exception('Error')
+
+
+
+FILES = []
+COUNT = 0
+ALL_TYPES = []
+
+def ReportProblem(message, fqn, cursor):
+    global FILES, COUNT
+    FILES.append(os.path.normpath(str(cursor.location.file)))
+    COUNT += 1
+
+    print('%s: %s::%s()' % (message, '::'.join(fqn), cursor.spelling))
+
+
+def ExploreClass(child, fqn):
+    # Safety check
+    if (child.kind != clang.cindex.CursorKind.CLASS_DECL and
+        child.kind != clang.cindex.CursorKind.STRUCT_DECL):
+        raise Exception()
+
+    # Ignore forward declaration of classes
+    if not child.is_definition():
+        return
+    
+
+    ##
+    ## Verify that the class is publicly exported (its visibility must
+    ## be "default")
+    ##
+    visible = False
+
+    for i in child.get_children():
+        if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
+            i.spelling == 'default'):
+            visible = True
+
+    if not visible:
+        return
+
+    global ALL_TYPES
+    ALL_TYPES.append('::'.join(fqn))
+    
+    
+    ##
+    ## Ignore pure abstract interfaces, by checking the following
+    ## criteria:
+    ##   - It must be a C++ class (not a struct)
+    ##   - It must start with "I"
+    ##   - All its methods must be pure virtual (abstract) and public
+    ##   - Its destructor must be public, virtual, and must do nothing
+    ##
+    
+    if (child.kind == clang.cindex.CursorKind.CLASS_DECL and
+        child.spelling[0] == 'I' and
+        child.spelling[1].isupper()):
+        abstract = True
+        isPublic = False
+
+        for i in child.get_children():
+            if i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR:      # "default"
+                pass
+            elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
+                isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
+            elif i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER:
+                if i.spelling != 'boost::noncopyable':
+                    abstract = False
+            elif isPublic:
+                if i.kind == clang.cindex.CursorKind.CXX_METHOD:
+                    if i.is_pure_virtual_method():
+                        pass  # pure virtual is ok
+                    elif i.is_static_method():
+                        # static method without an inline implementation is ok
+                        for j in i.get_children():
+                            if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                                abstract = False
+                    else:
+                        abstract = False
+                elif (i.kind == clang.cindex.CursorKind.DESTRUCTOR and
+                      i.is_virtual_method()):
+                    # The destructor must be virtual, and must do nothing
+                    c = list(i.get_children())
+                    if (len(c) != 1 or
+                        c[0].kind != clang.cindex.CursorKind.COMPOUND_STMT or
+                        len(list(c[0].get_children())) != 0):
+                        abstract = False
+                elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
+                    ExploreClass(i, fqn + [ i.spelling ])
+                elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or  # Allow "typedef"
+                      i.kind == clang.cindex.CursorKind.ENUM_DECL):      # Allow enums
+                    pass
+                else:
+                    abstract = False
+
+        if abstract:
+            print('Detected a pure interface (this is fine): %s' % ('::'.join(fqn)))
+        else:
+            ReportProblem('Not a pure interface', fqn, child)
+
+        return
+                  
+
+    ##
+    ## We are facing a standard C++ class or struct
+    ##
+    
+    isPublic = (child.kind == clang.cindex.CursorKind.STRUCT_DECL)
+
+    membersCount = 0
+    membersSize = 0
+
+    for i in child.get_children():
+        if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR or    # "default"
+            i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER):  # base class
+            pass
+        
+        elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
+            isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
+
+        elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
+            # This is a subclass
+            if isPublic:
+                ExploreClass(i, fqn + [ i.spelling ])
+
+        elif (i.kind == clang.cindex.CursorKind.CXX_METHOD or
+              i.kind == clang.cindex.CursorKind.CONSTRUCTOR or
+              i.kind == clang.cindex.CursorKind.DESTRUCTOR):
+            if isPublic:
+                hasImplementation = False
+                for j in i.get_children():
+                    if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                        hasImplementation = True
+
+                if hasImplementation:
+                    ReportProblem('Exported public method with an implementation', fqn, i)
+
+        elif i.kind == clang.cindex.CursorKind.VAR_DECL:
+            raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
+
+        elif i.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE:
+            # An inline function template is OK, as it is not added to
+            # a shared library, but compiled by the client of the library
+            if isPublic:
+                print('Detected a template function (this is fine, but avoid it as much as possible): %s' % ('::'.join(fqn + [ i.spelling ])))
+                hasImplementation = False
+                for j in i.get_children():
+                    if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                        hasImplementation = True
+
+                if not hasImplementation:
+                    ReportProblem('Exported template function without an inline implementation', fqn, i)
+
+        elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or  # Allow "typedef"
+              i.kind == clang.cindex.CursorKind.ENUM_DECL):      # Allow enums
+            pass
+
+        elif i.kind == clang.cindex.CursorKind.FRIEND_DECL:
+            children = list(i.get_children())
+            if (isPublic and
+                (len(children) != 1 or
+                 not children[0].displayname in [
+                     # This is supported for ABI compatibility with Orthanc <= 1.8.0
+                     'operator<<(std::ostream &, const Orthanc::DicomTag &)',
+                 ])):
+                raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
+
+        elif i.kind == clang.cindex.CursorKind.FIELD_DECL:
+            # TODO
+            if i.type.get_size() > 0:
+                membersSize += i.type.get_size()
+            membersCount += 1
+            
+        else:
+            if isPublic:
+                raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
+
+    #print('Size of %s => (%d,%d)' % ('::'.join(fqn), membersCount, membersSize))
+
+
+def ExploreNamespace(node, namespace):
+    for child in node.get_children():
+        fqn = namespace + [ child.spelling ]
+        
+        if child.kind == clang.cindex.CursorKind.NAMESPACE:
+            ExploreNamespace(child, fqn)
+
+        elif (child.kind == clang.cindex.CursorKind.CLASS_DECL or
+              child.kind == clang.cindex.CursorKind.STRUCT_DECL):
+            ExploreClass(child, fqn)
+
+        elif child.kind == clang.cindex.CursorKind.FUNCTION_DECL:
+            visible = False
+            hasImplementation = False
+            for i in child.get_children():
+                if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
+                    i.spelling == 'default'):
+                    visible = True
+                elif i.kind == clang.cindex.CursorKind.COMPOUND_STMT:
+                    hasImplementation = True
+
+            if visible and hasImplementation:
+                ReportProblem('Exported public function with an implementation', fqn, i)
+
+
+
+print('')
+
+for node in tu.cursor.get_children():
+    if (node.kind == clang.cindex.CursorKind.NAMESPACE and
+        node.spelling == 'Orthanc'):
+        ExploreNamespace(node, [ 'Orthanc' ])
+
+
+if args.target_cpp_size != '':
+    with open(args.target_cpp_size, 'w') as f:
+        for t in sorted(ALL_TYPES):
+            f.write('  printf("sizeof(::%s) == %%d\\n", static_cast(sizeof(::%s)));\n' % (t, t))
+
+
+print('\nTotal of possibly problematic methods: %d' % COUNT)
+
+print('\nProblematic files:\n')
+for i in sorted(list(set(FILES))):
+    print(i)
+
+print('')
diff --git a/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py b/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py
new file mode 100755
index 0000000..71b9f98
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py
@@ -0,0 +1,51 @@
+#!/usr/bin/python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+import json
+import os
+import re
+import sys
+
+if len(sys.argv) != 2:
+    print('Usage: %s [Path to DCMTK source code]' % sys.argv[0])
+    exit(-1)
+
+
+orthancSyntaxes = []
+    
+
+with open(os.path.join(os.path.dirname(__file__), 'DicomTransferSyntaxes.json'), 'r') as f:
+    for syntax in json.loads(f.read()):
+        orthancSyntaxes.append(syntax['UID'])
+
+
+with open(os.path.join(sys.argv[1], 'dcmdata/include/dcmtk/dcmdata/dcuid.h'), 'r') as f:
+    r = re.compile(r'^#define UID_([A-Za-z0-9_]+)TransferSyntax\s+"([0-9.]+)"$')
+    
+    for line in f.readlines():
+        m = r.match(line)
+        if m != None:
+            syntax = m.group(2)
+            if not syntax in orthancSyntaxes:
+                print('Missing syntax: %s => %s' % (syntax, m.group(1)))
diff --git a/OrthancFramework/Resources/CodeGeneration/DicomTransferSyntaxes.json b/OrthancFramework/Resources/CodeGeneration/DicomTransferSyntaxes.json
new file mode 100644
index 0000000..459d5e0
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/DicomTransferSyntaxes.json
@@ -0,0 +1,376 @@
+[
+  {
+    "UID" : "1.2.840.10008.1.2",
+    "Name" : "Implicit VR Little Endian",
+    "Value" : "LittleEndianImplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_LittleEndianImplicit",
+    "GDCM" : "gdcm::TransferSyntax::ImplicitVRLittleEndian"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.1",
+    "Name" : "Explicit VR Little Endian",
+    "Value" : "LittleEndianExplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_LittleEndianExplicit",
+    "GDCM" : "gdcm::TransferSyntax::ExplicitVRLittleEndian"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.1.99",
+    "Name" : "Deflated Explicit VR Little Endian",
+    "Value" : "DeflatedLittleEndianExplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_DeflatedLittleEndianExplicit"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.2",
+    "Name" : "Explicit VR Big Endian",
+    "Value" : "BigEndianExplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_BigEndianExplicit"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.50",
+    "Name" : "JPEG Baseline (process 1, lossy)",
+    "Value" : "JPEGProcess1",
+    "Retired" : false,
+    "Note" : "Default Transfer Syntax for Lossy JPEG 8-bit Image Compression",
+    "DCMTK" : "EXS_JPEGProcess1",
+    "DCMTK360" : "EXS_JPEGProcess1TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.51",
+    "Name" : "JPEG Extended Sequential (processes 2 & 4)",
+    "Value" : "JPEGProcess2_4",
+    "Retired" : false,
+    "Note" : "Default Transfer Syntax for Lossy JPEG (lossy, 8/12 bit), 12-bit Image Compression (Process 4 only)",
+    "DCMTK" : "EXS_JPEGProcess2_4",
+    "DCMTK360" : "EXS_JPEGProcess2_4TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.52",
+    "Name" : "JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess3_5",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess3_5",
+    "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.53",
+    "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess6_8",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess6_8",
+    "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.54",
+    "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess7_9",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess7_9",
+    "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.55",
+    "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess10_12",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess10_12",
+    "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.56",
+    "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess11_13",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess11_13",
+    "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.57",
+    "Name" : "JPEG Lossless, Nonhierarchical with any selection value (process 14)",
+    "Value" : "JPEGProcess14",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEGProcess14",
+    "DCMTK360" : "EXS_JPEGProcess14TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.58",
+    "Name" : "JPEG Lossless with any selection value, arithmetic coding",
+    "Value" : "JPEGProcess15",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess15",
+    "DCMTK360" : "EXS_JPEGProcess15TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.59",
+    "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess16_18",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess16_18",
+    "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.60",
+    "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess17_19",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess17_19",
+    "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.61",
+    "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess20_22",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess20_22",
+    "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.62",
+    "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess21_23",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess21_23",
+    "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.63",
+    "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess24_26",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess24_26",
+    "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.64",
+    "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess25_27",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess25_27",
+    "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.65",
+    "Name" : "JPEG Lossless, Hierarchical",
+    "Value" : "JPEGProcess28",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess28",
+    "DCMTK360" : "EXS_JPEGProcess28TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.66",
+    "Name" : "JPEG Lossless, Hierarchical, arithmetic coding",
+    "Value" : "JPEGProcess29",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess29",
+    "DCMTK360" : "EXS_JPEGProcess29TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.70",
+    "Name" : "JPEG Lossless, Nonhierarchical, First-Order Prediction (Processes 14 [Selection Value 1])",
+    "Value" : "JPEGProcess14SV1",
+    "Retired" : false,
+    "Note" : "Default Transfer Syntax for Lossless JPEG Image Compression",
+    "DCMTK" : "EXS_JPEGProcess14SV1",
+    "DCMTK360" : "EXS_JPEGProcess14SV1TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.80",
+    "Name" : "JPEG-LS (lossless)",
+    "Value" : "JPEGLSLossless",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEGLSLossless",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLSLossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.81",
+    "Name" : "JPEG-LS (lossy or near-lossless)",
+    "Value" : "JPEGLSLossy",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEGLSLossy",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLSNearLossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.90",
+    "Name" : "JPEG 2000 (lossless)",
+    "Value" : "JPEG2000LosslessOnly",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000LosslessOnly",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Lossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.91",
+    "Name" : "JPEG 2000 (lossless or lossy)",
+    "Value" : "JPEG2000",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.92",
+    "Name" : "JPEG 2000 part 2 multicomponent extensions (lossless)",
+    "Value" : "JPEG2000MulticomponentLosslessOnly",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000MulticomponentLosslessOnly",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2Lossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.93",
+    "Name" : "JPEG 2000 part 2 multicomponent extensions (lossless or lossy)",
+    "Value" : "JPEG2000Multicomponent",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000Multicomponent",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.94",
+    "Name" : "JPIP Referenced",
+    "Value" : "JPIPReferenced",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPIPReferenced"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.95",
+    "Name" : "JPIP Referenced Deflate",
+    "Value" : "JPIPReferencedDeflate",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPIPReferencedDeflate"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.100",
+    "Name" : "MPEG2 Main Profile / Main Level",
+    "Value" : "MPEG2MainProfileAtMainLevel",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.101",
+    "Name" : "MPEG2 Main Profile / High Level",
+    "Value" : "MPEG2MainProfileAtHighLevel",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.102",
+    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.1",
+    "Value" : "MPEG4HighProfileLevel4_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_1",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.103",
+    "Name" : "MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1",
+    "Value" : "MPEG4BDcompatibleHighProfileLevel4_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.104",
+    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video",
+    "Value" : "MPEG4HighProfileLevel4_2_For2DVideo",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.105",
+    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video",
+    "Value" : "MPEG4HighProfileLevel4_2_For3DVideo",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.106",
+    "Name" : "MPEG4 AVC/H.264 Stereo High Profile / Level 4.2",
+    "Value" : "MPEG4StereoHighProfileLevel4_2",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.107",
+    "Name" : "HEVC/H.265 Main Profile / Level 5.1",
+    "Value" : "HEVCMainProfileLevel5_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_HEVCMainProfileLevel5_1",
+    "SinceDCMTK" : "362"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.108",
+    "Name" : "HEVC/H.265 Main 10 Profile / Level 5.1",
+    "Value" : "HEVCMain10ProfileLevel5_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_HEVCMain10ProfileLevel5_1",
+    "SinceDCMTK" : "362"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.5",
+    "Name" : "RLE - Run Length Encoding (lossless)",
+    "Value" : "RLELossless",
+    "Retired" : false,
+    "DCMTK" : "EXS_RLELossless",
+    "GDCM" : "gdcm::TransferSyntax::RLELossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.6.1",
+    "Name" : "RFC 2557 MIME Encapsulation",
+    "Value" : "RFC2557MimeEncapsulation",
+    "Retired" : true
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.6.2",
+    "Name" : "XML Encoding",
+    "Value" : "XML",
+    "Retired" : true
+  }
+]
diff --git a/OrthancFramework/Resources/CodeGeneration/EncodingTests.h b/OrthancFramework/Resources/CodeGeneration/EncodingTests.h
new file mode 100644
index 0000000..8415aa3
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/EncodingTests.h
@@ -0,0 +1,63 @@
+static const unsigned int testEncodingsCount = 18;
+static const ::Orthanc::Encoding testEncodings[] = {
+  ::Orthanc::Encoding_Latin5,
+  ::Orthanc::Encoding_Hebrew,
+  ::Orthanc::Encoding_Greek,
+  ::Orthanc::Encoding_Arabic,
+  ::Orthanc::Encoding_Cyrillic,
+  ::Orthanc::Encoding_Latin4,
+  ::Orthanc::Encoding_Latin3,
+  ::Orthanc::Encoding_Latin2,
+  ::Orthanc::Encoding_Latin1,
+  ::Orthanc::Encoding_Utf8,
+  ::Orthanc::Encoding_Thai,
+  ::Orthanc::Encoding_Japanese,
+  ::Orthanc::Encoding_Ascii,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Chinese,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Windows1251
+};
+static const char *testEncodingsEncoded[18] = {
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xdd",
+  "\x54\x65\x73\x74\xe3",
+  "\x54\x65\x73\x74\xc8",
+  "\x54\x65\x73\x74\xd5",
+  "\x54\x65\x73\x74\xb4\xfb",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf3",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xf8\xa9",
+  "\x54\x65\x73\x74\xe9\xe4\xf6",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0",
+  "\x54\x65\x73\x74\xfb",
+  "\x54\x65\x73\x74\x84\x44\x83\xa6\xc8",
+  "\x54\x65\x73\x74",
+  "\x54\x65\x73\x74\xc4\x9e",
+  "\x81\x30\x89\x37\x81\x30\x89\x38\xA8\xA4\xA8\xA2\x81\x30\x89\x39\x81\x30\x8A\x30",
+  "\xd0\xe5\xed\xf2\xe3\xe5\xed\xee\xe3\xf0\xe0\xf4\xe8\xff",
+  "\xD2\xE0\xE7",
+  "\xcf\xf0\xff\xec\xe0\xff"
+};
+static const char *testEncodingsExpected[18] = {
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\xb0",
+  "\x54\x65\x73\x74\xd7\x93",
+  "\x54\x65\x73\x74\xce\x98",
+  "\x54\x65\x73\x74\xd8\xb5",
+  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc4\xb7",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\x9d\xc4\xb0",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0",
+  "\x54\x65\x73\x74\xe0\xb9\x9b",
+  "\x54\x65\x73\x74\xd0\x94\xce\x98\xef\xbe\x88",
+  "\x54\x65\x73\x74",
+  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
+  "\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3",
+  "\xd0\xa0\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb3\xd0\xb5\xd0\xbd\xd0\xbe\xd0\xb3\xd1\x80\xd0\xb0\xd1\x84\xd0\xb8\xd1\x8f",
+  "\xd0\xa2\xd0\xb0\xd0\xb7",
+  "\xd0\x9f\xd1\x80\xd1\x8f\xd0\xbc\xd0\xb0\xd1\x8f"
+};
+static const char *toUpperSource = "\x67\x72\xc3\xbc\xc3\x9f\x45\x4e\x20\x53\xc3\xa9\x62\x61\x73\x54\x49\x65\x6e\x20\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0";
+static const char *toUpperResult = "\x47\x52\xc3\x9c\xc3\x9f\x45\x4e\x20\x53\xc3\x89\x42\x41\x53\x54\x49\x45\x4e\x20\x54\x45\x53\x54\xc3\x89\xc3\x84\xc3\x96\xc3\x92\xd0\x94\xce\x98\xc4\x9c\xd7\x93\xd8\xb5\xc4\xb6\xd0\x8b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0";
diff --git a/OrthancFramework/Resources/CodeGeneration/EncodingTests.py b/OrthancFramework/Resources/CodeGeneration/EncodingTests.py
new file mode 100755
index 0000000..cb39276
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/EncodingTests.py
@@ -0,0 +1,80 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+source = u'TestéäöòДΘĝדصķћ๛ネİ'
+
+encodings = {
+    'UTF-8' : 'Utf8',
+    'ASCII' : 'Ascii',
+    'ISO-8859-1' : 'Latin1',
+    'ISO-8859-2' : 'Latin2',
+    'ISO-8859-3' : 'Latin3',
+    'ISO-8859-4' : 'Latin4',
+    'ISO-8859-9' : 'Latin5',
+    'ISO-8859-5' : 'Cyrillic',
+    'WINDOWS-1251' : 'Windows1251',
+    'ISO-8859-6' : 'Arabic',
+    'ISO-8859-7' : 'Greek',
+    'ISO-8859-8' : 'Hebrew',
+    'TIS-620' : 'Thai',
+    'SHIFT-JIS' : 'Japanese',
+    #'GB18030' : 'Chinese',  # Done manually below (*)
+}
+
+#from encodings.aliases import aliases
+#for a, b in aliases.iteritems():
+#    print '%s : %s' % (a, b)
+
+
+# "63" corresponds to "?"
+l = []
+encoded = []
+expected = []
+
+def ToArray(source):
+    result = ''
+    for byte in bytearray(source):
+        result += '\\x%02x' % byte
+    return '"%s"' % result
+    
+
+for encoding, orthancEnumeration in encodings.iteritems():
+    l.append('::Orthanc::Encoding_%s' % orthancEnumeration)
+    s = source.encode(encoding, 'ignore')
+    encoded.append(ToArray(s))
+    expected.append(ToArray(s.decode(encoding).encode('utf-8')))
+
+
+# https://en.wikipedia.org/wiki/GB_18030#Technical_details  (*)
+l.append('::Orthanc::Encoding_Chinese')
+expected.append(ToArray('Þßàáâã'))
+encoded.append('"\\x81\\x30\\x89\\x37\\x81\\x30\\x89\\x38\\xA8\\xA4\\xA8\\xA2\\x81\\x30\\x89\\x39\\x81\\x30\\x8A\\x30"')
+
+# Issue 32
+# "encoded" is the copy/paste from "dcm2xml +Ca cyrillic Issue32.dcm"
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xd0\\xe5\\xed\\xf2\\xe3\\xe5\\xed\\xee\\xe3\\xf0\\xe0\\xf4\\xe8\\xff"')
+expected.append(ToArray('Рентгенография'))
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xD2\\xE0\\xE7"')
+expected.append(ToArray('Таз'))
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xcf\\xf0\\xff\\xec\\xe0\\xff"')
+expected.append(ToArray('Прямая'))
+
+
+if True:
+    print 'static const unsigned int testEncodingsCount = %d;' % len(l)
+    print 'static const ::Orthanc::Encoding testEncodings[] = {\n  %s\n};' % (',\n  '.join(l))
+    print 'static const char *testEncodingsEncoded[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(encoded))
+    print 'static const char *testEncodingsExpected[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(expected))
+else:
+    for i in range(len(expected)):
+        print expected[i]
+        #print '%s: %s' % (expected[i], l[i])
+
+
+
+u = (u'grüßEN SébasTIen %s' % source)
+print 'static const char *toUpperSource = %s;' % ToArray(u.encode('utf-8'))
+print 'static const char *toUpperResult = %s;' % ToArray(u.upper().encode('utf-8'))
diff --git a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json
new file mode 100644
index 0000000..2d925b1
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json
@@ -0,0 +1,626 @@
+[
+  /** Generic error codes **/
+
+  {
+    "Code": -1, 
+    "Name": "InternalError", 
+    "Description": "Internal error"
+  }, 
+  {
+    "Code": 0, 
+    "HttpStatus": 200, 
+    "Name": "Success", 
+    "Description": "Success"
+  }, 
+  {
+    "Code": 1, 
+    "Name": "Plugin", 
+    "Description": "Error encountered within the plugin engine"
+  },
+  {
+    "Code": 2, 
+    "Name": "NotImplemented", 
+    "Description": "Not implemented yet"
+  }, 
+  {
+    "Code": 3, 
+    "HttpStatus": 400, 
+    "Name": "ParameterOutOfRange", 
+    "Description": "Parameter out of range",
+    "SQLite": true
+  }, 
+  {
+    "Code": 4, 
+    "Name": "NotEnoughMemory", 
+    "Description": "The server hosting Orthanc is running out of memory"
+  }, 
+  {
+    "Code": 5, 
+    "HttpStatus": 400, 
+    "Name": "BadParameterType", 
+    "Description": "Bad type for a parameter",
+    "SQLite": true
+  }, 
+  {
+    "Code": 6, 
+    "Name": "BadSequenceOfCalls", 
+    "Description": "Bad sequence of calls"
+  }, 
+  {
+    "Code": 7, 
+    "HttpStatus": 404, 
+    "Name": "InexistentItem", 
+    "Description": "Accessing an inexistent item"
+  }, 
+  {
+    "Code": 8, 
+    "HttpStatus": 400, 
+    "Name": "BadRequest", 
+    "Description": "Bad request"
+  }, 
+  {
+    "Code": 9, 
+    "Name": "NetworkProtocol", 
+    "Description": "Error in the network protocol"
+  }, 
+  {
+    "Code": 10, 
+    "Name": "SystemCommand", 
+    "Description": "Error while calling a system command"
+  }, 
+  {
+    "Code": 11, 
+    "Name": "Database", 
+    "Description": "Error with the database engine"
+  }, 
+  {
+    "Code": 12, 
+    "HttpStatus": 400, 
+    "Name": "UriSyntax", 
+    "Description": "Badly formatted URI"
+  }, 
+  {
+    "Code": 13, 
+    "HttpStatus": 404, 
+    "Name": "InexistentFile", 
+    "Description": "Inexistent file"
+  }, 
+  {
+    "Code": 14, 
+    "Name": "CannotWriteFile", 
+    "Description": "Cannot write to file"
+  }, 
+  {
+    "Code": 15, 
+    "HttpStatus": 400, 
+    "Name": "BadFileFormat", 
+    "Description": "Bad file format"
+  }, 
+  {
+    "Code": 16, 
+    "Name": "Timeout", 
+    "Description": "Timeout"
+  }, 
+  {
+    "Code": 17, 
+    "HttpStatus": 404, 
+    "Name": "UnknownResource", 
+    "Description": "Unknown resource"
+  }, 
+  {
+    "Code": 18, 
+    "Name": "IncompatibleDatabaseVersion", 
+    "Description": "Incompatible version of the database"
+  }, 
+  {
+    "Code": 19, 
+    "Name": "FullStorage", 
+    "Description": "The file storage is full"
+  }, 
+  {
+    "Code": 20, 
+    "Name": "CorruptedFile", 
+    "Description": "Corrupted file (e.g. inconsistent MD5 hash)"
+  }, 
+  {
+    "Code": 21, 
+    "HttpStatus": 404, 
+    "Name": "InexistentTag", 
+    "Description": "Inexistent tag"
+  }, 
+  {
+    "Code": 22, 
+    "Name": "ReadOnly", 
+    "Description": "Cannot modify a read-only data structure"
+  }, 
+  {
+    "Code": 23, 
+    "Name": "IncompatibleImageFormat", 
+    "Description": "Incompatible format of the images"
+  }, 
+  {
+    "Code": 24, 
+    "Name": "IncompatibleImageSize", 
+    "Description": "Incompatible size of the images"
+  }, 
+  {
+    "Code": 25, 
+    "Name": "SharedLibrary", 
+    "Description": "Error while using a shared library (plugin)"
+  }, 
+  {
+    "Code": 26, 
+    "Name": "UnknownPluginService", 
+    "Description": "Plugin invoking an unknown service"
+  }, 
+  {
+    "Code": 27, 
+    "Name": "UnknownDicomTag", 
+    "Description": "Unknown DICOM tag"
+  }, 
+  {
+    "Code": 28, 
+    "HttpStatus": 400, 
+    "Name": "BadJson", 
+    "Description": "Cannot parse a JSON document"
+  }, 
+  {
+    "Code": 29, 
+    "HttpStatus": 401, 
+    "Name": "Unauthorized", 
+    "Description": "Bad credentials were provided to an HTTP request"
+  }, 
+  {
+    "Code": 30, 
+    "Name": "BadFont", 
+    "Description": "Badly formatted font file"
+  },
+  {
+    "Code": 31, 
+    "Name": "DatabasePlugin", 
+    "Description": "The plugin implementing a custom database back-end does not fulfill the proper interface"
+  }, 
+  {
+    "Code": 32, 
+    "Name": "StorageAreaPlugin", 
+    "Description": "Error in the plugin implementing a custom storage area"
+  },
+  {
+    "Code": 33,
+    "Name": "EmptyRequest",
+    "Description": "The request is empty"
+  }, 
+  {
+    "Code": 34, 
+    "HttpStatus": 406, 
+    "Name": "NotAcceptable", 
+    "Description": "Cannot send a response which is acceptable according to the Accept HTTP header"
+  }, 
+  {
+    "Code": 35, 
+    "Name": "NullPointer", 
+    "Description": "Cannot handle a NULL pointer"
+  },
+  {
+    "Code": 36, 
+    "HttpStatus": 503, 
+    "Name": "DatabaseUnavailable", 
+    "Description": "The database is currently not available (probably a transient situation)"
+  }, 
+  {
+    "Code": 37, 
+    "Name": "CanceledJob", 
+    "Description": "This job was canceled"
+  }, 
+  {
+    "Code": 38, 
+    "Name": "BadGeometry", 
+    "Description": "Geometry error encountered in Stone"
+  }, 
+  {
+    "Code": 39, 
+    "Name": "SslInitialization", 
+    "Description": "Cannot initialize SSL encryption, check out your certificates"
+  }, 
+  {
+    "Code": 40, 
+    "Name": "DiscontinuedAbi", 
+    "Description": "Calling a function that has been removed from the Orthanc Framework"
+  }, 
+  {
+    "Code": 41, 
+    "HttpStatus" : 416,
+    "Name": "BadRange",
+    "Description": "Incorrect range request"
+  },
+  {
+    "Code": 42,
+    "HttpStatus": 503,
+    "Name": "DatabaseCannotSerialize",
+    "Description": "Database could not serialize access due to concurrent update, the transaction should be retried"
+  }, 
+  {
+    "Code": 43,
+    "HttpStatus": 409,
+    "Name": "Revision",
+    "Description": "A bad revision number was provided, which might indicate conflict between multiple writers"
+  }, 
+  {
+    "Code": 44,
+    "Name": "MainDicomTagsMultiplyDefined",
+    "Description": "A main DICOM Tag has been defined multiple times for the same resource level"
+  }, 
+  {
+    "Code": 45, 
+    "HttpStatus": 403, 
+    "Name": "ForbiddenAccess", 
+    "Description": "Access to a resource is forbidden"
+  }, 
+  {
+    "Code": 46,
+    "HttpStatus": 409,
+    "Name": "DuplicateResource", 
+    "Description": "Duplicate resource"
+  }, 
+  {
+    "Code": 47,
+    "Name": "IncompatibleConfigurations", 
+    "Description": "Your configuration file contains configuration that are mutually incompatible"
+  },
+
+
+
+  /** SQLite **/
+
+
+  {
+    "Code": 1000, 
+    "Name": "SQLiteNotOpened",
+    "Description": "SQLite: The database is not opened",
+    "SQLite": true
+  },
+  {
+    "Code": 1001, 
+    "Name": "SQLiteAlreadyOpened", 
+    "Description": "SQLite: Connection is already open",
+    "SQLite": true
+  },
+  {
+    "Code": 1002, 
+    "Name": "SQLiteCannotOpen", 
+    "Description": "SQLite: Unable to open the database",
+    "SQLite": true
+  },
+  {
+    "Code": 1003, 
+    "Name": "SQLiteStatementAlreadyUsed", 
+    "Description": "SQLite: This cached statement is already being referred to",
+    "SQLite": true
+  },
+  {
+    "Code": 1004, 
+    "Name": "SQLiteExecute", 
+    "Description": "SQLite: Cannot execute a command",
+    "SQLite": true
+  },
+  {
+    "Code": 1005, 
+    "Name": "SQLiteRollbackWithoutTransaction", 
+    "Description": "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)",
+    "SQLite": true
+  },
+  {
+    "Code": 1006, 
+    "Name": "SQLiteCommitWithoutTransaction", 
+    "Description": "SQLite: Committing a nonexistent transaction",
+    "SQLite": true
+  },
+  {
+    "Code": 1007, 
+    "Name": "SQLiteRegisterFunction", 
+    "Description": "SQLite: Unable to register a function",
+    "SQLite": true
+  },
+  {
+    "Code": 1008, 
+    "Name": "SQLiteFlush", 
+    "Description": "SQLite: Unable to flush the database",
+    "SQLite": true
+  },
+  {
+    "Code": 1009, 
+    "Name": "SQLiteCannotRun", 
+    "Description": "SQLite: Cannot run a cached statement",
+    "SQLite": true
+  },
+  {
+    "Code": 1010, 
+    "Name": "SQLiteCannotStep", 
+    "Description": "SQLite: Cannot step over a cached statement",
+    "SQLite": true
+  },
+  {
+    "Code": 1011, 
+    "Name": "SQLiteBindOutOfRange", 
+    "Description": "SQLite: Bind a value while out of range (serious error)",
+    "SQLite": true
+  },
+  {
+    "Code": 1012, 
+    "Name": "SQLitePrepareStatement", 
+    "Description": "SQLite: Cannot prepare a cached statement",
+    "SQLite": true
+  },
+  {
+    "Code": 1013, 
+    "Name": "SQLiteTransactionAlreadyStarted", 
+    "Description": "SQLite: Beginning the same transaction twice",
+    "SQLite": true
+  },
+  {
+    "Code": 1014, 
+    "Name": "SQLiteTransactionCommit", 
+    "Description": "SQLite: Failure when committing the transaction",
+    "SQLite": true
+  },
+  {
+    "Code": 1015, 
+    "Name": "SQLiteTransactionBegin", 
+    "Description": "SQLite: Cannot start a transaction",
+    "SQLite": true
+  },
+
+
+
+  /** Specific error codes **/
+
+  
+  {
+    "Code": 2000, 
+    "Name": "DirectoryOverFile", 
+    "Description": "The directory to be created is already occupied by a regular file"
+  },
+  {
+    "Code": 2001, 
+    "Name": "FileStorageCannotWrite", 
+    "Description": "Unable to create a subdirectory or a file in the file storage"
+  },
+  {
+    "Code": 2002, 
+    "Name": "DirectoryExpected", 
+    "Description": "The specified path does not point to a directory"
+  },
+  {
+    "Code": 2003, 
+    "Name": "HttpPortInUse", 
+    "Description": "The TCP port of the HTTP server is privileged or already in use"
+  },
+  {
+    "Code": 2004, 
+    "Name": "DicomPortInUse", 
+    "Description": "The TCP port of the DICOM server is privileged or already in use"
+  },
+  {
+    "Code": 2005, 
+    "Name": "BadHttpStatusInRest", 
+    "Description": "This HTTP status is not allowed in a REST API"
+  },
+  {
+    "Code": 2006, 
+    "Name": "RegularFileExpected", 
+    "Description": "The specified path does not point to a regular file"
+  },
+  {
+    "Code": 2007, 
+    "Name": "PathToExecutable", 
+    "Description": "Unable to get the path to the executable"
+  },
+  {
+    "Code": 2008, 
+    "Name": "MakeDirectory", 
+    "Description": "Cannot create a directory"
+  },
+  {
+    "Code": 2009, 
+    "Name": "BadApplicationEntityTitle", 
+    "Description": "An application entity title (AET) cannot be empty or be longer than 16 characters"
+  },
+  {
+    "Code": 2010, 
+    "Name": "NoCFindHandler", 
+    "Description": "No request handler factory for DICOM C-FIND SCP"
+  },
+  {
+    "Code": 2011, 
+    "Name": "NoCMoveHandler", 
+    "Description": "No request handler factory for DICOM C-MOVE SCP"
+  },
+  {
+    "Code": 2012, 
+    "Name": "NoCStoreHandler", 
+    "Description": "No request handler factory for DICOM C-STORE SCP"
+  },
+  {
+    "Code": 2013, 
+    "Name": "NoApplicationEntityFilter", 
+    "Description": "No application entity filter"
+  },
+  {
+    "Code": 2014, 
+    "Name": "NoSopClassOrInstance", 
+    "Description": "DicomUserConnection: Unable to find the SOP class and instance"
+  },
+  {
+    "Code": 2015, 
+    "Name": "NoPresentationContext", 
+    "Description": "DicomUserConnection: No acceptable presentation context for modality"
+  },
+  {
+    "Code": 2016, 
+    "Name": "DicomFindUnavailable", 
+    "Description": "DicomUserConnection: The C-FIND command is not supported by the remote SCP"
+  },
+  {
+    "Code": 2017, 
+    "Name": "DicomMoveUnavailable", 
+    "Description": "DicomUserConnection: The C-MOVE command is not supported by the remote SCP"
+  },
+  {
+    "Code": 2018, 
+    "Name": "CannotStoreInstance", 
+    "Description": "Cannot store an instance"
+  },
+  {
+    "Code": 2019,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomNotString", 
+    "Description": "Only string values are supported when creating DICOM instances"
+  },
+  {
+    "Code": 2020,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomOverrideTag", 
+    "Description": "Trying to override a value inherited from a parent module"
+  },
+  {
+    "Code": 2021,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomUseContent", 
+    "Description": "Use \\\"Content\\\" to inject an image into a new DICOM instance"
+  },
+  {
+    "Code": 2022,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomNoPayload", 
+    "Description": "No payload is present for one instance in the series"
+  },
+  {
+    "Code": 2023,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomUseDataUriScheme", 
+    "Description": "The payload of the DICOM instance must be specified according to Data URI scheme"
+  },
+  {
+    "Code": 2024,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomBadParent", 
+    "Description": "Trying to attach a new DICOM instance to an inexistent resource"
+  },
+  {
+    "Code": 2025,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomParentIsInstance", 
+    "Description": "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)"
+  },
+  {
+    "Code": 2026, 
+    "Name": "CreateDicomParentEncoding", 
+    "Description": "Unable to get the encoding of the parent resource"
+  },
+  {
+    "Code": 2027, 
+    "Name": "UnknownModality", 
+    "Description": "Unknown modality"
+  },
+  {
+    "Code": 2028, 
+    "Name": "BadJobOrdering", 
+    "Description": "Bad ordering of filters in a job"
+  },
+  {
+    "Code": 2029, 
+    "Name": "JsonToLuaTable", 
+    "Description": "Cannot convert the given JSON object to a Lua table"
+  },
+  {
+    "Code": 2030, 
+    "Name": "CannotCreateLua", 
+    "Description": "Cannot create the Lua context"
+  },
+  {
+    "Code": 2031, 
+    "Name": "CannotExecuteLua", 
+    "Description": "Cannot execute a Lua command"
+  },
+  {
+    "Code": 2032, 
+    "Name": "LuaAlreadyExecuted", 
+    "Description": "Arguments cannot be pushed after the Lua function is executed"
+  },
+  {
+    "Code": 2033, 
+    "Name": "LuaBadOutput", 
+    "Description": "The Lua function does not give the expected number of outputs"
+  },
+  {
+    "Code": 2034, 
+    "Name": "NotLuaPredicate", 
+    "Description": "The Lua function is not a predicate (only true/false outputs allowed)"
+  },
+  {
+    "Code": 2035, 
+    "Name": "LuaReturnsNoString", 
+    "Description": "The Lua function does not return a string"
+  },
+  {
+    "Code": 2036,
+    "Name": "StorageAreaAlreadyRegistered",
+    "Description": "Another plugin has already registered a custom storage area"
+  },
+  {
+    "Code": 2037,
+    "Name": "DatabaseBackendAlreadyRegistered",
+    "Description": "Another plugin has already registered a custom database back-end"
+  },
+  {
+    "Code": 2038,
+    "Name": "DatabaseNotInitialized",
+    "Description": "Plugin trying to call the database during its initialization"
+  },
+  { 
+    "Code": 2039,
+    "Name": "SslDisabled",
+    "Description": "Orthanc has been built without SSL support"
+  },
+  {
+    "Code": 2040,
+    "Name": "CannotOrderSlices",
+    "Description": "Unable to order the slices of the series"
+  },
+  {
+    "Code": 2041, 
+    "Name": "NoWorklistHandler", 
+    "Description": "No request handler factory for DICOM C-Find Modality SCP"
+  },
+  {
+    "Code": 2042,
+    "Name": "AlreadyExistingTag",
+    "Description": "Cannot override the value of a tag that already exists"
+  },
+  {
+    "Code": 2043, 
+    "Name": "NoStorageCommitmentHandler", 
+    "Description": "No request handler factory for DICOM N-ACTION SCP (storage commitment)"
+  },
+  {
+    "Code": 2044,
+    "Name": "NoCGetHandler", 
+    "Description": "No request handler factory for DICOM C-GET SCP"
+  },
+  {
+    "Code": 2045, 
+    "Name": "DicomGetUnavailable", 
+    "Description": "DicomUserConnection: The C-GET command is not supported by the remote SCP"
+  },
+
+
+
+  /** HTTP-related error codes **/
+
+  {
+    "Code": 3000,
+    "HttpStatus": 415, 
+    "Name": "UnsupportedMediaType",
+    "Description": "Unsupported media type"
+  }
+]
diff --git a/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py
new file mode 100755
index 0000000..95c2345
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py
@@ -0,0 +1,151 @@
+#!/usr/bin/python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+import json
+import os
+import re
+import sys
+
+START_PLUGINS = 1000000
+BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
+
+
+
+## 
+## Read all the available error codes and HTTP status
+##
+
+with open(os.path.join(BASE, 'OrthancFramework', 'Resources', 'CodeGeneration', 'ErrorCodes.json'), 'r') as f:
+    ERRORS = json.loads(re.sub('/\*.*?\*/', '', f.read()))
+
+for error in ERRORS:
+    if error['Code'] >= START_PLUGINS:
+        print('ERROR: Error code must be below %d, but "%s" is set to %d' % (START_PLUGINS, error['Name'], error['Code']))
+        sys.exit(-1)
+
+with open(os.path.join(BASE, 'OrthancFramework', 'Sources', 'Enumerations.h'), 'r') as f:
+    a = f.read()
+
+HTTP = {}
+for i in re.findall('(HttpStatus_([0-9]+)_\w+)', a):
+    HTTP[int(i[1])] = i[0]
+
+
+
+##
+## Generate the "ErrorCode" enumeration in "Enumerations.h"
+##
+
+path = os.path.join(BASE, 'OrthancFramework', 'Sources', 'Enumerations.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = ',\n'.join(map(lambda x: '    ErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
+
+s += ',\n    ErrorCode_START_PLUGINS = %d' % START_PLUGINS
+a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "OrthancPluginErrorCode" enumeration in "OrthancCPlugin.h"
+##
+
+path = os.path.join(BASE, 'OrthancServer', 'Plugins', 'Include', 'orthanc', 'OrthancCPlugin.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = ',\n'.join(map(lambda x: '    OrthancPluginErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
+s += ',\n\n    _OrthancPluginErrorCode_INTERNAL = 0x7fffffff\n  '
+a = re.sub('(typedef enum\s*{)[^}]*?(} OrthancPluginErrorCode;)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "EnumerationToString(ErrorCode)" and
+## "ConvertErrorCodeToHttpStatus(ErrorCode)" functions in
+## "Enumerations.cpp"
+##
+
+path = os.path.join(BASE, 'OrthancFramework', 'Sources', 'Enumerations.cpp')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = '\n\n'.join(map(lambda x: '      case ErrorCode_%s:\n        return "%s";' % (x['Name'], x['Description']), ERRORS))
+a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+           r'\1\n%s\2' % s, a, re.DOTALL)
+
+def GetHttpStatus(x):
+    s = HTTP[x['HttpStatus']]
+    return '      case ErrorCode_%s:\n        return %s;' % (x['Name'], s)
+
+s = '\n\n'.join(map(GetHttpStatus, filter(lambda x: 'HttpStatus' in x, ERRORS)))
+a = re.sub('(ConvertErrorCodeToHttpStatus\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+           r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "ErrorCode" enumeration in "OrthancSQLiteException.h"
+##
+
+path = os.path.join(BASE, 'OrthancFramework', 'Sources', 'SQLite', 'OrthancSQLiteException.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+e = list(filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS))
+s = ',\n'.join(map(lambda x: '      ErrorCode_%s' % x['Name'], e))
+a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+s = '\n\n'.join(map(lambda x: '          case ErrorCode_%s:\n            return "%s";' % (x['Name'], x['Description']), e))
+a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+           r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "PrintErrors" function in "main.cpp"
+##
+
+path = os.path.join(BASE, 'OrthancServer', 'Sources', 'main.cpp')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = '\n'.join(map(lambda x: '    PrintErrorCode(ErrorCode_%s, "%s");' % (x['Name'], x['Description']), ERRORS))
+a = re.sub('(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n  }' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
diff --git a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py
new file mode 100755
index 0000000..0d37428
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py
@@ -0,0 +1,75 @@
+#!/usr/bin/python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+import json
+import os
+import re
+import sys
+import pystache
+
+BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
+
+
+
+## https://www.dicomlibrary.com/dicom/transfer-syntax/
+## https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=EDICOM_transfer_syntax
+
+
+with open(os.path.join(BASE, 'Resources', 'CodeGeneration', 'DicomTransferSyntaxes.json'), 'r') as f:
+    SYNTAXES = json.loads(f.read())
+
+
+
+##
+## Generate the "DicomTransferSyntax" enumeration in "Enumerations.h"
+##
+
+path = os.path.join(BASE, 'Sources', 'Enumerations.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = ',\n'.join(map(lambda x: '    DicomTransferSyntax_%s    /*!< %s */' % (x['Value'], x['Name']), SYNTAXES))
+
+a = re.sub('(enum DicomTransferSyntax\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the implementations
+##
+
+with open(os.path.join(BASE, 'Sources', 'Enumerations_TransferSyntaxes.impl.h'), 'w') as b:
+    with open(os.path.join(BASE, 'Resources', 'CodeGeneration', 'GenerateTransferSyntaxesEnumerations.mustache'), 'r') as a:
+        b.write(pystache.render(a.read(), {
+            'Syntaxes' : SYNTAXES
+        }))
+
+with open(os.path.join(BASE, 'Sources', 'DicomParsing', 'FromDcmtkBridge_TransferSyntaxes.impl.h'), 'w') as b:
+    with open(os.path.join(BASE, 'Resources', 'CodeGeneration', 'GenerateTransferSyntaxesDcmtk.mustache'), 'r') as a:
+        b.write(pystache.render(a.read(), {
+            'Syntaxes' : SYNTAXES
+        }))
diff --git a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache
new file mode 100644
index 0000000..15a7632
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache
@@ -0,0 +1,94 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+// This file is autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                                  DicomTransferSyntax source)
+  {
+    switch (source)
+    {
+      {{#Syntaxes}}
+      {{#DCMTK}}
+      {{#SinceDCMTK}}
+#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
+      {{/SinceDCMTK}}
+      case DicomTransferSyntax_{{Value}}:
+        {{#DCMTK360}}
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = {{DCMTK360}};
+#  else
+        target = {{DCMTK}};
+#  endif
+        {{/DCMTK360}}
+        {{^DCMTK360}}
+        target = {{DCMTK}};
+        {{/DCMTK360}}
+        return true;
+      {{#SinceDCMTK}}
+#endif
+      {{/SinceDCMTK}}
+
+      {{/DCMTK}}
+      {{/Syntaxes}}
+      default:
+        return false;
+    }
+  }
+  
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    E_TransferSyntax source)
+  {
+    switch (source)
+    {
+      {{#Syntaxes}}
+      {{#DCMTK}}
+      {{#SinceDCMTK}}
+#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
+      {{/SinceDCMTK}}
+      {{#DCMTK360}}
+#  if DCMTK_VERSION_NUMBER <= 360
+      case {{DCMTK360}}:
+#  else
+      case {{DCMTK}}:
+#  endif
+      {{/DCMTK360}}
+      {{^DCMTK360}}
+      case {{DCMTK}}:
+      {{/DCMTK360}}
+        target = DicomTransferSyntax_{{Value}};
+        return true;
+      {{#SinceDCMTK}}
+#endif
+      {{/SinceDCMTK}}
+
+      {{/DCMTK}}
+      {{/Syntaxes}}
+      default:
+        return false;
+    }
+  }
+}
diff --git a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache
new file mode 100644
index 0000000..efda2f4
--- /dev/null
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache
@@ -0,0 +1,85 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+// This file is autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      {{#Syntaxes}}
+      case DicomTransferSyntax_{{Value}}:
+        return "{{UID}}";
+
+      {{/Syntaxes}}
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      {{#Syntaxes}}
+      case DicomTransferSyntax_{{Value}}:
+        {{#Retired}}
+        return true;
+        {{/Retired}}
+        {{^Retired}}
+        return false;
+        {{/Retired}}
+
+      {{/Syntaxes}}
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool LookupTransferSyntax(DicomTransferSyntax& target,
+                            const std::string& uid)
+  {
+    {{#Syntaxes}}
+    if (uid == "{{UID}}")
+    {
+      target = DicomTransferSyntax_{{Value}};
+      return true;
+    }
+    
+    {{/Syntaxes}}
+    return false;
+  }
+
+
+  void GetAllDicomTransferSyntaxes(std::set& target)
+  {
+    target.clear();
+    {{#Syntaxes}}
+    target.insert(DicomTransferSyntax_{{Value}});
+    {{/Syntaxes}}
+  }
+}
diff --git a/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt b/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt
new file mode 100644
index 0000000..b0b0c78
--- /dev/null
+++ b/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt
@@ -0,0 +1,52 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../../Resources/Toolchains/LinuxStandardBaseToolchain.cmake -G Ninja
+
+cmake_minimum_required(VERSION 2.8...4.0)
+
+project(DcmtkTools)
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Compiler.cmake)
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/DownloadPackage.cmake)
+
+set(STATIC_BUILD ON CACHE BOOL "")
+set(ALLOW_DOWNLOADS ON CACHE BOOL "")
+
+set(DCMTK_STATIC_VERSION "3.6.5" CACHE STRING "")
+set(ENABLE_DCMTK_JPEG ON CACHE BOOL "")
+set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "")
+set(ENABLE_DCMTK_LOG ON CACHE BOOL "")
+set(ENABLE_DCMTK_NETWORKING ON CACHE BOOL "")
+set(ENABLE_DCMTK_TRANSCODING ON CACHE BOOL "")
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/DcmtkConfiguration.cmake)
+
+add_library(dcmtk STATIC
+  ${CMAKE_SOURCE_DIR}/dummy.cpp
+  ${DCMTK_SOURCES}
+  )
+
+add_executable(getscu
+  ${DCMTK_SOURCES_DIR}/dcmnet/apps/getscu.cc
+  )
+target_link_libraries(getscu dcmtk)
diff --git a/OrthancFramework/Resources/DcmtkTools/dummy.cpp b/OrthancFramework/Resources/DcmtkTools/dummy.cpp
new file mode 100644
index 0000000..8490354
--- /dev/null
+++ b/OrthancFramework/Resources/DcmtkTools/dummy.cpp
@@ -0,0 +1,46 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include 
+
+struct OrthancLinesIterator;
+
+OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content)
+{
+  return NULL;
+}
+
+bool OrthancLinesIterator_GetLine(std::string& target,
+                                  const OrthancLinesIterator* iterator)
+{
+  return false;
+}
+
+void OrthancLinesIterator_Next(OrthancLinesIterator* iterator)
+{
+}
+
+void OrthancLinesIterator_Free(OrthancLinesIterator* iterator)
+{
+}
diff --git a/OrthancFramework/Resources/EmbedResources.py b/OrthancFramework/Resources/EmbedResources.py
new file mode 100755
index 0000000..7d7ff36
--- /dev/null
+++ b/OrthancFramework/Resources/EmbedResources.py
@@ -0,0 +1,446 @@
+#!/usr/bin/python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+import sys
+import os
+import os.path
+import pprint
+import re
+
+UPCASE_CHECK = True
+USE_SYSTEM_EXCEPTION = False
+EXCEPTION_CLASS = 'OrthancException'
+OUT_OF_RANGE_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_ParameterOutOfRange)'
+INEXISTENT_PATH_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_InexistentItem)'
+NAMESPACE = 'Orthanc.EmbeddedResources'
+FRAMEWORK_PATH = None
+
+ARGS = []
+for i in range(len(sys.argv)):
+    if not sys.argv[i].startswith('--'):
+        ARGS.append(sys.argv[i])
+    elif sys.argv[i].lower() == '--no-upcase-check':
+        UPCASE_CHECK = False
+    elif sys.argv[i].lower() == '--system-exception':
+        USE_SYSTEM_EXCEPTION = True
+        EXCEPTION_CLASS = '::std::runtime_error'
+        OUT_OF_RANGE_EXCEPTION = '%s("Parameter out of range")' % EXCEPTION_CLASS
+        INEXISTENT_PATH_EXCEPTION = '%s("Unknown path in a directory resource")' % EXCEPTION_CLASS
+    elif sys.argv[i].startswith('--namespace='):
+        NAMESPACE = sys.argv[i][sys.argv[i].find('=') + 1 : ]
+    elif sys.argv[i].startswith('--framework-path='):
+        FRAMEWORK_PATH = sys.argv[i][sys.argv[i].find('=') + 1 : ]
+
+if len(ARGS) < 2 or len(ARGS) % 2 != 0:
+    print ('Usage:')
+    print ('python %s [--no-upcase-check] [--system-exception] [--namespace=]  [   ]*' % sys.argv[0])
+    exit(-1)
+
+TARGET_BASE_FILENAME = ARGS[1]
+SOURCES = ARGS[2:]
+
+try:
+    # Make sure the destination directory exists
+    os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
+except:
+    pass
+
+
+#####################################################################
+## Read each resource file
+#####################################################################
+
+def CheckNoUpcase(s):
+    global UPCASE_CHECK
+    if (UPCASE_CHECK and
+        re.search('[A-Z]', s) != None):
+        raise Exception("Path in a directory with an upcase letter: %s" % s)
+
+resources = {}
+
+counter = 0
+i = 0
+while i < len(SOURCES):
+    resourceName = SOURCES[i].upper()
+    pathName = SOURCES[i + 1]
+
+    if not os.path.exists(pathName):
+        raise Exception("Non existing path: %s" % pathName)
+
+    if resourceName in resources:
+        raise Exception("Twice the same resource: " + resourceName)
+    
+    if os.path.isdir(pathName):
+        # The resource is a directory: Recursively explore its files
+        content = {}
+        for root, dirs, files in os.walk(pathName):
+            dirs.sort()
+            files.sort()
+            base = os.path.relpath(root, pathName)
+
+            # Fix issue #24 (Build fails on OSX when directory has .DS_Store files):
+            # Ignore folders whose name starts with a dot (".")
+            if base.find('/.') != -1:
+                print('Ignoring folder: %s' % root)
+                continue
+
+            for f in files:
+                if f.find('~') == -1:  # Ignore Emacs backup files
+                    if base == '.':
+                        r = f
+                    else:
+                        r = os.path.join(base, f)
+
+                    CheckNoUpcase(r)
+                    r = '/' + r.replace('\\', '/')
+                    if r in content:
+                        raise Exception("Twice the same filename (check case): " + r)
+
+                    content[r] = {
+                        'Filename' : os.path.join(root, f),
+                        'Index' : counter
+                        }
+                    counter += 1
+
+        resources[resourceName] = {
+            'Type' : 'Directory',
+            'Files' : content
+            }
+
+    elif os.path.isfile(pathName):
+        resources[resourceName] = {
+            'Type' : 'File',
+            'Index' : counter,
+            'Filename' : pathName
+            }
+        counter += 1
+
+    else:
+        raise Exception("Not a regular file, nor a directory: " + pathName)
+
+    i += 2
+
+#pprint.pprint(resources)
+
+
+#####################################################################
+## Write .h header
+#####################################################################
+
+header = open(TARGET_BASE_FILENAME + '.h', 'w')
+
+header.write("""
+#pragma once
+
+#include 
+#include 
+
+#if defined(_MSC_VER)
+#  pragma warning(disable: 4065)  // "Switch statement contains 'default' but no 'case' labels"
+#endif
+
+""")
+
+
+for ns in NAMESPACE.split('.'):
+    header.write('namespace %s {\n' % ns)
+    
+
+header.write("""
+    enum FileResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    enum DirectoryResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    const void* GetFileResourceBuffer(FileResourceId id);
+    size_t GetFileResourceSize(FileResourceId id);
+    void GetFileResource(std::string& result, FileResourceId id);
+
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
+
+    void ListResources(std::list& result, DirectoryResourceId id);
+
+""")
+
+
+for ns in NAMESPACE.split('.'):
+    header.write('}\n')
+
+header.close()
+
+
+
+#####################################################################
+## Write the resource content in the .cpp source
+#####################################################################
+
+PYTHON_MAJOR_VERSION = sys.version_info[0]
+
+def WriteResource(cpp, item):
+    cpp.write('    static const uint8_t resource%dBuffer[] = {' % item['Index'])
+
+    f = open(item['Filename'], "rb")
+    content = f.read()
+    f.close()
+
+    # http://stackoverflow.com/a/1035360
+    pos = 0
+    buffer = []  # instead of appending a few bytes at a time to the cpp file, 
+                 # we first append each chunk to a list, join it and write it 
+                 # to the file.  We've measured that it was 2-3 times faster in python3.
+                 # Note that speed is important since if generation is too slow,
+                 # cmake might try to compile the EmbeddedResources.cpp file while it is
+                 # still being generated !
+    for b in content:
+        if PYTHON_MAJOR_VERSION == 2:
+            c = ord(b[0])
+        else:
+            c = b
+
+        if pos > 0:
+            buffer.append(",")
+
+        if (pos % 16) == 0:
+            buffer.append("\n")
+
+        if c < 0:
+            raise Exception("Internal error")
+
+        buffer.append("0x%02x" % c)
+        pos += 1
+
+    cpp.write("".join(buffer))
+    # Zero-size array are disallowed, so we put one single void character in it.
+    if pos == 0:
+        cpp.write('  0')
+
+    cpp.write('  };\n')
+    cpp.write('    static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
+
+
+cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
+
+cpp.write('#include "%s.h"\n' % os.path.basename(TARGET_BASE_FILENAME))
+
+if USE_SYSTEM_EXCEPTION:
+    cpp.write('#include ')
+elif FRAMEWORK_PATH != None:
+    cpp.write('#include "%s/OrthancException.h"' % FRAMEWORK_PATH)
+else:
+    cpp.write('#include ')
+
+cpp.write("""
+#include 
+#include 
+
+""")
+
+for ns in NAMESPACE.split('.'):
+    cpp.write('namespace %s {\n' % ns)
+
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        WriteResource(cpp, resources[name])
+    else:
+        for f in resources[name]['Files']:
+            WriteResource(cpp, resources[name]['Files'][f])
+
+
+
+#####################################################################
+## Write the accessors to the file resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetFileResourceBuffer(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dBuffer;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw %s;
+      }
+    }
+
+    size_t GetFileResourceSize(FileResourceId id)
+    {
+      switch (id)
+      {
+""" % OUT_OF_RANGE_EXCEPTION)
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dSize;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw %s;
+      }
+    }
+""" % OUT_OF_RANGE_EXCEPTION)
+
+
+
+#####################################################################
+## Write the accessors to the directory resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
+
+cpp.write("""      default:
+        throw %s;
+      }
+    }
+
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""" % OUT_OF_RANGE_EXCEPTION)
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
+
+cpp.write("""      default:
+        throw %s;
+      }
+    }
+""" % OUT_OF_RANGE_EXCEPTION)
+
+
+
+
+#####################################################################
+## List the resources in a directory
+#####################################################################
+
+cpp.write("""
+    void ListResources(std::list& result, DirectoryResourceId id)
+    {
+      result.clear();
+
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        for path in sorted(resources[name]['Files']):
+            cpp.write('        result.push_back("%s");\n' % path)
+        cpp.write('        break;\n\n')
+
+cpp.write("""      default:
+        throw %s;
+      }
+    }
+""" % OUT_OF_RANGE_EXCEPTION)
+
+
+
+
+#####################################################################
+## Write the convenience wrappers in .cpp
+#####################################################################
+
+cpp.write("""
+    void GetFileResource(std::string& result, FileResourceId id)
+    {
+      size_t size = GetFileResourceSize(id);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetFileResourceBuffer(id), size);
+    }
+
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
+    {
+      size_t size = GetDirectoryResourceSize(id, path);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
+    }
+""")
+
+
+for ns in NAMESPACE.split('.'):
+    cpp.write('}\n')
+
+cpp.close()
diff --git a/OrthancFramework/Resources/Patches/OpenSSL-ConfigureHeaders.py b/OrthancFramework/Resources/Patches/OpenSSL-ConfigureHeaders.py
new file mode 100755
index 0000000..b9a207a
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/OpenSSL-ConfigureHeaders.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+import json
+import os
+import re
+import sys
+
+if len(sys.argv) != 2:
+    raise Exception('Bad number of arguments')
+
+
+# This emulates "util/perl/OpenSSL/stackhash.pm"
+
+GENERATE_STACK_MACROS = '''
+SKM_DEFINE_STACK_OF_INTERNAL(${nametype}, ${realtype}, ${plaintype})
+#define sk_${nametype}_num(sk) OPENSSL_sk_num(ossl_check_const_${nametype}_sk_type(sk))
+#define sk_${nametype}_value(sk, idx) ((${realtype} *)OPENSSL_sk_value(ossl_check_const_${nametype}_sk_type(sk), (idx)))
+#define sk_${nametype}_new(cmp) ((STACK_OF(${nametype}) *)OPENSSL_sk_new(ossl_check_${nametype}_compfunc_type(cmp)))
+#define sk_${nametype}_new_null() ((STACK_OF(${nametype}) *)OPENSSL_sk_new_null())
+#define sk_${nametype}_new_reserve(cmp, n) ((STACK_OF(${nametype}) *)OPENSSL_sk_new_reserve(ossl_check_${nametype}_compfunc_type(cmp), (n)))
+#define sk_${nametype}_reserve(sk, n) OPENSSL_sk_reserve(ossl_check_${nametype}_sk_type(sk), (n))
+#define sk_${nametype}_free(sk) OPENSSL_sk_free(ossl_check_${nametype}_sk_type(sk))
+#define sk_${nametype}_zero(sk) OPENSSL_sk_zero(ossl_check_${nametype}_sk_type(sk))
+#define sk_${nametype}_delete(sk, i) ((${realtype} *)OPENSSL_sk_delete(ossl_check_${nametype}_sk_type(sk), (i)))
+#define sk_${nametype}_delete_ptr(sk, ptr) ((${realtype} *)OPENSSL_sk_delete_ptr(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_type(ptr)))
+#define sk_${nametype}_push(sk, ptr) OPENSSL_sk_push(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_type(ptr))
+#define sk_${nametype}_unshift(sk, ptr) OPENSSL_sk_unshift(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_type(ptr))
+#define sk_${nametype}_pop(sk) ((${realtype} *)OPENSSL_sk_pop(ossl_check_${nametype}_sk_type(sk)))
+#define sk_${nametype}_shift(sk) ((${realtype} *)OPENSSL_sk_shift(ossl_check_${nametype}_sk_type(sk)))
+#define sk_${nametype}_pop_free(sk, freefunc) OPENSSL_sk_pop_free(ossl_check_${nametype}_sk_type(sk),ossl_check_${nametype}_freefunc_type(freefunc))
+#define sk_${nametype}_insert(sk, ptr, idx) OPENSSL_sk_insert(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_type(ptr), (idx))
+#define sk_${nametype}_set(sk, idx, ptr) ((${realtype} *)OPENSSL_sk_set(ossl_check_${nametype}_sk_type(sk), (idx), ossl_check_${nametype}_type(ptr)))
+#define sk_${nametype}_find(sk, ptr) OPENSSL_sk_find(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_type(ptr))
+#define sk_${nametype}_find_ex(sk, ptr) OPENSSL_sk_find_ex(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_type(ptr))
+#define sk_${nametype}_find_all(sk, ptr, pnum) OPENSSL_sk_find_all(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_type(ptr), pnum)
+#define sk_${nametype}_sort(sk) OPENSSL_sk_sort(ossl_check_${nametype}_sk_type(sk))
+#define sk_${nametype}_is_sorted(sk) OPENSSL_sk_is_sorted(ossl_check_const_${nametype}_sk_type(sk))
+#define sk_${nametype}_dup(sk) ((STACK_OF(${nametype}) *)OPENSSL_sk_dup(ossl_check_const_${nametype}_sk_type(sk)))
+#define sk_${nametype}_deep_copy(sk, copyfunc, freefunc) ((STACK_OF(${nametype}) *)OPENSSL_sk_deep_copy(ossl_check_const_${nametype}_sk_type(sk), ossl_check_${nametype}_copyfunc_type(copyfunc), ossl_check_${nametype}_freefunc_type(freefunc)))
+#define sk_${nametype}_set_cmp_func(sk, cmp) ((sk_${nametype}_compfunc)OPENSSL_sk_set_cmp_func(ossl_check_${nametype}_sk_type(sk), ossl_check_${nametype}_compfunc_type(cmp)))
+'''
+
+
+GENERATE_LHASH_MACROS = '''
+DEFINE_LHASH_OF_INTERNAL(${type});
+#define lh_${type}_new(hfn, cmp) ((LHASH_OF(${type}) *)OPENSSL_LH_new(ossl_check_${type}_lh_hashfunc_type(hfn), ossl_check_${type}_lh_compfunc_type(cmp)))
+#define lh_${type}_free(lh) OPENSSL_LH_free(ossl_check_${type}_lh_type(lh))
+#define lh_${type}_flush(lh) OPENSSL_LH_flush(ossl_check_${type}_lh_type(lh))
+#define lh_${type}_insert(lh, ptr) ((${type} *)OPENSSL_LH_insert(ossl_check_${type}_lh_type(lh), ossl_check_${type}_lh_plain_type(ptr)))
+#define lh_${type}_delete(lh, ptr) ((${type} *)OPENSSL_LH_delete(ossl_check_${type}_lh_type(lh), ossl_check_const_${type}_lh_plain_type(ptr)))
+#define lh_${type}_retrieve(lh, ptr) ((${type} *)OPENSSL_LH_retrieve(ossl_check_${type}_lh_type(lh), ossl_check_const_${type}_lh_plain_type(ptr)))
+#define lh_${type}_error(lh) OPENSSL_LH_error(ossl_check_${type}_lh_type(lh))
+#define lh_${type}_num_items(lh) OPENSSL_LH_num_items(ossl_check_${type}_lh_type(lh))
+#define lh_${type}_node_stats_bio(lh, out) OPENSSL_LH_node_stats_bio(ossl_check_const_${type}_lh_type(lh), out)
+#define lh_${type}_node_usage_stats_bio(lh, out) OPENSSL_LH_node_usage_stats_bio(ossl_check_const_${type}_lh_type(lh), out)
+#define lh_${type}_stats_bio(lh, out) OPENSSL_LH_stats_bio(ossl_check_const_${type}_lh_type(lh), out)
+#define lh_${type}_get_down_load(lh) OPENSSL_LH_get_down_load(ossl_check_${type}_lh_type(lh))
+#define lh_${type}_set_down_load(lh, dl) OPENSSL_LH_set_down_load(ossl_check_${type}_lh_type(lh), dl)
+#define lh_${type}_doall(lh, dfn) OPENSSL_LH_doall(ossl_check_${type}_lh_type(lh), ossl_check_${type}_lh_doallfunc_type(dfn))
+'''
+
+
+with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                       'OpenSSL-ExtractProvidersOIDs.json'), 'r') as f:
+    OIDS = json.loads(f.read())
+
+
+CURRENT_HEADER = ''
+    
+def Parse(match):
+    s = ''
+    
+    for t in re.findall('generate_stack_macros\("(.+?)"\)', match.group(1)):
+        s += (GENERATE_STACK_MACROS
+              .replace('${nametype}', t)
+              .replace('${realtype}', t)
+              .replace('${plaintype}', t))
+        
+    for t in re.findall('generate_const_stack_macros\("(.+?)"\)', match.group(1)):
+        s += (GENERATE_STACK_MACROS
+              .replace('${nametype}', t)
+              .replace('${realtype}', 'const %s' % t)
+              .replace('${plaintype}', t))
+
+    for t in re.findall('generate_stack_string_macros\(\)', match.group(1)):
+        s += (GENERATE_STACK_MACROS
+              .replace('${nametype}', 'OPENSSL_STRING')
+              .replace('${realtype}', 'char')
+              .replace('${plaintype}', 'char'))
+
+    for t in re.findall('generate_stack_const_string_macros\(\)', match.group(1)):
+        s += (GENERATE_STACK_MACROS
+              .replace('${nametype}', 'OPENSSL_CSTRING')
+              .replace('${realtype}', 'const char')
+              .replace('${plaintype}', 'char'))
+
+    for t in re.findall('generate_stack_block_macros\(\)', match.group(1)):
+        s += (GENERATE_STACK_MACROS
+              .replace('${nametype}', 'OPENSSL_BLOCK')
+              .replace('${realtype}', 'void')
+              .replace('${plaintype}', 'void'))
+        
+    for t in re.findall('generate_lhash_macros\("(.+?)"\)', match.group(1)):
+        s += GENERATE_LHASH_MACROS.replace('${type}', t)
+
+    for t in re.findall('\$config{rc4_int}', match.group(1)):
+        s += 'unsigned int'
+
+    for t in re.findall('oids_to_c::process_leaves\(.+?\)', match.group(1), re.MULTILINE | re.DOTALL):
+        if not CURRENT_HEADER in OIDS:
+            raise Exception('Unknown header: %s' % CURRENT_HEADER)
+
+        for (name, definition) in OIDS[CURRENT_HEADER].items():
+            s += '#define DER_OID_V_%s %s\n' % (name, ', '.join(definition))
+            s += '#define DER_OID_SZ_%s %d\n' % (name, len(definition))
+            s += 'extern const unsigned char ossl_der_oid_%s[DER_OID_SZ_%s];\n\n' % (name, name)
+        
+    return s
+
+
+for base in [ 'include/openssl',
+              'providers/common/include/prov' ]:
+    directory = os.path.join(sys.argv[1], base)
+    for source in os.listdir(directory):
+        if source.endswith('.h.in'):
+            target = re.sub('\.h\.in$', '.h', source)
+                            
+            with open(os.path.join(directory, source), 'r') as f:
+                with open(os.path.join(directory, target), 'w') as g:
+                    CURRENT_HEADER = source
+                    g.write(re.sub('{-(.*?)-}.*?$', Parse, f.read(),
+                                   flags = re.MULTILINE | re.DOTALL))
+
+
+with open(os.path.join(sys.argv[1], 'providers/common/der/orthanc_oids_gen.c'), 'w') as f:
+    for (header, content) in OIDS.items():
+        f.write('#include "prov/%s"\n' % re.sub('\.h\.in$', '.h', header))
+
+    f.write('\n')
+        
+    for (header, content) in OIDS.items():
+        for (name, definition) in content.items():
+            f.write('const unsigned char ossl_der_oid_%s[DER_OID_SZ_%s] = { DER_OID_V_%s };\n' % (
+                name, name, name))
diff --git a/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.json b/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.json
new file mode 100644
index 0000000..b9295a5
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.json
@@ -0,0 +1,1225 @@
+{
+    "der_digests.h.in": {
+        "id_KMACWithSHAKE128": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x13"
+        ], 
+        "id_KMACWithSHAKE256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x14"
+        ], 
+        "id_md2": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x02", 
+            "0x02"
+        ], 
+        "id_md5": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x02", 
+            "0x05"
+        ], 
+        "id_sha1": [
+            "DER_P_OBJECT", 
+            "5", 
+            "0x2B", 
+            "0x0E", 
+            "0x03", 
+            "0x02", 
+            "0x1A"
+        ], 
+        "id_sha224": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x04"
+        ], 
+        "id_sha256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x01"
+        ], 
+        "id_sha384": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x02"
+        ], 
+        "id_sha3_224": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x07"
+        ], 
+        "id_sha3_256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x08"
+        ], 
+        "id_sha3_384": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x09"
+        ], 
+        "id_sha3_512": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x0A"
+        ], 
+        "id_sha512": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x03"
+        ], 
+        "id_sha512_224": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x05"
+        ], 
+        "id_sha512_256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x06"
+        ], 
+        "id_shake128": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x0B"
+        ], 
+        "id_shake128_len": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x11"
+        ], 
+        "id_shake256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x0C"
+        ], 
+        "id_shake256_len": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02", 
+            "0x12"
+        ], 
+        "sigAlgs": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03"
+        ]
+    }, 
+    "der_dsa.h.in": {
+        "id_dsa": [
+            "DER_P_OBJECT", 
+            "7", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x38", 
+            "0x04", 
+            "0x01"
+        ], 
+        "id_dsa_with_sha1": [
+            "DER_P_OBJECT", 
+            "7", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x38", 
+            "0x04", 
+            "0x03"
+        ], 
+        "id_dsa_with_sha224": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x01"
+        ], 
+        "id_dsa_with_sha256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x02"
+        ], 
+        "id_dsa_with_sha384": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x03"
+        ], 
+        "id_dsa_with_sha3_224": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x05"
+        ], 
+        "id_dsa_with_sha3_256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x06"
+        ], 
+        "id_dsa_with_sha3_384": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x07"
+        ], 
+        "id_dsa_with_sha3_512": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x08"
+        ], 
+        "id_dsa_with_sha512": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x04"
+        ]
+    }, 
+    "der_ec.h.in": {
+        "c2onb191v4": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x08"
+        ], 
+        "c2onb191v5": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x09"
+        ], 
+        "c2onb239v4": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x0E"
+        ], 
+        "c2onb239v5": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x0F"
+        ], 
+        "c2pnb163v1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x01"
+        ], 
+        "c2pnb163v2": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x02"
+        ], 
+        "c2pnb163v3": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x03"
+        ], 
+        "c2pnb176w1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x04"
+        ], 
+        "c2pnb208w1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x0A"
+        ], 
+        "c2pnb272w1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x10"
+        ], 
+        "c2pnb304w1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x11"
+        ], 
+        "c2pnb368w1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x13"
+        ], 
+        "c2tnb191v1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x05"
+        ], 
+        "c2tnb191v2": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x06"
+        ], 
+        "c2tnb191v3": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x07"
+        ], 
+        "c2tnb239v1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x0B"
+        ], 
+        "c2tnb239v2": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x0C"
+        ], 
+        "c2tnb239v3": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x0D"
+        ], 
+        "c2tnb359v1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x12"
+        ], 
+        "c2tnb431r1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x00", 
+            "0x14"
+        ], 
+        "ecdsa_with_SHA1": [
+            "DER_P_OBJECT", 
+            "7", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x04", 
+            "0x01"
+        ], 
+        "ecdsa_with_SHA224": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x04", 
+            "0x03", 
+            "0x01"
+        ], 
+        "ecdsa_with_SHA256": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x04", 
+            "0x03", 
+            "0x02"
+        ], 
+        "ecdsa_with_SHA384": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x04", 
+            "0x03", 
+            "0x03"
+        ], 
+        "ecdsa_with_SHA512": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x04", 
+            "0x03", 
+            "0x04"
+        ], 
+        "id_ecPublicKey": [
+            "DER_P_OBJECT", 
+            "7", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x02", 
+            "0x01"
+        ], 
+        "id_ecdsa_with_sha3_224": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x09"
+        ], 
+        "id_ecdsa_with_sha3_256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x0A"
+        ], 
+        "id_ecdsa_with_sha3_384": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x0B"
+        ], 
+        "id_ecdsa_with_sha3_512": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x0C"
+        ], 
+        "prime192v1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x01", 
+            "0x01"
+        ], 
+        "prime192v2": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x01", 
+            "0x02"
+        ], 
+        "prime192v3": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x01", 
+            "0x03"
+        ], 
+        "prime239v1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x01", 
+            "0x04"
+        ], 
+        "prime239v2": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x01", 
+            "0x05"
+        ], 
+        "prime239v3": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x01", 
+            "0x06"
+        ], 
+        "prime256v1": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0xCE", 
+            "0x3D", 
+            "0x03", 
+            "0x01", 
+            "0x07"
+        ]
+    }, 
+    "der_ecx.h.in": {
+        "id_Ed25519": [
+            "DER_P_OBJECT", 
+            "3", 
+            "0x2B", 
+            "0x65", 
+            "0x70"
+        ], 
+        "id_Ed448": [
+            "DER_P_OBJECT", 
+            "3", 
+            "0x2B", 
+            "0x65", 
+            "0x71"
+        ], 
+        "id_X25519": [
+            "DER_P_OBJECT", 
+            "3", 
+            "0x2B", 
+            "0x65", 
+            "0x6E"
+        ], 
+        "id_X448": [
+            "DER_P_OBJECT", 
+            "3", 
+            "0x2B", 
+            "0x65", 
+            "0x6F"
+        ]
+    }, 
+    "der_rsa.h.in": {
+        "hashAlgs": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x02"
+        ], 
+        "id_RSAES_OAEP": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x07"
+        ], 
+        "id_RSASSA_PSS": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x0A"
+        ], 
+        "id_mgf1": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x08"
+        ], 
+        "id_pSpecified": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x09"
+        ], 
+        "id_rsassa_pkcs1_v1_5_with_sha3_224": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x0D"
+        ], 
+        "id_rsassa_pkcs1_v1_5_with_sha3_256": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x0E"
+        ], 
+        "id_rsassa_pkcs1_v1_5_with_sha3_384": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x0F"
+        ], 
+        "id_rsassa_pkcs1_v1_5_with_sha3_512": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x03", 
+            "0x10"
+        ], 
+        "md2WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x02"
+        ], 
+        "md4WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x03"
+        ], 
+        "md5WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x04"
+        ], 
+        "mdc2WithRSASignature": [
+            "DER_P_OBJECT", 
+            "5", 
+            "0x2B", 
+            "0x0E", 
+            "0x03", 
+            "0x02", 
+            "0x0E"
+        ], 
+        "ripemd160WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "6", 
+            "0x2B", 
+            "0x24", 
+            "0x03", 
+            "0x03", 
+            "0x01", 
+            "0x02"
+        ], 
+        "rsaEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x01"
+        ], 
+        "sha1WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x05"
+        ], 
+        "sha224WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x0E"
+        ], 
+        "sha256WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x0B"
+        ], 
+        "sha384WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x0C"
+        ], 
+        "sha512WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x0D"
+        ], 
+        "sha512_224WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x0F"
+        ], 
+        "sha512_256WithRSAEncryption": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x01", 
+            "0x10"
+        ]
+    }, 
+    "der_sm2.h.in": {
+        "curveSM2": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x81", 
+            "0x1C", 
+            "0xCF", 
+            "0x55", 
+            "0x01", 
+            "0x82", 
+            "0x2D"
+        ], 
+        "sm2_with_SM3": [
+            "DER_P_OBJECT", 
+            "8", 
+            "0x2A", 
+            "0x81", 
+            "0x1C", 
+            "0xCF", 
+            "0x55", 
+            "0x01", 
+            "0x83", 
+            "0x75"
+        ]
+    }, 
+    "der_wrap.h.in": {
+        "id_aes128_wrap": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x01", 
+            "0x05"
+        ], 
+        "id_aes192_wrap": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x01", 
+            "0x19"
+        ], 
+        "id_aes256_wrap": [
+            "DER_P_OBJECT", 
+            "9", 
+            "0x60", 
+            "0x86", 
+            "0x48", 
+            "0x01", 
+            "0x65", 
+            "0x03", 
+            "0x04", 
+            "0x01", 
+            "0x2D"
+        ], 
+        "id_alg_CMS3DESwrap": [
+            "DER_P_OBJECT", 
+            "11", 
+            "0x2A", 
+            "0x86", 
+            "0x48", 
+            "0x86", 
+            "0xF7", 
+            "0x0D", 
+            "0x01", 
+            "0x09", 
+            "0x10", 
+            "0x03", 
+            "0x06"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.py b/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.py
new file mode 100755
index 0000000..5a260ea
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/OpenSSL-ExtractProvidersOIDs.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+##
+## This is a maintenance script to automatically extract the OIDs
+## generated from the ".asn1" files by the OpenSSL configuration
+## script "./Configure". This script generates the file
+## "OpenSSL-ExtractProvidersOIDs.json". The output JSON is then used
+## by "OpenSSL-ConfigureHeaders.py".
+##
+
+
+import json
+import os
+import re
+import sys
+
+if len(sys.argv) != 2:
+    raise Exception('Provide the path to your configured OpenSSL 3.x build directory')
+
+BASE = os.path.join(sys.argv[1], 'providers/common/include/prov')
+TARGET = 'OpenSSL-ExtractProvidersOIDs.json'
+RESULT = {}
+
+
+for source in os.listdir(BASE):
+    if source.endswith('.h.in'):
+        path = os.path.join(BASE, re.sub('.in$', '', source))
+
+        content = {}
+        
+        with open(path, 'r') as f:
+            for definition in re.findall('#define (DER_OID_V_.+?)#define (DER_OID_SZ_.+?)extern const(.+?)$', f.read(), re.MULTILINE | re.DOTALL):
+                oid = definition[0].strip().split(' ')
+                
+                name = oid[0].replace('DER_OID_V_', '')
+                oid = oid[1:]
+
+                sizes = definition[1].strip().split(' ')
+                if (name in content or
+                    len(sizes) != 2 or
+                    sizes[0] != 'DER_OID_SZ_%s' % name or
+                    int(sizes[1]) != len(oid)):
+                    raise Exception('Cannot parse %s, for OID %s' % (path, name))
+
+                content[name] = list(map(lambda x: x.replace(',', ''), oid))
+
+        RESULT[source] = content
+
+
+with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), TARGET), 'w') as f:
+    f.write(json.dumps(RESULT, sort_keys = True, indent = 4))
diff --git a/OrthancFramework/Resources/Patches/boost-1.69.0-linux-standard-base.patch b/OrthancFramework/Resources/Patches/boost-1.69.0-linux-standard-base.patch
new file mode 100644
index 0000000..7a640bd
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/boost-1.69.0-linux-standard-base.patch
@@ -0,0 +1,136 @@
+diff -urEb boost_1_69_0.orig/boost/move/adl_move_swap.hpp boost_1_69_0/boost/move/adl_move_swap.hpp
+--- boost_1_69_0.orig/boost/move/adl_move_swap.hpp	2019-02-22 15:05:32.682359994 +0100
++++ boost_1_69_0/boost/move/adl_move_swap.hpp	2019-02-22 15:05:48.426358034 +0100
+@@ -28,6 +28,8 @@
+ //Try to avoid including , as it's quite big
+ #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
+    #include    //Dinkum libraries define std::swap in utility which is lighter than algorithm
++#elif defined(__LSB_VERSION__)
++#  include 
+ #elif defined(BOOST_GNU_STDLIB)
+    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
+    //use the good old stl_algobase header, which is quite lightweight
+diff -urEb boost_1_69_0.orig/boost/system/detail/system_category_win32.hpp boost_1_69_0/boost/system/detail/system_category_win32.hpp
+--- boost_1_69_0.orig/boost/system/detail/system_category_win32.hpp	2019-02-22 15:05:32.722359989 +0100
++++ boost_1_69_0/boost/system/detail/system_category_win32.hpp	2019-02-22 15:06:31.922352713 +0100
+@@ -26,7 +26,7 @@
+ namespace detail
+ {
+ 
+-#if ( defined(_MSC_VER) && _MSC_VER < 1900 ) || ( defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) )
++#if ( defined(_MSC_VER) && _MSC_VER < 1900 ) || ( defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) ) || 1  /* std::snprintf() does not seem to exist on Visual Studio 2015 */
+ 
+ inline char const * unknown_message_win32( int ev, char * buffer, std::size_t len )
+ {
+diff -urEb boost_1_69_0.orig/boost/thread/detail/config.hpp boost_1_69_0/boost/thread/detail/config.hpp
+--- boost_1_69_0.orig/boost/thread/detail/config.hpp	2019-02-22 15:05:32.598360004 +0100
++++ boost_1_69_0/boost/thread/detail/config.hpp	2019-02-22 15:05:48.426358034 +0100
+@@ -418,7 +418,7 @@
+   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+ #elif defined(BOOST_THREAD_CHRONO_MAC_API)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+-#elif defined(__ANDROID__)
++#elif defined(__ANDROID__) || defined(__LSB_VERSION__)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+   #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21
+     #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+diff -urEb boost_1_69_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_69_0/boost/type_traits/detail/has_postfix_operator.hpp
+--- boost_1_69_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2019-02-22 15:05:32.650359998 +0100
++++ boost_1_69_0/boost/type_traits/detail/has_postfix_operator.hpp	2019-02-22 15:05:48.426358034 +0100
+@@ -85,8 +85,11 @@
+ namespace boost {
+ namespace detail {
+ 
++// https://stackoverflow.com/a/15474269
++#ifndef Q_MOC_RUN
+ // This namespace ensures that argument-dependent name lookup does not mess things up.
+ namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
++#endif
+ 
+ // 1. a function to have an instance of type T without requiring T to be default
+ // constructible
+@@ -234,7 +237,9 @@
+    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
+ };
+ 
++#ifndef Q_MOC_RUN
+ } // namespace impl
++#endif
+ } // namespace detail
+ 
+ // this is the accessible definition of the trait to end user
+diff -urEb boost_1_69_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_69_0/boost/type_traits/detail/has_prefix_operator.hpp
+--- boost_1_69_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2019-02-22 15:05:32.650359998 +0100
++++ boost_1_69_0/boost/type_traits/detail/has_prefix_operator.hpp	2019-02-22 15:05:48.426358034 +0100
+@@ -114,8 +114,11 @@
+ namespace boost {
+ namespace detail {
+ 
++// https://stackoverflow.com/a/15474269
++#ifndef Q_MOC_RUN
+ // This namespace ensures that argument-dependent name lookup does not mess things up.
+ namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
++#endif
+ 
+ // 1. a function to have an instance of type T without requiring T to be default
+ // constructible
+@@ -263,7 +266,9 @@
+    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
+ };
+ 
++#ifndef Q_MOC_RUN
+ } // namespace impl
++#endif
+ } // namespace detail
+ 
+ // this is the accessible definition of the trait to end user
+diff -urEb boost_1_69_0.orig/libs/filesystem/src/operations.cpp boost_1_69_0/libs/filesystem/src/operations.cpp
+--- boost_1_69_0.orig/libs/filesystem/src/operations.cpp	2019-02-22 15:05:32.566360008 +0100
++++ boost_1_69_0/libs/filesystem/src/operations.cpp	2019-02-22 18:04:17.346573047 +0100
+@@ -2111,9 +2111,16 @@
+     std::size_t path_size (0);  // initialization quiets gcc warning (ticket #3509)
+     error_code ec = path_max(path_size);
+     if (ec)return ec;
+-    dirent de;
+-    buffer = std::malloc((sizeof(dirent) - sizeof(de.d_name))
+-      +  path_size + 1); // + 1 for "/0"
++
++    // Fixed possible use of uninitialized dirent::d_type in dir_iterator
++    // https://github.com/boostorg/filesystem/commit/bbe9d1771e5d679b3f10c42a58fc81f7e8c024a9
++    const std::size_t buffer_size = (sizeof(dirent) - sizeof(dirent().d_name))
++      +  path_size + 1; // + 1 for "\0"
++    buffer = std::malloc(buffer_size);
++    if (BOOST_UNLIKELY(!buffer))
++      return make_error_code(boost::system::errc::not_enough_memory);
++    std::memset(buffer, 0, buffer_size);
++    
+     return ok;
+   }  
+ 
+@@ -2142,6 +2149,13 @@
+     *result = 0;
+     if ((p = ::readdir(dirp))== 0)
+       return errno;
++
++    // Fixed possible use of uninitialized dirent::d_type in dir_iterator
++    // https://github.com/boostorg/filesystem/commit/bbe9d1771e5d679b3f10c42a58fc81f7e8c024a9    
++#   ifdef BOOST_FILESYSTEM_STATUS_CACHE
++    entry->d_type = p->d_type;
++#   endif
++
+     std::strcpy(entry->d_name, p->d_name);
+     *result = entry;
+     return 0;
+
+diff -urEb boost_1_69_0.orig/boost/thread/pthread/thread_data.hpp boost_1_69_0/boost/thread/pthread/thread_data.hpp 
+--- boost_1_69_0.orig/boost/thread/pthread/thread_data.hpp      2022-08-11 07:26:14.343376000 +0000
++++ boost_1_69_0/boost/thread/pthread/thread_data.hpp   2022-08-11 07:27:21.009862000 +0000
+@@ -57,7 +57,7 @@
+ #else
+           std::size_t page_size = ::sysconf( _SC_PAGESIZE);
+ #endif
+-#if PTHREAD_STACK_MIN > 0
++#ifdef PTHREAD_STACK_MIN
+           if (size
+ #include 
+ #include 
+-#include 
+-#include 
++#if !defined(__EMSCRIPTEN__)
++#  include 
++#  include 
++#endif
+ #include 
+ 
+ namespace boost { namespace locale {
+@@ -400,6 +402,7 @@
+         return impl_->get_option(abstract_calendar::is_dst) != 0;
+     }
+ 
++#if !defined(__EMSCRIPTEN__)
+     namespace time_zone {
+         boost::mutex& tz_mutex()
+         {
+@@ -422,6 +425,7 @@
+             return boost::exchange(tz_id(), new_id);
+         }
+     } // namespace time_zone
++#endif
+ 
+ }} // namespace boost::locale
+ 
+diff -urEb boost_1_85_0.orig/libs/locale/src/boost/locale/shared/generator.cpp boost_1_85_0/libs/locale/src/boost/locale/shared/generator.cpp
+--- boost_1_85_0.orig/libs/locale/src/boost/locale/shared/generator.cpp	2024-05-16 20:54:25.516816710 +0200
++++ boost_1_85_0/libs/locale/src/boost/locale/shared/generator.cpp	2024-05-16 20:56:20.231509636 +0200
+@@ -7,8 +7,10 @@
+ #include 
+ #include 
+ #include 
+-#include 
+-#include 
++#if !defined(__EMSCRIPTEN__)
++#  include 
++#  include 
++#endif
+ #include 
+ #include 
+ #include 
+@@ -21,7 +23,9 @@
+         {}
+ 
+         mutable std::map cached;
++#if !defined(__EMSCRIPTEN__)
+         mutable boost::mutex cached_lock;
++#endif
+ 
+         category_t cats;
+         char_facet_t chars;
+@@ -101,7 +105,9 @@
+     std::locale generator::generate(const std::locale& base, const std::string& id) const
+     {
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p != d->cached.end())
+                 return p->second;
+@@ -126,7 +132,9 @@
+                 result = backend->install(result, facet, char_facet_t::nochar);
+         }
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p == d->cached.end())
+                 d->cached[id] = result;
+diff -urEb boost_1_85_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp boost_1_85_0/libs/locale/src/boost/locale/shared/localization_backend.cpp
+--- boost_1_85_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-05-16 20:54:25.516816710 +0200
++++ boost_1_85_0/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-05-16 20:56:58.823070064 +0200
+@@ -5,8 +5,10 @@
+ // https://www.boost.org/LICENSE_1_0.txt
+ 
+ #include 
+-#include 
+-#include 
++#if !defined(__EMSCRIPTEN__)
++#  include 
++#  include 
++#endif
+ #include 
+ #include 
+ #include 
+@@ -211,11 +213,13 @@
+             return mgr;
+         }
+ 
++#if !defined(__EMSCRIPTEN__)
+         boost::mutex& localization_backend_manager_mutex()
+         {
+             static boost::mutex the_mutex;
+             return the_mutex;
+         }
++#endif
+         localization_backend_manager& localization_backend_manager_global()
+         {
+             static localization_backend_manager the_manager = make_default_backend_mgr();
+@@ -225,12 +229,16 @@
+ 
+     localization_backend_manager localization_backend_manager::global()
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock lock(localization_backend_manager_mutex());
++#endif
+         return localization_backend_manager_global();
+     }
+     localization_backend_manager localization_backend_manager::global(const localization_backend_manager& in)
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock lock(localization_backend_manager_mutex());
++#endif
+         return exchange(localization_backend_manager_global(), in);
+     }
+ 
diff --git a/OrthancFramework/Resources/Patches/boost-1.86.0-emscripten.patch b/OrthancFramework/Resources/Patches/boost-1.86.0-emscripten.patch
new file mode 100644
index 0000000..9505d4c
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/boost-1.86.0-emscripten.patch
@@ -0,0 +1,127 @@
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/date_time.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/date_time.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/date_time.cpp	2024-09-25 15:58:51.306131987 +0200
+@@ -12,8 +12,10 @@
+ #include 
+ #include 
+ #include 
+-#include 
+-#include 
++#if !defined(__EMSCRIPTEN__)
++#  include 
++#  include 
++#endif
+ #include 
+ 
+ namespace boost { namespace locale {
+@@ -400,6 +402,7 @@
+         return impl_->get_option(abstract_calendar::is_dst) != 0;
+     }
+ 
++#if !defined(__EMSCRIPTEN__)
+     namespace time_zone {
+         boost::mutex& tz_mutex()
+         {
+@@ -422,7 +425,7 @@
+             return boost::exchange(tz_id(), new_id);
+         }
+     } // namespace time_zone
+-
++#endif
+ }} // namespace boost::locale
+ 
+ // boostinspect:nominmax
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/generator.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/generator.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/generator.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/generator.cpp	2024-09-25 16:00:07.756233916 +0200
+@@ -7,8 +7,10 @@
+ #include 
+ #include 
+ #include 
+-#include 
+-#include 
++#if !defined(__EMSCRIPTEN__)
++#  include 
++#  include 
++#endif
+ #include 
+ #include 
+ #include 
+@@ -21,8 +23,9 @@
+         {}
+ 
+         mutable std::map cached;
++#if !defined(__EMSCRIPTEN__)
+         mutable boost::mutex cached_lock;
+-
++#endif
+         category_t cats;
+         char_facet_t chars;
+ 
+@@ -101,7 +104,9 @@
+     std::locale generator::generate(const std::locale& base, const std::string& id) const
+     {
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p != d->cached.end())
+                 return p->second;
+@@ -126,7 +131,9 @@
+                 result = backend->install(result, facet, char_facet_t::nochar);
+         }
+         if(d->caching_enabled) {
++#if !defined(__EMSCRIPTEN__)
+             boost::unique_lock guard(d->cached_lock);
++#endif
+             const auto p = d->cached.find(id);
+             if(p == d->cached.end())
+                 d->cached[id] = result;
+diff -urEb boost_1_86_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp boost_1_86_0/libs/locale/src/boost/locale/shared/localization_backend.cpp
+--- boost_1_86_0.orig/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-09-25 15:46:01.000000000 +0200
++++ boost_1_86_0/libs/locale/src/boost/locale/shared/localization_backend.cpp	2024-09-25 16:01:09.196820495 +0200
+@@ -5,8 +5,10 @@
+ // https://www.boost.org/LICENSE_1_0.txt
+ 
+ #include 
+-#include 
+-#include 
++#if !defined(__EMSCRIPTEN__)
++#  include 
++#  include 
++#endif
+ #include 
+ #include 
+ #include 
+@@ -211,11 +213,13 @@
+             return mgr;
+         }
+ 
++#if !defined(__EMSCRIPTEN__)
+         boost::mutex& localization_backend_manager_mutex()
+         {
+             static boost::mutex the_mutex;
+             return the_mutex;
+         }
++#endif
+         localization_backend_manager& localization_backend_manager_global()
+         {
+             static localization_backend_manager the_manager = make_default_backend_mgr();
+@@ -225,12 +229,16 @@
+ 
+     localization_backend_manager localization_backend_manager::global()
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock lock(localization_backend_manager_mutex());
++#endif
+         return localization_backend_manager_global();
+     }
+     localization_backend_manager localization_backend_manager::global(const localization_backend_manager& in)
+     {
++#if !defined(__EMSCRIPTEN__)
+         boost::unique_lock lock(localization_backend_manager_mutex());
++#endif
+         return exchange(localization_backend_manager_global(), in);
+     }
+ 
diff --git a/OrthancFramework/Resources/Patches/civetweb-1.13.patch b/OrthancFramework/Resources/Patches/civetweb-1.13.patch
new file mode 100644
index 0000000..b26f04b
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/civetweb-1.13.patch
@@ -0,0 +1,41 @@
+diff -urEb civetweb-1.13.orig/include/civetweb.h civetweb-1.13/include/civetweb.h
+--- civetweb-1.13.orig/include/civetweb.h	2020-10-27 13:11:13.870113645 +0100
++++ civetweb-1.13/include/civetweb.h	2020-10-27 13:12:33.997986337 +0100
+@@ -1695,6 +1695,9 @@
+                                   struct mg_error_data *error);
+ #endif
+ 
++// Added by SJ
++CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn);
++
+ #ifdef __cplusplus
+ }
+ #endif /* __cplusplus */
+diff -urEb civetweb-1.13.orig/src/civetweb.c civetweb-1.13/src/civetweb.c
+--- civetweb-1.13.orig/src/civetweb.c	2020-10-27 13:11:13.870113645 +0100
++++ civetweb-1.13/src/civetweb.c	2020-10-27 13:12:14.534017414 +0100
+@@ -10876,6 +10876,11 @@
+     /* + MicroSoft extensions
+      * https://msdn.microsoft.com/en-us/library/aa142917.aspx */
+ 
++    /* Added by SJ, for write access to WebDAV on Windows >= 7 */
++    {"LOCK", 1, 1, 0, 0, 0},
++    {"UNLOCK", 1, 0, 0, 0, 0},
++    {"PROPPATCH", 1, 1, 0, 0, 0},
++    
+     /* REPORT method (RFC 3253) */
+     {"REPORT", 1, 1, 1, 1, 1},
+     /* REPORT method only allowed for CGI/Lua/LSP and callbacks. */
+@@ -21287,4 +21292,12 @@
+ }
+ 
+ 
++// Added by SJ
++void mg_disable_keep_alive(struct mg_connection *conn)
++{
++  if (conn != NULL) {
++    conn->must_close = 1;
++  }
++}
++ 
+ /* End of civetweb.c */
diff --git a/OrthancFramework/Resources/Patches/civetweb-1.14.patch b/OrthancFramework/Resources/Patches/civetweb-1.14.patch
new file mode 100644
index 0000000..5f060b2
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/civetweb-1.14.patch
@@ -0,0 +1,43 @@
+diff -urEb civetweb-1.14.orig/src/civetweb.c civetweb-1.14/src/civetweb.c
+--- civetweb-1.14.orig/src/civetweb.c	2023-07-06 15:48:01.163703913 +0200
++++ civetweb-1.14/src/civetweb.c	2023-07-06 15:48:51.207843938 +0200
+@@ -567,7 +567,7 @@
+ #if (_MSC_VER < 1300)
+ #define STRX(x) #x
+ #define STR(x) STRX(x)
+-#define __func__ __FILE__ ":" STR(__LINE__)
++#define __func__ __ORTHANC_FILE__ ":" STR(__LINE__)
+ #define strtoull(x, y, z) ((unsigned __int64)_atoi64(x))
+ #define strtoll(x, y, z) (_atoi64(x))
+ #else
+@@ -1450,14 +1450,14 @@
+ }
+ 
+ 
+-#define mg_malloc(a) mg_malloc_ex(a, NULL, __FILE__, __LINE__)
+-#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __FILE__, __LINE__)
+-#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __FILE__, __LINE__)
+-#define mg_free(a) mg_free_ex(a, __FILE__, __LINE__)
+-
+-#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __FILE__, __LINE__)
+-#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __FILE__, __LINE__)
+-#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __FILE__, __LINE__)
++#define mg_malloc(a) mg_malloc_ex(a, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__)
++#define mg_free(a) mg_free_ex(a, __ORTHANC_FILE__, __LINE__)
++
++#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __ORTHANC_FILE__, __LINE__)
++#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__)
++#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__)
+ 
+ 
+ #else /* USE_SERVER_STATS */
+@@ -1774,6 +1774,7 @@
+ #if !defined(OPENSSL_API_3_0)
+ #define OPENSSL_API_3_0
+ #endif
++#define OPENSSL_REMOVE_THREAD_STATE()
+ #else
+ #if (OPENSSL_VERSION_NUMBER >= 0x10100000L)
+ #if !defined(OPENSSL_API_1_1)
diff --git a/OrthancFramework/Resources/Patches/curl-8.12.1.patch b/OrthancFramework/Resources/Patches/curl-8.12.1.patch
new file mode 100644
index 0000000..caeb5b1
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/curl-8.12.1.patch
@@ -0,0 +1,12 @@
+diff -urEb curl-8.12.1-orig/CMake/Macros.cmake curl-8.12.1/CMake/Macros.cmake
+--- curl-8.12.1-orig/CMake/Macros.cmake 2025-02-13 08:15:00.000000000 +0100
++++ curl-8.12.1/CMake/Macros.cmake      2025-03-27 10:25:42.119275658 +0100
+@@ -50,7 +50,7 @@
+     message(STATUS "Performing Test ${_curl_test}")
+     try_compile(${_curl_test}
+       ${PROJECT_BINARY_DIR}
+-      "${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c"
++      "${CURL_SOURCES_DIR}/CMake/CurlTests.c"
+       CMAKE_FLAGS
+         "-DCOMPILE_DEFINITIONS:STRING=-D${_curl_test} ${CURL_TEST_DEFINES} ${_cmake_required_definitions}"
+         "${_curl_test_add_libraries}"
diff --git a/OrthancFramework/Resources/Patches/curl-8.9.0.patch b/OrthancFramework/Resources/Patches/curl-8.9.0.patch
new file mode 100644
index 0000000..4c4ca0c
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/curl-8.9.0.patch
@@ -0,0 +1,24 @@
+diff -urEb curl-8.9.0.orig/CMake/Macros.cmake curl-8.9.0/CMake/Macros.cmake
+--- curl-8.9.0.orig/CMake/Macros.cmake	2025-02-18 16:04:59.818585107 +0100
++++ curl-8.9.0/CMake/Macros.cmake	2025-02-18 16:05:16.867458366 +0100
+@@ -48,7 +48,7 @@
+     message(STATUS "Performing Test ${CURL_TEST}")
+     try_compile(${CURL_TEST}
+       ${CMAKE_BINARY_DIR}
+-      ${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c
++      ${CURL_SOURCES_DIR}/CMake/CurlTests.c
+       CMAKE_FLAGS -DCOMPILE_DEFINITIONS:STRING=${MACRO_CHECK_FUNCTION_DEFINITIONS}
+       "${CURL_TEST_ADD_LIBRARIES}"
+       OUTPUT_VARIABLE OUTPUT)
+diff -urEb curl-8.9.0.orig/lib/system_win32.c curl-8.9.0/lib/system_win32.c
+--- curl-8.9.0.orig/lib/system_win32.c	2025-02-18 16:04:59.834584988 +0100
++++ curl-8.9.0/lib/system_win32.c	2025-02-18 16:06:26.448941452 +0100
+@@ -273,7 +273,7 @@
+ 
+ bool Curl_win32_impersonating(void)
+ {
+-#ifndef CURL_WINDOWS_APP
++#if !defined(CURL_WINDOWS_APP) && !defined(__MINGW32__)
+   HANDLE token = NULL;
+   if(OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &token)) {
+     CloseHandle(token);
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch
new file mode 100644
index 0000000..321f810
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch
@@ -0,0 +1,29 @@
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dulparse.cc dcmtk-3.6.0/dcmnet/libsrc/dulparse.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dulparse.cc	2010-12-01 09:26:36.000000000 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dulparse.cc	2016-12-02 15:58:49.930540033 +0100
+@@ -393,6 +393,8 @@
+                     return cond;
+ 
+                 buf += length;
++                if (presentationLength < length)
++                  return EC_MemoryExhausted;
+                 presentationLength -= length;
+                 DCMNET_TRACE("Successfully parsed Abstract Syntax");
+                 break;
+@@ -404,12 +406,16 @@
+                 cond = LST_Enqueue(&context->transferSyntaxList, (LST_NODE*)subItem);
+                 if (cond.bad()) return cond;
+                 buf += length;
++                if (presentationLength < length)
++                  return EC_MemoryExhausted;
+                 presentationLength -= length;
+                 DCMNET_TRACE("Successfully parsed Transfer Syntax");
+                 break;
+             default:
+                 cond = parseDummy(buf, &length, presentationLength);
+                 buf += length;
++                if (presentationLength < length)
++                  return EC_MemoryExhausted;
+                 presentationLength -= length;
+                 break;
+             }
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.0-mingw64.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.0-mingw64.patch
new file mode 100644
index 0000000..f6deeae
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.0-mingw64.patch
@@ -0,0 +1,21 @@
+diff -urEb dcmtk-3.6.0.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.0/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.0.orig/ofstd/include/dcmtk/ofstd/offile.h	2010-12-17 11:50:30.000000000 +0100
++++ dcmtk-3.6.0/ofstd/include/dcmtk/ofstd/offile.h	2013-07-19 15:56:25.688996134 +0200
+@@ -196,7 +196,7 @@
+   OFBool popen(const char *command, const char *modes)
+   {
+     if (file_) fclose();
+-#ifdef _WIN32
++#if defined(_WIN32) && !defined(__MINGW64_VERSION_MAJOR)
+     file_ = _popen(command, modes);
+ #else
+     file_ = :: popen(command, modes);
+@@ -258,7 +258,7 @@
+     {
+       if (popened_)
+       {
+-#ifdef _WIN32
++#if defined(_WIN32) && !defined(__MINGW64_VERSION_MAJOR)
+         result = _pclose(file_);
+ #else
+         result = :: pclose(file_);
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.0-speed.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.0-speed.patch
new file mode 100644
index 0000000..45ee396
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.0-speed.patch
@@ -0,0 +1,54 @@
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc dcmtk-3.6.0/dcmnet/libsrc/dul.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc	2017-03-17 15:49:23.043061969 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dul.cc	2017-03-17 15:50:44.075359547 +0100
+@@ -630,7 +630,10 @@
+     if (cond.bad())
+         return cond;
+ 
+-    cond = PRV_NextPDUType(association, block, timeout, &pduType);
++    /* This is the first time we read from this new connection, so in case it
++     * doesn't speak DICOM, we shouldn't wait forever (= DUL_NOBLOCK).
++     */
++    cond = PRV_NextPDUType(association, DUL_NOBLOCK, PRV_DEFAULTTIMEOUT, &pduType);
+ 
+     if (cond == DUL_NETWORKCLOSED)
+         event = TRANS_CONN_CLOSED;
+@@ -1770,7 +1773,7 @@
+                 // send number of socket handle in child process over anonymous pipe
+                 DWORD bytesWritten;
+                 char buf[20];
+-                sprintf(buf, "%i", OFreinterpret_cast(int, childSocketHandle));
++                sprintf(buf, "%i", OFstatic_cast(int, OFreinterpret_cast(size_t, childSocketHandle)));
+                 if (!WriteFile(hChildStdInWriteDup, buf, strlen(buf) + 1, &bytesWritten, NULL))
+                 {
+                     CloseHandle(hChildStdInWriteDup);
+@@ -1780,7 +1783,7 @@
+                 // return OF_ok status code DULC_FORKEDCHILD with descriptive text
+                 OFOStringStream stream;
+                 stream << "New child process started with pid " << OFstatic_cast(int, pi.dwProcessId)
+-                       << ", socketHandle " << OFreinterpret_cast(int, childSocketHandle) << OFStringStream_ends;
++                       << ", socketHandle " << OFstatic_cast(int, OFreinterpret_cast(size_t, childSocketHandle)) << OFStringStream_ends;
+                 OFSTRINGSTREAM_GETOFSTRING(stream, msg)
+                 return makeDcmnetCondition(DULC_FORKEDCHILD, OF_ok, msg.c_str());
+             }
+@@ -1840,7 +1843,7 @@
+     }
+ #endif
+ #endif
+-    setTCPBufferLength(sock);
++    //setTCPBufferLength(sock);
+ 
+ #ifndef DONT_DISABLE_NAGLE_ALGORITHM
+     /*
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc	2017-03-17 15:49:23.043061969 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc	2017-03-17 15:49:48.467144792 +0100
+@@ -2417,7 +2417,7 @@
+           return makeDcmnetCondition(DULC_TCPINITERROR, OF_error, msg.c_str());
+         }
+ #endif
+-        setTCPBufferLength(s);
++        //setTCPBufferLength(s);
+ 
+ #ifndef DONT_DISABLE_NAGLE_ALGORITHM
+         /*
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch
new file mode 100644
index 0000000..cea33ac
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch
@@ -0,0 +1,12 @@
+diff -urEb dcmtk-3.6.2.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.2/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.2.orig/ofstd/include/dcmtk/ofstd/offile.h	2017-07-14 17:41:11.000000000 +0200
++++ dcmtk-3.6.2/ofstd/include/dcmtk/ofstd/offile.h	2018-01-02 13:56:04.075293459 +0100
+@@ -551,7 +551,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.2-private.dic b/OrthancFramework/Resources/Patches/dcmtk-3.6.2-private.dic
new file mode 100644
index 0000000..880753f
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.2-private.dic
@@ -0,0 +1,3040 @@
+#
+#  Copyright (C) 1994-2013, OFFIS e.V.
+#  All rights reserved.  See COPYRIGHT file for details.
+#
+#  This software and supporting documentation were developed by
+#
+#    OFFIS e.V.
+#    R&D Division Health
+#    Escherweg 2
+#    D-26121 Oldenburg, Germany
+#
+#
+#  Module:  dcmdata
+#
+#  Author:  Andrew Hewett, Marco Eichelberg, Joerg Riesmeier
+#
+#  Purpose:
+#  This is the private tag DICOM data dictionary for the dcmtk class library.
+#
+#
+# Dictionary of Private Tags
+#
+#  This dictionary contains the private tags defined in the following
+#  reference documents (in alphabetical order):
+#   - AGFA IMPAX 6.5.x Solution conformance statement
+#   - Circle Cardiovascular Imaging cmr42 3.0 conformance statement
+#   - David Clunie's dicom3tools package, 2002-04-20 snapshot
+#   - Fuji CR console, 3rd release
+#   - Intelerad Medical Systems Inc., Image Server
+#   - OCULUS Pentacam 1.17 conformance statement
+#   - Philips Digital Diagnost 1.3 conformance statement
+#   - Philips Integris H, catheterization laboratory, RIS-interface
+#   - Philips Intera Achieva conformance statement
+#   - Philips MR Achieva conformance statement
+#   - Siemens Somatom syngo VA40B conformance statement
+#   - Siemens AXIOM Artis VB30 conformance statement
+#   - SonoWand Invite 2.1.1 conformance statement
+#   - Swissvision TR4000 conformance statement
+#   - private tags for DCMTK anonymizer tool
+#
+# Each line represents an entry in the data dictionary.  Each line
+# has 5 fields (Tag, VR, Name, VM, Version).  Entries need not be
+# in ascending tag order.
+#
+# Entries may override existing entries.
+#
+# Each field must be separated by a single tab.
+# The tag value may take one of two forms:
+#   (gggg,"CREATOR",ee)
+#   (gggg,"CREATOR",eeee) [eeee >= 1000]
+# The first form describes a private tag that may be used with different
+# element numbers as reserved by the private creator element.
+# The second form describes a private tag that may only occur with a
+# certain fixed element number.
+# In both cases, the tag values must be in hexadecimal.
+# Repeating groups are represented by indicating the range
+# (gggg-o-gggg,"CREATOR",ee) or (gggg-o-gggg,"CREATOR",eeee)
+# where "-o-" indicates that only odd group numbers match the definition.
+# The element part of the tag can also be a range.
+#
+# Comments have a '#' at the beginning of the line.
+#
+# Tag				VR	Name			VM	Version / Description
+#
+(0019,"1.2.840.113681",10)	ST	CRImageParamsCommon	1	PrivateTag
+(0019,"1.2.840.113681",11)	ST	CRImageIPParamsSingle	1	PrivateTag
+(0019,"1.2.840.113681",12)	ST	CRImageIPParamsLeft	1	PrivateTag
+(0019,"1.2.840.113681",13)	ST	CRImageIPParamsRight	1	PrivateTag
+
+(0087,"1.2.840.113708.794.1.1.2.0",10)	CS	MediaType	1	PrivateTag
+(0087,"1.2.840.113708.794.1.1.2.0",20)	CS	MediaLocation	1	PrivateTag
+(0087,"1.2.840.113708.794.1.1.2.0",50)	IS	EstimatedRetrieveTime	1	PrivateTag
+
+(0009,"ACUSON",00)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",01)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",02)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",03)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",04)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",05)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",06)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",07)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",08)	LT	Unknown	1	PrivateTag
+(0009,"ACUSON",09)	LT	Unknown	1	PrivateTag
+(0009,"ACUSON",0a)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0b)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0c)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0d)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0e)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",0f)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",10)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",11)	UN	Unknown	1	PrivateTag
+(0009,"ACUSON",12)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",13)	IS	Unknown	1	PrivateTag
+(0009,"ACUSON",14)	LT	Unknown	1	PrivateTag
+(0009,"ACUSON",15)	UN	Unknown	1	PrivateTag
+
+(0003,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0005,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0009,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0019,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(0029,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+(1369,"AEGIS_DICOM_2.00",00)	US	Unknown	1-n	PrivateTag
+
+(0009,"AGFA",10)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",11)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",13)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",14)	LO	Unknown	1	PrivateTag
+(0009,"AGFA",15)	LO	Unknown	1	PrivateTag
+
+(0031,"AGFA PACS Archive Mirroring 1.0",00)	CS	StudyStatus	1	PrivateTag
+(0031,"AGFA PACS Archive Mirroring 1.0",01)	UL	DateTimeVerified	1	PrivateTag
+
+(0029,"CAMTRONICS IP",10)	LT	Unknown	1	PrivateTag
+(0029,"CAMTRONICS IP",20)	UN	Unknown	1	PrivateTag
+(0029,"CAMTRONICS IP",30)	UN	Unknown	1	PrivateTag
+(0029,"CAMTRONICS IP",40)	UN	Unknown	1	PrivateTag
+
+(0029,"CAMTRONICS",10)	LT	Commentline	1	PrivateTag
+(0029,"CAMTRONICS",20)	DS	EdgeEnhancementCoefficient	1	PrivateTag
+(0029,"CAMTRONICS",50)	LT	SceneText	1	PrivateTag
+(0029,"CAMTRONICS",60)	LT	ImageText	1	PrivateTag
+(0029,"CAMTRONICS",70)	IS	PixelShiftHorizontal	1	PrivateTag
+(0029,"CAMTRONICS",80)	IS	PixelShiftVertical	1	PrivateTag
+(0029,"CAMTRONICS",90)	IS	Unknown	1	PrivateTag
+
+(0009,"CARDIO-D.R. 1.0",00)	UL	FileLocation	1	PrivateTag
+(0009,"CARDIO-D.R. 1.0",01)	UL	FileSize	1	PrivateTag
+(0009,"CARDIO-D.R. 1.0",40)	SQ	AlternateImageSequence	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",00)	CS	ImageBlankingShape	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",02)	IS	ImageBlankingLeftVerticalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",04)	IS	ImageBlankingRightVerticalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",06)	IS	ImageBlankingUpperHorizontalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",08)	IS	ImageBlankingLowerHorizontalEdge	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",10)	IS	CenterOfCircularImageBlanking	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",12)	IS	RadiusOfCircularImageBlanking	1	PrivateTag
+(0019,"CARDIO-D.R. 1.0",30)	UL	MaximumImageFrameSize	1	PrivateTag
+(0021,"CARDIO-D.R. 1.0",13)	IS	ImageSequenceNumber	1	PrivateTag
+(0029,"CARDIO-D.R. 1.0",00)	SQ	EdgeEnhancementSequence	1	PrivateTag
+(0029,"CARDIO-D.R. 1.0",01)	US	ConvolutionKernelSize	2	PrivateTag
+(0029,"CARDIO-D.R. 1.0",02)	DS	ConvolutionKernelCoefficients	1-n	PrivateTag
+(0029,"CARDIO-D.R. 1.0",03)	DS	EdgeEnhancementGain	1	PrivateTag
+
+(0025,"CMR42 CIRCLECVI",1010)	LO	WorkspaceID	1	PrivateTag
+(0025,"CMR42 CIRCLECVI",1020)	LO	WorkspaceTimeString	1	PrivateTag
+(0025,"CMR42 CIRCLECVI",1030)	OB	WorkspaceStream	1	PrivateTag
+
+(0009,"DCMTK_ANONYMIZER",00)	SQ	AnonymizerUIDMap	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",10)	UI	AnonymizerUIDKey	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",20)	UI	AnonymizerUIDValue	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",30)	SQ	AnonymizerPatientIDMap	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",40)	LO	AnonymizerPatientIDKey	1	PrivateTag
+(0009,"DCMTK_ANONYMIZER",50)	LO	AnonymizerPatientIDValue	1	PrivateTag
+
+(0019,"DIDI TO PCR 1.1",22)	UN	RouteAET	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",23)	DS	PCRPrintScale	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",24)	UN	PCRPrintJobEnd	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",25)	IS	PCRNoFilmCopies	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",26)	IS	PCRFilmLayoutPosition	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",27)	UN	PCRPrintReportName	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",70)	UN	RADProtocolPrinter	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",71)	UN	RADProtocolMedium	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",90)	LO	UnprocessedFlag	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",91)	UN	KeyValues	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",92)	UN	DestinationPostprocessingFunction	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A0)	UN	Version	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A1)	UN	RangingMode	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A2)	UN	AbdomenBrightness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A3)	UN	FixedBrightness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A4)	UN	DetailContrast	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A5)	UN	ContrastBalance	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A6)	UN	StructureBoost	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A7)	UN	StructurePreference	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A8)	UN	NoiseRobustness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",A9)	UN	NoiseDoseLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AA)	UN	NoiseDoseStep	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AB)	UN	NoiseFrequencyLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AC)	UN	WeakContrastLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AD)	UN	StrongContrastLimit	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AE)	UN	StructureBoostOffset	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",AF)	UN	SmoothGain	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B0)	UN	MeasureField1	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B1)	UN	MeasureField2	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B2)	UN	KeyPercentile1	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B3)	UN	KeyPercentile2	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B4)	UN	DensityLUT	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B5)	UN	Brightness	1	PrivateTag
+(0019,"DIDI TO PCR 1.1",B6)	UN	Gamma	1	PrivateTag
+(0089,"DIDI TO PCR 1.1",10)	SQ	Unknown	1	PrivateTag
+
+(0029,"DIGISCAN IMAGE",31)	US	Unknown	1-n	PrivateTag
+(0029,"DIGISCAN IMAGE",32)	US	Unknown	1-n	PrivateTag
+(0029,"DIGISCAN IMAGE",33)	LT	Unknown	1	PrivateTag
+(0029,"DIGISCAN IMAGE",34)	LT	Unknown	1	PrivateTag
+
+(7001-o-70ff,"DLX_ANNOT_01",04)	ST	TextAnnotation	1	PrivateTag
+(7001-o-70ff,"DLX_ANNOT_01",05)	IS	Box	2	PrivateTag
+(7001-o-70ff,"DLX_ANNOT_01",07)	IS	ArrowEnd	2	PrivateTag
+
+(0015,"DLX_EXAMS_01",01)	DS	StenosisCalibrationRatio	1	PrivateTag
+(0015,"DLX_EXAMS_01",02)	DS	StenosisMagnification	1	PrivateTag
+(0015,"DLX_EXAMS_01",03)	DS	CardiacCalibrationRatio	1	PrivateTag
+
+(6001-o-60ff,"DLX_LKUP_01",01)	US	GrayPaletteColorLookupTableDescriptor	3	PrivateTag
+(6001-o-60ff,"DLX_LKUP_01",02)	US	GrayPaletteColorLookupTableData	1	PrivateTag
+
+(0011,"DLX_PATNT_01",01)	LT	PatientDOB	1	PrivateTag
+
+(0019,"DLX_SERIE_01",01)	DS	AngleValueLArm	1	PrivateTag
+(0019,"DLX_SERIE_01",02)	DS	AngleValuePArm	1	PrivateTag
+(0019,"DLX_SERIE_01",03)	DS	AngleValueCArm	1	PrivateTag
+(0019,"DLX_SERIE_01",04)	CS	AngleLabelLArm	1	PrivateTag
+(0019,"DLX_SERIE_01",05)	CS	AngleLabelPArm	1	PrivateTag
+(0019,"DLX_SERIE_01",06)	CS	AngleLabelCArm	1	PrivateTag
+(0019,"DLX_SERIE_01",07)	ST	ProcedureName	1	PrivateTag
+(0019,"DLX_SERIE_01",08)	ST	ExamName	1	PrivateTag
+(0019,"DLX_SERIE_01",09)	SH	PatientSize	1	PrivateTag
+(0019,"DLX_SERIE_01",0a)	IS	RecordView	1	PrivateTag
+(0019,"DLX_SERIE_01",10)	DS	InjectorDelay	1	PrivateTag
+(0019,"DLX_SERIE_01",11)	CS	AutoInject	1	PrivateTag
+(0019,"DLX_SERIE_01",14)	IS	AcquisitionMode	1	PrivateTag
+(0019,"DLX_SERIE_01",15)	CS	CameraRotationEnabled	1	PrivateTag
+(0019,"DLX_SERIE_01",16)	CS	ReverseSweep	1	PrivateTag
+(0019,"DLX_SERIE_01",17)	IS	SpatialFilterStrength	1	PrivateTag
+(0019,"DLX_SERIE_01",18)	IS	ZoomFactor	1	PrivateTag
+(0019,"DLX_SERIE_01",19)	IS	XZoomCenter	1	PrivateTag
+(0019,"DLX_SERIE_01",1a)	IS	YZoomCenter	1	PrivateTag
+(0019,"DLX_SERIE_01",1b)	DS	Focus	1	PrivateTag
+(0019,"DLX_SERIE_01",1c)	CS	Dose	1	PrivateTag
+(0019,"DLX_SERIE_01",1d)	IS	SideMark	1	PrivateTag
+(0019,"DLX_SERIE_01",1e)	IS	PercentageLandscape	1	PrivateTag
+(0019,"DLX_SERIE_01",1f)	DS	ExposureDuration	1	PrivateTag
+
+(00E1,"ELSCINT1",01)	US	DataDictionaryVersion	1	PrivateTag
+(00E1,"ELSCINT1",14)	LT	Unknown	1	PrivateTag
+(00E1,"ELSCINT1",22)	DS	Unknown	2	PrivateTag
+(00E1,"ELSCINT1",23)	DS	Unknown	2	PrivateTag
+(00E1,"ELSCINT1",24)	LT	Unknown	1	PrivateTag
+(00E1,"ELSCINT1",25)	LT	Unknown	1	PrivateTag
+(00E1,"ELSCINT1",40)	SH	OffsetFromCTMRImages	1	PrivateTag
+(0601,"ELSCINT1",00)	SH	ImplementationVersion	1	PrivateTag
+(0601,"ELSCINT1",20)	DS	RelativeTablePosition	1	PrivateTag
+(0601,"ELSCINT1",21)	DS	RelativeTableHeight	1	PrivateTag
+(0601,"ELSCINT1",30)	SH	SurviewDirection	1	PrivateTag
+(0601,"ELSCINT1",31)	DS	SurviewLength	1	PrivateTag
+(0601,"ELSCINT1",50)	SH	ImageViewType	1	PrivateTag
+(0601,"ELSCINT1",70)	DS	BatchNumber	1	PrivateTag
+(0601,"ELSCINT1",71)	DS	BatchSize	1	PrivateTag
+(0601,"ELSCINT1",72)	DS	BatchSliceNumber	1	PrivateTag
+
+(0009,"FDMS 1.0",04)	SH	ImageControlUnit	1	PrivateTag
+(0009,"FDMS 1.0",05)	OW	ImageUID	1	PrivateTag
+(0009,"FDMS 1.0",06)	OW	RouteImageUID	1	PrivateTag
+(0009,"FDMS 1.0",08)	UL	ImageDisplayInformationVersionNo	1	PrivateTag
+(0009,"FDMS 1.0",09)	UL	PatientInformationVersionNo	1	PrivateTag
+(0009,"FDMS 1.0",0C)	OW	FilmUID	1	PrivateTag
+(0009,"FDMS 1.0",10)	CS	ExposureUnitTypeCode	1	PrivateTag
+(0009,"FDMS 1.0",80)	LO	KanjiHospitalName	1	PrivateTag
+(0009,"FDMS 1.0",90)	ST	DistributionCode	1	PrivateTag
+(0009,"FDMS 1.0",92)	SH	KanjiDepartmentName	1	PrivateTag
+(0009,"FDMS 1.0",F0)	CS	BlackeningProcessFlag	1	PrivateTag
+(0019,"FDMS 1.0",15)	LO	KanjiBodyPartForExposure	1	PrivateTag
+(0019,"FDMS 1.0",32)	LO	KanjiMenuName	1	PrivateTag
+(0019,"FDMS 1.0",40)	CS	ImageProcessingType	1	PrivateTag
+(0019,"FDMS 1.0",50)	CS	EDRMode	1	PrivateTag
+(0019,"FDMS 1.0",60)	SH	RadiographersCode	1	PrivateTag
+(0019,"FDMS 1.0",70)	IS	SplitExposureFormat	1	PrivateTag
+(0019,"FDMS 1.0",71)	IS	NoOfSplitExposureFrames	1	PrivateTag
+(0019,"FDMS 1.0",80)	IS	ReadingPositionSpecification	1	PrivateTag
+(0019,"FDMS 1.0",81)	IS	ReadingSensitivityCenter	1	PrivateTag
+(0019,"FDMS 1.0",90)	SH	FilmAnnotationCharacterString1	1	PrivateTag
+(0019,"FDMS 1.0",91)	SH	FilmAnnotationCharacterString2	1	PrivateTag
+(0021,"FDMS 1.0",10)	CS	FCRImageID	1	PrivateTag
+(0021,"FDMS 1.0",30)	CS	SetNo	1	PrivateTag
+(0021,"FDMS 1.0",40)	IS	ImageNoInTheSet	1	PrivateTag
+(0021,"FDMS 1.0",50)	CS	PairProcessingInformation	1	PrivateTag
+(0021,"FDMS 1.0",80)	OB	EquipmentTypeSpecificInformation	1	PrivateTag
+(0023,"FDMS 1.0",10)	SQ	Unknown	1	PrivateTag
+(0023,"FDMS 1.0",20)	SQ	Unknown	1	PrivateTag
+(0023,"FDMS 1.0",30)	SQ	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",10)	US	RelativeLightEmissionAmountSk	1	PrivateTag
+(0025,"FDMS 1.0",11)	US	TermOfCorrectionForEachIPTypeSt	1	PrivateTag
+(0025,"FDMS 1.0",12)	US	ReadingGainGp	1	PrivateTag
+(0025,"FDMS 1.0",13)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",15)	CS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",20)	US	Unknown	2	PrivateTag
+(0025,"FDMS 1.0",21)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",30)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",31)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",32)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",33)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",34)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",40)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",41)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",42)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",43)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",50)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",51)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",52)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",53)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",60)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",61)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",62)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",63)	CS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",70)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",71)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",72)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",73)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",74)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",80)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",81)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",82)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",83)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",84)	US	Unknown	1-n	PrivateTag
+(0025,"FDMS 1.0",90)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",91)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",92)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",93)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",94)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",95)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",96)	CS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a0)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a1)	SS	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a2)	US	Unknown	1	PrivateTag
+(0025,"FDMS 1.0",a3)	SS	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",10)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",20)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",30)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",40)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",50)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",60)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",70)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",80)	SQ	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",a0)	IS	Unknown	1	PrivateTag
+(0027,"FDMS 1.0",a1)	CS	Unknown	2	PrivateTag
+(0027,"FDMS 1.0",a2)	CS	Unknown	2	PrivateTag
+(0027,"FDMS 1.0",a3)	SS	Unknown	1-n	PrivateTag
+(0029,"FDMS 1.0",20)	CS	ImageScanningDirection	1	PrivateTag
+(0029,"FDMS 1.0",30)	CS	ExtendedReadingSizeValue	1	PrivateTag
+(0029,"FDMS 1.0",34)	US	MagnificationReductionRatio	1	PrivateTag
+(0029,"FDMS 1.0",44)	CS	LineDensityCode	1	PrivateTag
+(0029,"FDMS 1.0",50)	CS	DataCompressionCode	1	PrivateTag
+(2011,"FDMS 1.0",11)	CS	ImagePosition SpecifyingFlag	1	PrivateTag
+(50F1,"FDMS 1.0",06)	CS	EnergySubtractionParam	1	PrivateTag
+(50F1,"FDMS 1.0",07)	CS	SubtractionRegistrationResult	1	PrivateTag
+(50F1,"FDMS 1.0",08)	CS	EnergySubtractionParam2	1	PrivateTag
+(50F1,"FDMS 1.0",09)	SL	AfinConversionCoefficient	1	PrivateTag
+(50F1,"FDMS 1.0",10)	CS	FilmOutputFormat	1	PrivateTag
+(50F1,"FDMS 1.0",20)	CS	ImageProcessingModificationFlag	1	PrivateTag
+
+(0009,"FFP DATA",01)	UN	CRHeaderInformation	1	PrivateTag
+
+(0019,"GE ??? From Adantage Review CS",30)	LO	CREDRMode	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",40)	LO	CRLatitude	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",50)	LO	CRGroupNumber	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",70)	LO	CRImageSerialNumber	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",80)	LO	CRBarCodeNumber	1	PrivateTag
+(0019,"GE ??? From Adantage Review CS",90)	LO	CRFilmOutputExposures	1	PrivateTag
+
+(0009,"GEMS_ACQU_01",24)	DS	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",25)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",3e)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",3f)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",42)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",43)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",f8)	US	Unknown	1	PrivateTag
+(0009,"GEMS_ACQU_01",fb)	IS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",01)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",02)	SL	NumberOfCellsInDetector	1	PrivateTag
+(0019,"GEMS_ACQU_01",03)	DS	CellNumberAtTheta	1	PrivateTag
+(0019,"GEMS_ACQU_01",04)	DS	CellSpacing	1	PrivateTag
+(0019,"GEMS_ACQU_01",05)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",06)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",0e)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",0f)	DS	HorizontalFrameOfReference	1	PrivateTag
+(0019,"GEMS_ACQU_01",11)	SS	SeriesContrast	1	PrivateTag
+(0019,"GEMS_ACQU_01",12)	SS	LastPseq	1	PrivateTag
+(0019,"GEMS_ACQU_01",13)	SS	StartNumberForBaseline	1	PrivateTag
+(0019,"GEMS_ACQU_01",14)	SS	End NumberForBaseline	1	PrivateTag
+(0019,"GEMS_ACQU_01",15)	SS	StartNumberForEnhancedScans	1	PrivateTag
+(0019,"GEMS_ACQU_01",16)	SS	EndNumberForEnhancedScans	1	PrivateTag
+(0019,"GEMS_ACQU_01",17)	SS	SeriesPlane	1	PrivateTag
+(0019,"GEMS_ACQU_01",18)	LO	FirstScanRAS	1	PrivateTag
+(0019,"GEMS_ACQU_01",19)	DS	FirstScanLocation	1	PrivateTag
+(0019,"GEMS_ACQU_01",1a)	LO	LastScanRAS	1	PrivateTag
+(0019,"GEMS_ACQU_01",1b)	DS	LastScanLocation	1	PrivateTag
+(0019,"GEMS_ACQU_01",1e)	DS	DisplayFieldOfView	1	PrivateTag
+(0019,"GEMS_ACQU_01",20)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",22)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",23)	DS	TableSpeed	1	PrivateTag
+(0019,"GEMS_ACQU_01",24)	DS	MidScanTime	1	PrivateTag
+(0019,"GEMS_ACQU_01",25)	SS	MidScanFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",26)	SL	DegreesOfAzimuth	1	PrivateTag
+(0019,"GEMS_ACQU_01",27)	DS	GantryPeriod	1	PrivateTag
+(0019,"GEMS_ACQU_01",2a)	DS	XrayOnPosition	1	PrivateTag
+(0019,"GEMS_ACQU_01",2b)	DS	XrayOffPosition	1	PrivateTag
+(0019,"GEMS_ACQU_01",2c)	SL	NumberOfTriggers	1	PrivateTag
+(0019,"GEMS_ACQU_01",2d)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",2e)	DS	AngleOfFirstView	1	PrivateTag
+(0019,"GEMS_ACQU_01",2f)	DS	TriggerFrequency	1	PrivateTag
+(0019,"GEMS_ACQU_01",39)	SS	ScanFOVType	1	PrivateTag
+(0019,"GEMS_ACQU_01",3a)	IS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3b)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3c)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3e)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",3f)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",40)	SS	StatReconFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",41)	SS	ComputeType	1	PrivateTag
+(0019,"GEMS_ACQU_01",42)	SS	SegmentNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",43)	SS	TotalSegmentsRequested	1	PrivateTag
+(0019,"GEMS_ACQU_01",44)	DS	InterscanDelay	1	PrivateTag
+(0019,"GEMS_ACQU_01",47)	SS	ViewCompressionFactor	1	PrivateTag
+(0019,"GEMS_ACQU_01",48)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",49)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",4a)	SS	TotalNumberOfRefChannels	1	PrivateTag
+(0019,"GEMS_ACQU_01",4b)	SL	DataSizeForScanData	1	PrivateTag
+(0019,"GEMS_ACQU_01",52)	SS	ReconPostProcessingFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",54)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",57)	SS	CTWaterNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",58)	SS	CTBoneNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",5a)	FL	AcquisitionDuration	1	PrivateTag
+(0019,"GEMS_ACQU_01",5d)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",5e)	SL	NumberOfChannels1To512	1	PrivateTag
+(0019,"GEMS_ACQU_01",5f)	SL	IncrementBetweenChannels	1	PrivateTag
+(0019,"GEMS_ACQU_01",60)	SL	StartingView	1	PrivateTag
+(0019,"GEMS_ACQU_01",61)	SL	NumberOfViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",62)	SL	IncrementBetweenViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",6a)	SS	DependantOnNumberOfViewsProcessed	1	PrivateTag
+(0019,"GEMS_ACQU_01",6b)	SS	FieldOfViewInDetectorCells	1	PrivateTag
+(0019,"GEMS_ACQU_01",70)	SS	ValueOfBackProjectionButton	1	PrivateTag
+(0019,"GEMS_ACQU_01",71)	SS	SetIfFatqEstimatesWereUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",72)	DS	ZChannelAvgOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",73)	DS	AvgOfLeftRefChannelsOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",74)	DS	MaxLeftChannelOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",75)	DS	AvgOfRightRefChannelsOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",76)	DS	MaxRightChannelOverViews	1	PrivateTag
+(0019,"GEMS_ACQU_01",7d)	DS	SecondEcho	1	PrivateTag
+(0019,"GEMS_ACQU_01",7e)	SS	NumberOfEchos	1	PrivateTag
+(0019,"GEMS_ACQU_01",7f)	DS	TableDelta	1	PrivateTag
+(0019,"GEMS_ACQU_01",81)	SS	Contiguous	1	PrivateTag
+(0019,"GEMS_ACQU_01",82)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",83)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",84)	DS	PeakSAR	1	PrivateTag
+(0019,"GEMS_ACQU_01",85)	SS	MonitorSAR	1	PrivateTag
+(0019,"GEMS_ACQU_01",86)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",87)	DS	CardiacRepetition Time	1	PrivateTag
+(0019,"GEMS_ACQU_01",88)	SS	ImagesPerCardiacCycle	1	PrivateTag
+(0019,"GEMS_ACQU_01",8a)	SS	ActualReceiveGainAnalog	1	PrivateTag
+(0019,"GEMS_ACQU_01",8b)	SS	ActualReceiveGainDigital	1	PrivateTag
+(0019,"GEMS_ACQU_01",8d)	DS	DelayAfterTrigger	1	PrivateTag
+(0019,"GEMS_ACQU_01",8f)	SS	SwapPhaseFrequency	1	PrivateTag
+(0019,"GEMS_ACQU_01",90)	SS	PauseInterval	1	PrivateTag
+(0019,"GEMS_ACQU_01",91)	DS	PulseTime	1	PrivateTag
+(0019,"GEMS_ACQU_01",92)	SL	SliceOffsetOnFrequencyAxis	1	PrivateTag
+(0019,"GEMS_ACQU_01",93)	DS	CenterFrequency	1	PrivateTag
+(0019,"GEMS_ACQU_01",94)	SS	TransmitGain	1	PrivateTag
+(0019,"GEMS_ACQU_01",95)	SS	AnalogReceiverGain	1	PrivateTag
+(0019,"GEMS_ACQU_01",96)	SS	DigitalReceiverGain	1	PrivateTag
+(0019,"GEMS_ACQU_01",97)	SL	BitmapDefiningCVs	1	PrivateTag
+(0019,"GEMS_ACQU_01",98)	SS	CenterFrequencyMethod	1	PrivateTag
+(0019,"GEMS_ACQU_01",99)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",9b)	SS	PulseSequenceMode	1	PrivateTag
+(0019,"GEMS_ACQU_01",9c)	LO	PulseSequenceName	1	PrivateTag
+(0019,"GEMS_ACQU_01",9d)	DT	PulseSequenceDate	1	PrivateTag
+(0019,"GEMS_ACQU_01",9e)	LO	InternalPulseSequenceName	1	PrivateTag
+(0019,"GEMS_ACQU_01",9f)	SS	TransmittingCoil	1	PrivateTag
+(0019,"GEMS_ACQU_01",a0)	SS	SurfaceCoilType	1	PrivateTag
+(0019,"GEMS_ACQU_01",a1)	SS	ExtremityCoilFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",a2)	SL	RawDataRunNumber	1	PrivateTag
+(0019,"GEMS_ACQU_01",a3)	UL	CalibratedFieldStrength	1	PrivateTag
+(0019,"GEMS_ACQU_01",a4)	SS	SATFatWaterBone	1	PrivateTag
+(0019,"GEMS_ACQU_01",a5)	DS	ReceiveBandwidth	1	PrivateTag
+(0019,"GEMS_ACQU_01",a7)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",a8)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",a9)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",aa)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ab)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ac)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ad)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ae)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",af)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b0)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b1)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b2)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b3)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b4)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b5)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b6)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b7)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b8)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",b9)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",ba)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",bb)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",bc)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",bd)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",be)	DS	ProjectionAngle	1	PrivateTag
+(0019,"GEMS_ACQU_01",c0)	SS	SaturationPlanes	1	PrivateTag
+(0019,"GEMS_ACQU_01",c1)	SS	SurfaceCoilIntensityCorrectionFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",c2)	SS	SATLocationR	1	PrivateTag
+(0019,"GEMS_ACQU_01",c3)	SS	SATLocationL	1	PrivateTag
+(0019,"GEMS_ACQU_01",c4)	SS	SATLocationA	1	PrivateTag
+(0019,"GEMS_ACQU_01",c5)	SS	SATLocationP	1	PrivateTag
+(0019,"GEMS_ACQU_01",c6)	SS	SATLocationH	1	PrivateTag
+(0019,"GEMS_ACQU_01",c7)	SS	SATLocationF	1	PrivateTag
+(0019,"GEMS_ACQU_01",c8)	SS	SATThicknessRL	1	PrivateTag
+(0019,"GEMS_ACQU_01",c9)	SS	SATThicknessAP	1	PrivateTag
+(0019,"GEMS_ACQU_01",ca)	SS	SATThicknessHF	1	PrivateTag
+(0019,"GEMS_ACQU_01",cb)	SS	PrescribedFlowAxis	1	PrivateTag
+(0019,"GEMS_ACQU_01",cc)	SS	VelocityEncoding	1	PrivateTag
+(0019,"GEMS_ACQU_01",cd)	SS	ThicknessDisclaimer	1	PrivateTag
+(0019,"GEMS_ACQU_01",ce)	SS	PrescanType	1	PrivateTag
+(0019,"GEMS_ACQU_01",cf)	SS	PrescanStatus	1	PrivateTag
+(0019,"GEMS_ACQU_01",d0)	SH	RawDataType	1	PrivateTag
+(0019,"GEMS_ACQU_01",d2)	SS	ProjectionAlgorithm	1	PrivateTag
+(0019,"GEMS_ACQU_01",d3)	SH	ProjectionAlgorithm	1	PrivateTag
+(0019,"GEMS_ACQU_01",d4)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",d5)	SS	FractionalEcho	1	PrivateTag
+(0019,"GEMS_ACQU_01",d6)	SS	PrepPulse	1	PrivateTag
+(0019,"GEMS_ACQU_01",d7)	SS	CardiacPhases	1	PrivateTag
+(0019,"GEMS_ACQU_01",d8)	SS	VariableEchoFlag	1	PrivateTag
+(0019,"GEMS_ACQU_01",d9)	DS	ConcatenatedSAT	1	PrivateTag
+(0019,"GEMS_ACQU_01",da)	SS	ReferenceChannelUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",db)	DS	BackProjectorCoefficient	1	PrivateTag
+(0019,"GEMS_ACQU_01",dc)	SS	PrimarySpeedCorrectionUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",dd)	SS	OverrangeCorrectionUsed	1	PrivateTag
+(0019,"GEMS_ACQU_01",de)	DS	DynamicZAlphaValue	1	PrivateTag
+(0019,"GEMS_ACQU_01",df)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",e0)	DS	UserData	1	PrivateTag
+(0019,"GEMS_ACQU_01",e1)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e2)	DS	VelocityEncodeScale	1	PrivateTag
+(0019,"GEMS_ACQU_01",e3)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e4)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e5)	IS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e6)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e8)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",e9)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",eb)	DS	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",ec)	US	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f0)	UN	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f1)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f2)	SS	FastPhases	1	PrivateTag
+(0019,"GEMS_ACQU_01",f3)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f4)	LT	Unknown	1	PrivateTag
+(0019,"GEMS_ACQU_01",f9)	DS	TransmitGain	1	PrivateTag
+
+(0023,"GEMS_ACRQA_1.0 BLOCK1",00)	LO	CRExposureMenuCode	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",10)	LO	CRExposureMenuString	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",20)	LO	CREDRMode	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",30)	LO	CRLatitude	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",40)	LO	CRGroupNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",50)	US	CRImageSerialNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",60)	LO	CRBarCodeNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",70)	LO	CRFilmOutputExposure	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",80)	LO	CRFilmFormat	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK1",90)	LO	CRSShiftString	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",00)	US	CRSShift	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",10)	DS	CRCShift	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",20)	DS	CRGT	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",30)	DS	CRGA	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",40)	DS	CRGC	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",50)	DS	CRGS	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",60)	DS	CRRT	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",70)	DS	CRRE	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",80)	US	CRRN	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK2",90)	DS	CRDRT	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",00)	DS	CRDRE	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",10)	US	CRDRN	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",20)	DS	CRORE	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",30)	US	CRORN	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",40)	US	CRORD	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",50)	LO	CRCassetteSize	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",60)	LO	CRMachineID	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",70)	LO	CRMachineType	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",80)	LO	CRTechnicianCode	1	PrivateTag
+(0023,"GEMS_ACRQA_1.0 BLOCK3",90)	LO	CREnergySubtractionParameters	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",00)	LO	CRExposureMenuCode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",10)	LO	CRExposureMenuString	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",20)	LO	CREDRMode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",30)	LO	CRLatitude	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",40)	LO	CRGroupNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",50)	US	CRImageSerialNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",60)	LO	CRBarCodeNumber	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",70)	LO	CRFilmOutputExposure	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",80)	LO	CRFilmFormat	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK1",90)	LO	CRSShiftString	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",00)	US	CRSShift	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",10)	LO	CRCShift	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",20)	LO	CRGT	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",30)	DS	CRGA	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",40)	DS	CRGC	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",50)	DS	CRGS	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",60)	LO	CRRT	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",70)	DS	CRRE	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",80)	US	CRRN	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK2",90)	DS	CRDRT	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",00)	DS	CRDRE	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",10)	US	CRDRN	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",20)	DS	CRORE	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",30)	US	CRORN	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",40)	US	CRORD	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",50)	LO	CRCassetteSize	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",60)	LO	CRMachineID	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",70)	LO	CRMachineType	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",80)	LO	CRTechnicianCode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",90)	LO	CREnergySubtractionParameters	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",f0)	LO	CRDistributionCode	1	PrivateTag
+(0023,"GEMS_ACRQA_2.0 BLOCK3",ff)	US	CRShuttersApplied	1	PrivateTag
+
+(0047,"GEMS_ADWSoft_3D1",01)	SQ	Reconstruction Parameters Sequence	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",50)	UL	VolumeVoxelCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",51)	UL	VolumeSegmentCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",53)	US	VolumeSliceSize	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",54)	US	VolumeSliceCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",55)	SL	VolumeThresholdValue	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",57)	DS	VolumeVoxelRatio	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",58)	DS	VolumeVoxelSize	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",59)	US	VolumeZPositionSize	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",60)	DS	VolumeBaseLine	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",61)	DS	VolumeCenterPoint	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",63)	SL	VolumeSkewBase	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",64)	DS	VolumeRegistrationTransformRotationMatrix	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",65)	DS	VolumeRegistrationTransformTranslationVector	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",70)	DS	KVPList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",71)	IS	XRayTubeCurrentList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",72)	IS	ExposureList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",80)	LO	AcquisitionDLXIdentifier	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",85)	SQ	AcquisitionDLX2DSeriesSequence	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",89)	DS	ContrastAgentVolumeList	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",8A)	US	NumberOfInjections	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",8B)	US	FrameCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",91)	LO	XA3DReconstructionAlgorithmName	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",92)	CS	XA3DReconstructionAlgorithmVersion	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",93)	DA	DLXCalibrationDate	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",94)	TM	DLXCalibrationTime	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",95)	CS	DLXCalibrationStatus	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",96)	IS	UsedFrames	1-n	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",98)	US	TransformCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",99)	SQ	TransformSequence	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",9A)	DS	TransformRotationMatrix	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",9B)	DS	TransformTranslationVector	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",9C)	LO	TransformLabel	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B0)	SQ	WireframeList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B1)	US	WireframeCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B2)	US	LocationSystem	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B5)	LO	WireframeName	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B6)	LO	WireframeGroupName	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B7)	LO	WireframeColor	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B8)	SL	WireframeAttributes	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",B9)	SL	WireframePointCount	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",BA)	SL	WireframeTimestamp	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",BB)	SQ	WireframePointList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",BC)	DS	WireframePointsCoordinates	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",C0)	DS	VolumeUpperLeftHighCornerRAS	3	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",C1)	DS	VolumeSliceToRASRotationMatrix	9	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",C2)	DS	VolumeUpperLeftHighCornerTLOC	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D1)	OB	VolumeSegmentList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D2)	OB	VolumeGradientList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D3)	OB	VolumeDensityList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D4)	OB	VolumeZPositionList	1	PrivateTag
+(0047,"GEMS_ADWSoft_3D1",D5)	OB	VolumeOriginalIndexList	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",80)	IS	PrivateEntityNumber	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",85)	DA	PrivateEntityDate	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",90)	TM	PrivateEntityTime	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",95)	LO	PrivateEntityLaunchCommand	1	PrivateTag
+(0039,"GEMS_ADWSoft_DPO",AA)	CS	PrivateEntityType	1	PrivateTag
+
+(0033,"GEMS_CTHD_01",02)	UN	Unknown	1	PrivateTag
+
+(0037,"GEMS_DRS_1",10)	LO	ReferringDepartment	1	PrivateTag
+(0037,"GEMS_DRS_1",20)	US	ScreenNumber	1	PrivateTag
+(0037,"GEMS_DRS_1",40)	SH	LeftOrientation	1	PrivateTag
+(0037,"GEMS_DRS_1",42)	SH	RightOrientation	1	PrivateTag
+(0037,"GEMS_DRS_1",50)	CS	Inversion	1	PrivateTag
+(0037,"GEMS_DRS_1",60)	US	DSA	1	PrivateTag
+
+(0009,"GEMS_GENIE_1",10)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",11)	SL	StudyFlags	1	PrivateTag
+(0009,"GEMS_GENIE_1",12)	SL	StudyType	1	PrivateTag
+(0009,"GEMS_GENIE_1",1e)	UI	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",20)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",21)	SL	SeriesFlags	1	PrivateTag
+(0009,"GEMS_GENIE_1",22)	SH	UserOrientation	1	PrivateTag
+(0009,"GEMS_GENIE_1",23)	SL	InitiationType	1	PrivateTag
+(0009,"GEMS_GENIE_1",24)	SL	InitiationDelay	1	PrivateTag
+(0009,"GEMS_GENIE_1",25)	SL	InitiationCountRate	1	PrivateTag
+(0009,"GEMS_GENIE_1",26)	SL	NumberEnergySets	1	PrivateTag
+(0009,"GEMS_GENIE_1",27)	SL	NumberDetectors	1	PrivateTag
+(0009,"GEMS_GENIE_1",29)	SL	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",2a)	SL	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",2c)	LO	SeriesComments	1	PrivateTag
+(0009,"GEMS_GENIE_1",2d)	SL	TrackBeatAverage	1	PrivateTag
+(0009,"GEMS_GENIE_1",2e)	FD	DistancePrescribed	1	PrivateTag
+(0009,"GEMS_GENIE_1",30)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",35)	SL	GantryLocusType	1	PrivateTag
+(0009,"GEMS_GENIE_1",37)	SL	StartingHeartRate	1	PrivateTag
+(0009,"GEMS_GENIE_1",38)	SL	RRWindowWidth	1	PrivateTag
+(0009,"GEMS_GENIE_1",39)	SL	RRWindowOffset	1	PrivateTag
+(0009,"GEMS_GENIE_1",3a)	SL	PercentCycleImaged	1	PrivateTag
+(0009,"GEMS_GENIE_1",40)	LO	Unknown	1	PrivateTag
+(0009,"GEMS_GENIE_1",41)	SL	PatientFlags	1	PrivateTag
+(0009,"GEMS_GENIE_1",42)	DA	PatientCreationDate	1	PrivateTag
+(0009,"GEMS_GENIE_1",43)	TM	PatientCreationTime	1	PrivateTag
+(0011,"GEMS_GENIE_1",0a)	SL	SeriesType	1	PrivateTag
+(0011,"GEMS_GENIE_1",0b)	SL	EffectiveSeriesDuration	1	PrivateTag
+(0011,"GEMS_GENIE_1",0c)	SL	NumBeats	1	PrivateTag
+(0011,"GEMS_GENIE_1",0d)	LO	RadioNuclideName	1	PrivateTag
+(0011,"GEMS_GENIE_1",10)	LO	Unknown	1	PrivateTag
+(0011,"GEMS_GENIE_1",12)	LO	DatasetName	1	PrivateTag
+(0011,"GEMS_GENIE_1",13)	SL	DatasetType	1	PrivateTag
+(0011,"GEMS_GENIE_1",15)	SL	DetectorNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",16)	SL	EnergyNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",17)	SL	RRIntervalWindowNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",18)	SL	MGBinNumber	1	PrivateTag
+(0011,"GEMS_GENIE_1",19)	FD	RadiusOfRotation	1	PrivateTag
+(0011,"GEMS_GENIE_1",1a)	SL	DetectorCountZone	1	PrivateTag
+(0011,"GEMS_GENIE_1",1b)	SL	NumEnergyWindows	1	PrivateTag
+(0011,"GEMS_GENIE_1",1c)	SL	EnergyOffset	4	PrivateTag
+(0011,"GEMS_GENIE_1",1d)	SL	EnergyRange	1	PrivateTag
+(0011,"GEMS_GENIE_1",1f)	SL	ImageOrientation	1	PrivateTag
+(0011,"GEMS_GENIE_1",23)	SL	UseFOVMask	1	PrivateTag
+(0011,"GEMS_GENIE_1",24)	SL	FOVMaskYCutoffAngle	1	PrivateTag
+(0011,"GEMS_GENIE_1",25)	SL	FOVMaskCutoffAngle	1	PrivateTag
+(0011,"GEMS_GENIE_1",26)	SL	TableOrientation	1	PrivateTag
+(0011,"GEMS_GENIE_1",27)	SL	ROITopLeft	2	PrivateTag
+(0011,"GEMS_GENIE_1",28)	SL	ROIBottomRight	2	PrivateTag
+(0011,"GEMS_GENIE_1",30)	LO	Unknown	1	PrivateTag
+(0011,"GEMS_GENIE_1",33)	LO	EnergyCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",34)	LO	SpatialCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",35)	LO	TuningCalibName	1	PrivateTag
+(0011,"GEMS_GENIE_1",36)	LO	UniformityCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",37)	LO	AcquisitionSpecificCorrectName	1	PrivateTag
+(0011,"GEMS_GENIE_1",38)	SL	ByteOrder	1	PrivateTag
+(0011,"GEMS_GENIE_1",3a)	SL	PictureFormat	1	PrivateTag
+(0011,"GEMS_GENIE_1",3b)	FD	PixelScale	1	PrivateTag
+(0011,"GEMS_GENIE_1",3c)	FD	PixelOffset	1	PrivateTag
+(0011,"GEMS_GENIE_1",3e)	SL	FOVShape	1	PrivateTag
+(0011,"GEMS_GENIE_1",3f)	SL	DatasetFlags	1	PrivateTag
+(0011,"GEMS_GENIE_1",44)	FD	ThresholdCenter	1	PrivateTag
+(0011,"GEMS_GENIE_1",45)	FD	ThresholdWidth	1	PrivateTag
+(0011,"GEMS_GENIE_1",46)	SL	InterpolationType	1	PrivateTag
+(0011,"GEMS_GENIE_1",55)	FD	Period	1	PrivateTag
+(0011,"GEMS_GENIE_1",56)	FD	ElapsedTime	1	PrivateTag
+(0013,"GEMS_GENIE_1",10)	FD	DigitalFOV	2	PrivateTag
+(0013,"GEMS_GENIE_1",11)	SL	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",12)	SL	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",16)	SL	AutoTrackPeak	1	PrivateTag
+(0013,"GEMS_GENIE_1",17)	SL	AutoTrackWidth	1	PrivateTag
+(0013,"GEMS_GENIE_1",18)	FD	TransmissionScanTime	1	PrivateTag
+(0013,"GEMS_GENIE_1",19)	FD	TransmissionMaskWidth	1	PrivateTag
+(0013,"GEMS_GENIE_1",1a)	FD	CopperAttenuatorThickness	1	PrivateTag
+(0013,"GEMS_GENIE_1",1c)	FD	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",1d)	FD	Unknown	1	PrivateTag
+(0013,"GEMS_GENIE_1",1e)	FD	TomoViewOffset	1-n	PrivateTag
+(0013,"GEMS_GENIE_1",26)	LT	StudyComments	1	PrivateTag
+
+(0033,"GEMS_GNHD_01",01)	UN	Unknown	1	PrivateTag
+(0033,"GEMS_GNHD_01",02)	UN	Unknown	1	PrivateTag
+
+(0009,"GEMS_IDEN_01",01)	LO	FullFidelity	1	PrivateTag
+(0009,"GEMS_IDEN_01",02)	SH	SuiteId	1	PrivateTag
+(0009,"GEMS_IDEN_01",04)	SH	ProductId	1	PrivateTag
+(0009,"GEMS_IDEN_01",17)	LT	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",1a)	US	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",20)	US	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",27)	SL	ImageActualDate	1	PrivateTag
+(0009,"GEMS_IDEN_01",2f)	LT	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",30)	SH	ServiceId	1	PrivateTag
+(0009,"GEMS_IDEN_01",31)	SH	MobileLocationNumber	1	PrivateTag
+(0009,"GEMS_IDEN_01",e2)	LT	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",e3)	UI	EquipmentUID	1	PrivateTag
+(0009,"GEMS_IDEN_01",e6)	SH	GenesisVersionNow	1	PrivateTag
+(0009,"GEMS_IDEN_01",e7)	UL	ExamRecordChecksum	1	PrivateTag
+(0009,"GEMS_IDEN_01",e8)	UL	Unknown	1	PrivateTag
+(0009,"GEMS_IDEN_01",e9)	SL	ActualSeriesDataTimeStamp	1	PrivateTag
+
+(0027,"GEMS_IMAG_01",06)	SL	ImageArchiveFlag	1	PrivateTag
+(0027,"GEMS_IMAG_01",10)	SS	ScoutType	1	PrivateTag
+(0027,"GEMS_IMAG_01",1c)	SL	VmaMamp	1	PrivateTag
+(0027,"GEMS_IMAG_01",1d)	SS	VmaPhase	1	PrivateTag
+(0027,"GEMS_IMAG_01",1e)	SL	VmaMod	1	PrivateTag
+(0027,"GEMS_IMAG_01",1f)	SL	VmaClip	1	PrivateTag
+(0027,"GEMS_IMAG_01",20)	SS	SmartScanOnOffFlag	1	PrivateTag
+(0027,"GEMS_IMAG_01",30)	SH	ForeignImageRevision	1	PrivateTag
+(0027,"GEMS_IMAG_01",31)	SS	ImagingMode	1	PrivateTag
+(0027,"GEMS_IMAG_01",32)	SS	PulseSequence	1	PrivateTag
+(0027,"GEMS_IMAG_01",33)	SL	ImagingOptions	1	PrivateTag
+(0027,"GEMS_IMAG_01",35)	SS	PlaneType	1	PrivateTag
+(0027,"GEMS_IMAG_01",36)	SL	ObliquePlane	1	PrivateTag
+(0027,"GEMS_IMAG_01",40)	SH	RASLetterOfImageLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",41)	FL	ImageLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",42)	FL	CenterRCoordOfPlaneImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",43)	FL	CenterACoordOfPlaneImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",44)	FL	CenterSCoordOfPlaneImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",45)	FL	NormalRCoord	1	PrivateTag
+(0027,"GEMS_IMAG_01",46)	FL	NormalACoord	1	PrivateTag
+(0027,"GEMS_IMAG_01",47)	FL	NormalSCoord	1	PrivateTag
+(0027,"GEMS_IMAG_01",48)	FL	RCoordOfTopRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",49)	FL	ACoordOfTopRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4a)	FL	SCoordOfTopRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4b)	FL	RCoordOfBottomRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4c)	FL	ACoordOfBottomRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",4d)	FL	SCoordOfBottomRightCorner	1	PrivateTag
+(0027,"GEMS_IMAG_01",50)	FL	TableStartLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",51)	FL	TableEndLocation	1	PrivateTag
+(0027,"GEMS_IMAG_01",52)	SH	RASLetterForSideOfImage	1	PrivateTag
+(0027,"GEMS_IMAG_01",53)	SH	RASLetterForAnteriorPosterior	1	PrivateTag
+(0027,"GEMS_IMAG_01",54)	SH	RASLetterForScoutStartLoc	1	PrivateTag
+(0027,"GEMS_IMAG_01",55)	SH	RASLetterForScoutEndLoc	1	PrivateTag
+(0027,"GEMS_IMAG_01",60)	FL	ImageDimensionX	1	PrivateTag
+(0027,"GEMS_IMAG_01",61)	FL	ImageDimensionY	1	PrivateTag
+(0027,"GEMS_IMAG_01",62)	FL	NumberOfExcitations	1	PrivateTag
+
+(0029,"GEMS_IMPS_01",04)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",05)	DS	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",06)	DS	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",07)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",08)	SH	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",09)	SH	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",0a)	SS	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",15)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",16)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",17)	SL	LowerRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",18)	SL	UpperRangeOfPixels	1	PrivateTag
+(0029,"GEMS_IMPS_01",1a)	SL	LengthOfTotalHeaderInBytes	1	PrivateTag
+(0029,"GEMS_IMPS_01",26)	SS	VersionOfHeaderStructure	1	PrivateTag
+(0029,"GEMS_IMPS_01",34)	SL	AdvantageCompOverflow	1	PrivateTag
+(0029,"GEMS_IMPS_01",35)	SL	AdvantageCompUnderflow	1	PrivateTag
+
+(0043,"GEMS_PARM_01",01)	SS	BitmapOfPrescanOptions	1	PrivateTag
+(0043,"GEMS_PARM_01",02)	SS	GradientOffsetInX	1	PrivateTag
+(0043,"GEMS_PARM_01",03)	SS	GradientOffsetInY	1	PrivateTag
+(0043,"GEMS_PARM_01",04)	SS	GradientOffsetInZ	1	PrivateTag
+(0043,"GEMS_PARM_01",05)	SS	ImageIsOriginalOrUnoriginal	1	PrivateTag
+(0043,"GEMS_PARM_01",06)	SS	NumberOfEPIShots	1	PrivateTag
+(0043,"GEMS_PARM_01",07)	SS	ViewsPerSegment	1	PrivateTag
+(0043,"GEMS_PARM_01",08)	SS	RespiratoryRateInBPM	1	PrivateTag
+(0043,"GEMS_PARM_01",09)	SS	RespiratoryTriggerPoint	1	PrivateTag
+(0043,"GEMS_PARM_01",0a)	SS	TypeOfReceiverUsed	1	PrivateTag
+(0043,"GEMS_PARM_01",0b)	DS	PeakRateOfChangeOfGradientField	1	PrivateTag
+(0043,"GEMS_PARM_01",0c)	DS	LimitsInUnitsOfPercent	1	PrivateTag
+(0043,"GEMS_PARM_01",0d)	DS	PSDEstimatedLimit	1	PrivateTag
+(0043,"GEMS_PARM_01",0e)	DS	PSDEstimatedLimitInTeslaPerSecond	1	PrivateTag
+(0043,"GEMS_PARM_01",0f)	DS	SARAvgHead	1	PrivateTag
+(0043,"GEMS_PARM_01",10)	US	WindowValue	1	PrivateTag
+(0043,"GEMS_PARM_01",11)	US	TotalInputViews	1	PrivateTag
+(0043,"GEMS_PARM_01",12)	SS	XrayChain	3	PrivateTag
+(0043,"GEMS_PARM_01",13)	SS	ReconKernelParameters	5	PrivateTag
+(0043,"GEMS_PARM_01",14)	SS	CalibrationParameters	3	PrivateTag
+(0043,"GEMS_PARM_01",15)	SS	TotalOutputViews	3	PrivateTag
+(0043,"GEMS_PARM_01",16)	SS	NumberOfOverranges	5	PrivateTag
+(0043,"GEMS_PARM_01",17)	DS	IBHImageScaleFactors	1	PrivateTag
+(0043,"GEMS_PARM_01",18)	DS	BBHCoefficients	3	PrivateTag
+(0043,"GEMS_PARM_01",19)	SS	NumberOfBBHChainsToBlend	1	PrivateTag
+(0043,"GEMS_PARM_01",1a)	SL	StartingChannelNumber	1	PrivateTag
+(0043,"GEMS_PARM_01",1b)	SS	PPScanParameters	1	PrivateTag
+(0043,"GEMS_PARM_01",1c)	SS	GEImageIntegrity	1	PrivateTag
+(0043,"GEMS_PARM_01",1d)	SS	LevelValue	1	PrivateTag
+(0043,"GEMS_PARM_01",1e)	DS	DeltaStartTime	1	PrivateTag
+(0043,"GEMS_PARM_01",1f)	SL	MaxOverrangesInAView	1	PrivateTag
+(0043,"GEMS_PARM_01",20)	DS	AvgOverrangesAllViews	1	PrivateTag
+(0043,"GEMS_PARM_01",21)	SS	CorrectedAfterglowTerms	1	PrivateTag
+(0043,"GEMS_PARM_01",25)	SS	ReferenceChannels	6	PrivateTag
+(0043,"GEMS_PARM_01",26)	US	NoViewsRefChannelsBlocked	6	PrivateTag
+(0043,"GEMS_PARM_01",27)	SH	ScanPitchRatio	1	PrivateTag
+(0043,"GEMS_PARM_01",28)	OB	UniqueImageIdentifier	1	PrivateTag
+(0043,"GEMS_PARM_01",29)	OB	HistogramTables	1	PrivateTag
+(0043,"GEMS_PARM_01",2a)	OB	UserDefinedData	1	PrivateTag
+(0043,"GEMS_PARM_01",2b)	SS	PrivateScanOptions	4	PrivateTag
+(0043,"GEMS_PARM_01",2c)	SS	EffectiveEchoSpacing	1	PrivateTag
+(0043,"GEMS_PARM_01",2d)	SH	StringSlopField1	1	PrivateTag
+(0043,"GEMS_PARM_01",2e)	SH	StringSlopField2	1	PrivateTag
+(0043,"GEMS_PARM_01",2f)	SS	RawDataType	1	PrivateTag
+(0043,"GEMS_PARM_01",30)	SS	RawDataType	1	PrivateTag
+(0043,"GEMS_PARM_01",31)	DS	RACoordOfTargetReconCentre	2	PrivateTag
+(0043,"GEMS_PARM_01",32)	SS	RawDataType	1	PrivateTag
+(0043,"GEMS_PARM_01",33)	FL	NegScanSpacing	1	PrivateTag
+(0043,"GEMS_PARM_01",34)	IS	OffsetFrequency	1	PrivateTag
+(0043,"GEMS_PARM_01",35)	UL	UserUsageTag	1	PrivateTag
+(0043,"GEMS_PARM_01",36)	UL	UserFillMapMSW	1	PrivateTag
+(0043,"GEMS_PARM_01",37)	UL	UserFillMapLSW	1	PrivateTag
+(0043,"GEMS_PARM_01",38)	FL	User25ToUser48	24	PrivateTag
+(0043,"GEMS_PARM_01",39)	IS	SlopInteger6ToSlopInteger9	4	PrivateTag
+(0043,"GEMS_PARM_01",40)	FL	TriggerOnPosition	4	PrivateTag
+(0043,"GEMS_PARM_01",41)	FL	DegreeOfRotation	4	PrivateTag
+(0043,"GEMS_PARM_01",42)	SL	DASTriggerSource	4	PrivateTag
+(0043,"GEMS_PARM_01",43)	SL	DASFpaGain	4	PrivateTag
+(0043,"GEMS_PARM_01",44)	SL	DASOutputSource	4	PrivateTag
+(0043,"GEMS_PARM_01",45)	SL	DASAdInput	4	PrivateTag
+(0043,"GEMS_PARM_01",46)	SL	DASCalMode	4	PrivateTag
+(0043,"GEMS_PARM_01",47)	SL	DASCalFrequency	4	PrivateTag
+(0043,"GEMS_PARM_01",48)	SL	DASRegXm	4	PrivateTag
+(0043,"GEMS_PARM_01",49)	SL	DASAutoZero	4	PrivateTag
+(0043,"GEMS_PARM_01",4a)	SS	StartingChannelOfView	4	PrivateTag
+(0043,"GEMS_PARM_01",4b)	SL	DASXmPattern	4	PrivateTag
+(0043,"GEMS_PARM_01",4c)	SS	TGGCTriggerMode	4	PrivateTag
+(0043,"GEMS_PARM_01",4d)	FL	StartScanToXrayOnDelay	4	PrivateTag
+(0043,"GEMS_PARM_01",4e)	FL	DurationOfXrayOn	4	PrivateTag
+(0043,"GEMS_PARM_01",60)	IS	SlopInteger10ToSlopInteger17	8	PrivateTag
+(0043,"GEMS_PARM_01",61)	UI	ScannerStudyEntityUID	1	PrivateTag
+(0043,"GEMS_PARM_01",62)	SH	ScannerStudyID	1	PrivateTag
+(0043,"GEMS_PARM_01",6f)	DS	ScannerTableEntry	3	PrivateTag
+(0043,"GEMS_PARM_01",70)	LO	ParadigmName	1	PrivateTag
+(0043,"GEMS_PARM_01",71)	ST	ParadigmDescription	1	PrivateTag
+(0043,"GEMS_PARM_01",72)	UI	ParadigmUID	1	PrivateTag
+(0043,"GEMS_PARM_01",73)	US	ExperimentType	1	PrivateTag
+(0043,"GEMS_PARM_01",74)	US	NumberOfRestVolumes	1	PrivateTag
+(0043,"GEMS_PARM_01",75)	US	NumberOfActiveVolumes	1	PrivateTag
+(0043,"GEMS_PARM_01",76)	US	NumberOfDummyScans	1	PrivateTag
+(0043,"GEMS_PARM_01",77)	SH	ApplicationName	1	PrivateTag
+(0043,"GEMS_PARM_01",78)	SH	ApplicationVersion	1	PrivateTag
+(0043,"GEMS_PARM_01",79)	US	SlicesPerVolume	1	PrivateTag
+(0043,"GEMS_PARM_01",7a)	US	ExpectedTimePoints	1	PrivateTag
+(0043,"GEMS_PARM_01",7b)	FL	RegressorValues	1-n	PrivateTag
+(0043,"GEMS_PARM_01",7c)	FL	DelayAfterSliceGroup	1	PrivateTag
+(0043,"GEMS_PARM_01",7d)	US	ReconModeFlagWord	1	PrivateTag
+(0043,"GEMS_PARM_01",7e)	LO	PACCSpecificInformation	1-n	PrivateTag
+(0043,"GEMS_PARM_01",7f)	DS	EDWIScaleFactor	1-n	PrivateTag
+(0043,"GEMS_PARM_01",80)	LO	CoilIDData	1-n	PrivateTag
+(0043,"GEMS_PARM_01",81)	LO	GECoilName	1	PrivateTag
+(0043,"GEMS_PARM_01",82)	LO	SystemConfigurationInformation	1-n	PrivateTag
+(0043,"GEMS_PARM_01",83)	DS	AssetRFactors	1-2	PrivateTag
+(0043,"GEMS_PARM_01",84)	LO	AdditionalAssetData	5-n	PrivateTag
+(0043,"GEMS_PARM_01",85)	UT	DebugDataTextFormat	1	PrivateTag
+(0043,"GEMS_PARM_01",86)	OB	DebugDataBinaryFormat	1	PrivateTag
+(0043,"GEMS_PARM_01",87)	UT	ScannerSoftwareVersionLongForm	1	PrivateTag
+(0043,"GEMS_PARM_01",88)	UI	PUREAcquisitionCalibrationSeriesUID	1	PrivateTag
+(0043,"GEMS_PARM_01",89)	LO	GoverningBodydBdtAndSARDefinition	3	PrivateTag
+(0043,"GEMS_PARM_01",8a)	CS	PrivateInPlanePhaseEncodingDirection	1	PrivateTag
+(0043,"GEMS_PARM_01",8b)	OB	FMRIBinaryDataBlock	1	PrivateTag
+(0043,"GEMS_PARM_01",8c)	DS	VoxelLocation	6	PrivateTag
+(0043,"GEMS_PARM_01",8d)	DS	SATBandLocations	7-7n	PrivateTag
+(0043,"GEMS_PARM_01",8e)	DS	SpectroPrescanValues	3	PrivateTag
+(0043,"GEMS_PARM_01",8f)	DS	SpectroParameters	3	PrivateTag
+(0043,"GEMS_PARM_01",90)	LO	SARDefinition	1-n	PrivateTag
+(0043,"GEMS_PARM_01",91)	DS	SARValue	1-n	PrivateTag
+(0043,"GEMS_PARM_01",92)	LO	ImageErrorText	1	PrivateTag
+(0043,"GEMS_PARM_01",93)	DS	SpectroQuantitationValues	1-n	PrivateTag
+(0043,"GEMS_PARM_01",94)	DS	SpectroRatioValues	1-n	PrivateTag
+(0043,"GEMS_PARM_01",95)	LO	PrescanReuseString	1	PrivateTag
+(0043,"GEMS_PARM_01",96)	CS	ContentQualification	1	PrivateTag
+(0043,"GEMS_PARM_01",97)	LO	ImageFilteringParameters	9	PrivateTag
+(0043,"GEMS_PARM_01",98)	UI	ASSETAcquisitionCalibrationSeriesUID	1	PrivateTag
+(0043,"GEMS_PARM_01",99)	LO	ExtendedOptions	1-n	PrivateTag
+(0043,"GEMS_PARM_01",9a)	IS	RxStackIdentification	1	PrivateTag
+(0043,"GEMS_PARM_01",9b)	DS	NPWFactor	1	PrivateTag
+(0043,"GEMS_PARM_01",9c)	OB	ResearchTag1	1	PrivateTag
+(0043,"GEMS_PARM_01",9d)	OB	ResearchTag2	1	PrivateTag
+(0043,"GEMS_PARM_01",9e)	OB	ResearchTag3	1	PrivateTag
+(0043,"GEMS_PARM_01",9f)	OB	ResearchTag4	1	PrivateTag
+
+(0011,"GEMS_PATI_01",10)	SS	PatientStatus	1	PrivateTag
+
+(0021,"GEMS_RELA_01",03)	SS	SeriesFromWhichPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",05)	SH	GenesisVersionNow	1	PrivateTag
+(0021,"GEMS_RELA_01",07)	UL	SeriesRecordChecksum	1	PrivateTag
+(0021,"GEMS_RELA_01",15)	US	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",16)	SS	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",18)	SH	GenesisVersionNow	1	PrivateTag
+(0021,"GEMS_RELA_01",19)	UL	AcqReconRecordChecksum	1	PrivateTag
+(0021,"GEMS_RELA_01",20)	DS	TableStartLocation	1	PrivateTag
+(0021,"GEMS_RELA_01",35)	SS	SeriesFromWhichPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",36)	SS	ImageFromWhichPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",37)	SS	ScreenFormat	1	PrivateTag
+(0021,"GEMS_RELA_01",4a)	LO	AnatomicalReferenceForScout	1	PrivateTag
+(0021,"GEMS_RELA_01",4e)	US	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",4f)	SS	LocationsInAcquisition	1	PrivateTag
+(0021,"GEMS_RELA_01",50)	SS	GraphicallyPrescribed	1	PrivateTag
+(0021,"GEMS_RELA_01",51)	DS	RotationFromSourceXRot	1	PrivateTag
+(0021,"GEMS_RELA_01",52)	DS	RotationFromSourceYRot	1	PrivateTag
+(0021,"GEMS_RELA_01",53)	DS	RotationFromSourceZRot	1	PrivateTag
+(0021,"GEMS_RELA_01",54)	SH	ImagePosition	3	PrivateTag
+(0021,"GEMS_RELA_01",55)	SH	ImageOrientation	6	PrivateTag
+(0021,"GEMS_RELA_01",56)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",57)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",58)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",59)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5a)	SL	IntegerSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5b)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5c)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5d)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5e)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",5f)	DS	FloatSlop	1	PrivateTag
+(0021,"GEMS_RELA_01",70)	LT	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",71)	LT	Unknown	1	PrivateTag
+(0021,"GEMS_RELA_01",81)	DS	AutoWindowLevelAlpha	1	PrivateTag
+(0021,"GEMS_RELA_01",82)	DS	AutoWindowLevelBeta	1	PrivateTag
+(0021,"GEMS_RELA_01",83)	DS	AutoWindowLevelWindow	1	PrivateTag
+(0021,"GEMS_RELA_01",84)	DS	AutoWindowLevelLevel	1	PrivateTag
+(0021,"GEMS_RELA_01",90)	SS	TubeFocalSpotPosition	1	PrivateTag
+(0021,"GEMS_RELA_01",91)	SS	BiopsyPosition	1	PrivateTag
+(0021,"GEMS_RELA_01",92)	FL	BiopsyTLocation	1	PrivateTag
+(0021,"GEMS_RELA_01",93)	FL	BiopsyRefLocation	1	PrivateTag
+
+(0045,"GEMS_SENO_02",04)	CS	AES	1	PrivateTag
+(0045,"GEMS_SENO_02",06)	DS	Angulation	1	PrivateTag
+(0045,"GEMS_SENO_02",09)	DS	RealMagnificationFactor	1	PrivateTag
+(0045,"GEMS_SENO_02",0b)	CS	SenographType	1	PrivateTag
+(0045,"GEMS_SENO_02",0c)	DS	IntegrationTime	1	PrivateTag
+(0045,"GEMS_SENO_02",0d)	DS	ROIOriginXY	1	PrivateTag
+(0045,"GEMS_SENO_02",11)	DS	ReceptorSizeCmXY	2	PrivateTag
+(0045,"GEMS_SENO_02",12)	IS	ReceptorSizePixelsXY	2	PrivateTag
+(0045,"GEMS_SENO_02",13)	ST	Screen	1	PrivateTag
+(0045,"GEMS_SENO_02",14)	DS	PixelPitchMicrons	1	PrivateTag
+(0045,"GEMS_SENO_02",15)	IS	PixelDepthBits	1	PrivateTag
+(0045,"GEMS_SENO_02",16)	IS	BinningFactorXY	2	PrivateTag
+(0045,"GEMS_SENO_02",1B)	CS	ClinicalView	1	PrivateTag
+(0045,"GEMS_SENO_02",1D)	DS	MeanOfRawGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",1E)	DS	MeanOfOffsetGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",1F)	DS	MeanOfCorrectedGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",20)	DS	MeanOfRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",21)	DS	MeanOfLogRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",22)	DS	StandardDeviationOfRawGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",23)	DS	StandardDeviationOfCorrectedGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",24)	DS	StandardDeviationOfRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",25)	DS	StandardDeviationOfLogRegionGrayLevels	1	PrivateTag
+(0045,"GEMS_SENO_02",26)	OB	MAOBuffer	1	PrivateTag
+(0045,"GEMS_SENO_02",27)	IS	SetNumber	1	PrivateTag
+(0045,"GEMS_SENO_02",28)	CS	WindowingType	1	PrivateTag
+(0045,"GEMS_SENO_02",29)	DS	WindowingParameters	1-n	PrivateTag
+(0045,"GEMS_SENO_02",2a)	IS	CrosshairCursorXCoordinates	1	PrivateTag
+(0045,"GEMS_SENO_02",2b)	IS	CrosshairCursorYCoordinates	1	PrivateTag
+(0045,"GEMS_SENO_02",39)	US	VignetteRows	1	PrivateTag
+(0045,"GEMS_SENO_02",3a)	US	VignetteColumns	1	PrivateTag
+(0045,"GEMS_SENO_02",3b)	US	VignetteBitsAllocated	1	PrivateTag
+(0045,"GEMS_SENO_02",3c)	US	VignetteBitsStored	1	PrivateTag
+(0045,"GEMS_SENO_02",3d)	US	VignetteHighBit	1	PrivateTag
+(0045,"GEMS_SENO_02",3e)	US	VignettePixelRepresentation	1	PrivateTag
+(0045,"GEMS_SENO_02",3f)	OB	VignettePixelData	1	PrivateTag
+
+(0025,"GEMS_SERS_01",06)	SS	LastPulseSequenceUsed	1	PrivateTag
+(0025,"GEMS_SERS_01",07)	SL	ImagesInSeries	1	PrivateTag
+(0025,"GEMS_SERS_01",10)	SL	LandmarkCounter	1	PrivateTag
+(0025,"GEMS_SERS_01",11)	SS	NumberOfAcquisitions	1	PrivateTag
+(0025,"GEMS_SERS_01",14)	SL	IndicatesNumberOfUpdatesToHeader	1	PrivateTag
+(0025,"GEMS_SERS_01",17)	SL	SeriesCompleteFlag	1	PrivateTag
+(0025,"GEMS_SERS_01",18)	SL	NumberOfImagesArchived	1	PrivateTag
+(0025,"GEMS_SERS_01",19)	SL	LastImageNumberUsed	1	PrivateTag
+(0025,"GEMS_SERS_01",1a)	SH	PrimaryReceiverSuiteAndHost	1	PrivateTag
+
+(0023,"GEMS_STDY_01",01)	SL	NumberOfSeriesInStudy	1	PrivateTag
+(0023,"GEMS_STDY_01",02)	SL	NumberOfUnarchivedSeries	1	PrivateTag
+(0023,"GEMS_STDY_01",10)	SS	ReferenceImageField	1	PrivateTag
+(0023,"GEMS_STDY_01",50)	SS	SummaryImage	1	PrivateTag
+(0023,"GEMS_STDY_01",70)	FD	StartTimeSecsInFirstAxial	1	PrivateTag
+(0023,"GEMS_STDY_01",74)	SL	NumberOfUpdatesToHeader	1	PrivateTag
+(0023,"GEMS_STDY_01",7d)	SS	IndicatesIfStudyHasCompleteInfo	1	PrivateTag
+
+(0033,"GEMS_YMHD_01",05)	UN	Unknown	1	PrivateTag
+(0033,"GEMS_YMHD_01",06)	UN	Unknown	1	PrivateTag
+
+(0019,"GE_GENESIS_REV3.0",39)	SS	AxialType	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",8f)	SS	SwapPhaseFrequency	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",9c)	SS	PulseSequenceName	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",9f)	SS	CoilType	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",a4)	SS	SATFatWaterBone	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",c0)	SS	BitmapOfSATSelections	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",c1)	SS	SurfaceCoilIntensityCorrectionFlag	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",cb)	SS	PhaseContrastFlowAxis	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",cc)	SS	PhaseContrastVelocityEncoding	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",d5)	SS	FractionalEcho	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",d8)	SS	VariableEchoFlag	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",d9)	DS	ConcatenatedSat	1	PrivateTag
+(0019,"GE_GENESIS_REV3.0",f2)	SS	NumberOfPhases	1	PrivateTag
+(0043,"GE_GENESIS_REV3.0",1e)	DS	DeltaStartTime	1	PrivateTag
+(0043,"GE_GENESIS_REV3.0",27)	SH	ScanPitchRatio	1	PrivateTag
+
+(0029,"INTELERAD MEDICAL SYSTEMS",01)	FD	ImageCompressionFraction	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",02)	FD	ImageQuality	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",03)	FD	ImageBytesTransferred	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",10)	SH	J2cParameterType	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",11)	US	J2cPixelRepresentation	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",12)	US	J2cBitsAllocated	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",13)	US	J2cPixelShiftValue	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",14)	US	J2cPlanarConfiguration	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",15)	DS	J2cRescaleIntercept	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",20)	LO	PixelDataMD5SumPerFrame	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",21)	US	HistogramPercentileLabels	1	PrivateTag
+(0029,"INTELERAD MEDICAL SYSTEMS",22)	FD	HistogramPercentileValues	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",01)	LO	InstitutionCode	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",02)	LO	RoutedTransferAE	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",03)	LO	SourceAE	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",04)	SH	DeferredValidation	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",05)	LO	SeriesOwner	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",06)	LO	OrderGroupNumber	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",07)	SH	StrippedPixelData	1	PrivateTag
+(3f01,"INTELERAD MEDICAL SYSTEMS",08)	SH	PendingMoveRequest	1	PrivateTag
+
+(0041,"INTEGRIS 1.0",20)	FL	AccumulatedFluoroscopyDose	1	PrivateTag
+(0041,"INTEGRIS 1.0",30)	FL	AccumulatedExposureDose	1	PrivateTag
+(0041,"INTEGRIS 1.0",40)	FL	TotalDose	1	PrivateTag
+(0041,"INTEGRIS 1.0",41)	FL	TotalNumberOfFrames	1	PrivateTag
+(0041,"INTEGRIS 1.0",50)	SQ	ExposureInformationSequence	1	PrivateTag
+(0009,"INTEGRIS 1.0",08)	CS	ExposureChannel	1-n	PrivateTag
+(0009,"INTEGRIS 1.0",32)	TM	ExposureStartTime	1	PrivateTag
+(0019,"INTEGRIS 1.0",00)	LO	APRName	1	PrivateTag
+(0019,"INTEGRIS 1.0",40)	DS	FrameRate	1	PrivateTag
+(0021,"INTEGRIS 1.0",12)	IS	ExposureNumber	1	PrivateTag
+(0029,"INTEGRIS 1.0",08)	IS	NumberOfExposureResults	1	PrivateTag
+
+(0029,"ISG shadow",70)	IS	Unknown	1	PrivateTag
+(0029,"ISG shadow",80)	IS	Unknown	1	PrivateTag
+(0029,"ISG shadow",90)	IS	Unknown	1	PrivateTag
+
+(0009,"ISI",01)	UN	SIENETGeneralPurposeIMGEF	1	PrivateTag
+
+(0009,"MERGE TECHNOLOGIES, INC.",00)	OB	Unknown	1	PrivateTag
+
+(0029,"OCULUS Optikgeraete GmbH",1010)	OB	OriginalMeasuringData	1	PrivateTag
+(0029,"OCULUS Optikgeraete GmbH",1012)	UL	OriginalMeasuringDataLength	1	PrivateTag
+(0029,"OCULUS Optikgeraete GmbH",1020)	OB	OriginalMeasuringRawData	1	PrivateTag
+(0029,"OCULUS Optikgeraete GmbH",1022)	UL	OriginalMeasuringRawDataLength	1	PrivateTag
+
+(0041,"PAPYRUS 3.0",00)	LT	PapyrusComments	1	PrivateTag
+(0041,"PAPYRUS 3.0",10)	SQ	PointerSequence	1	PrivateTag
+(0041,"PAPYRUS 3.0",11)	UL	ImagePointer	1	PrivateTag
+(0041,"PAPYRUS 3.0",12)	UL	PixelOffset	1	PrivateTag
+(0041,"PAPYRUS 3.0",13)	SQ	ImageIdentifierSequence	1	PrivateTag
+(0041,"PAPYRUS 3.0",14)	SQ	ExternalFileReferenceSequence	1	PrivateTag
+(0041,"PAPYRUS 3.0",15)	US	NumberOfImages	1	PrivateTag
+(0041,"PAPYRUS 3.0",21)	UI	ReferencedSOPClassUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",22)	UI	ReferencedSOPInstanceUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",31)	LT	ReferencedFileName	1	PrivateTag
+(0041,"PAPYRUS 3.0",32)	LT	ReferencedFilePath	1-n	PrivateTag
+(0041,"PAPYRUS 3.0",41)	UI	ReferencedImageSOPClassUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",42)	UI	ReferencedImageSOPInstanceUID	1	PrivateTag
+(0041,"PAPYRUS 3.0",50)	SQ	ImageSequence	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",00)	IS	OverlayID	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",01)	LT	LinkedOverlays	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",10)	US	OverlayRows	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",11)	US	OverlayColumns	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",40)	LO	OverlayType	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",50)	US	OverlayOrigin	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",60)	LO	Editable	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",70)	LO	OverlayFont	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",72)	LO	OverlayStyle	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",74)	US	OverlayFontSize	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",76)	LO	OverlayColor	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",78)	US	ShadowSize	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",80)	LO	FillPattern	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",82)	US	OverlayPenSize	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",a0)	LO	Label	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",a2)	LT	PostItText	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",a4)	US	AnchorPoint	2	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",b0)	LO	ROIType	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",b2)	LT	AttachedAnnotation	1	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",ba)	US	ContourPoints	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",bc)	US	MaskData	1-n	PrivateTag
+(6001-o-60ff,"PAPYRUS 3.0",c0)	SQ	UINOverlaySequence	1	PrivateTag
+
+(0009,"PAPYRUS",00)	LT	OriginalFileName	1	PrivateTag
+(0009,"PAPYRUS",10)	LT	OriginalFileLocation	1	PrivateTag
+(0009,"PAPYRUS",18)	LT	DataSetIdentifier	1	PrivateTag
+(0041,"PAPYRUS",00)	LT	PapyrusComments	1-n	PrivateTag
+(0041,"PAPYRUS",10)	US	FolderType	1	PrivateTag
+(0041,"PAPYRUS",11)	LT	PatientFolderDataSetID	1	PrivateTag
+(0041,"PAPYRUS",20)	LT	FolderName	1	PrivateTag
+(0041,"PAPYRUS",30)	DA	CreationDate	1	PrivateTag
+(0041,"PAPYRUS",32)	TM	CreationTime	1	PrivateTag
+(0041,"PAPYRUS",34)	DA	ModifiedDate	1	PrivateTag
+(0041,"PAPYRUS",36)	TM	ModifiedTime	1	PrivateTag
+(0041,"PAPYRUS",40)	LT	OwnerName	1-n	PrivateTag
+(0041,"PAPYRUS",50)	LT	FolderStatus	1	PrivateTag
+(0041,"PAPYRUS",60)	UL	NumberOfImages	1	PrivateTag
+(0041,"PAPYRUS",62)	UL	NumberOfOther	1	PrivateTag
+(0041,"PAPYRUS",a0)	LT	ExternalFolderElementDSID	1-n	PrivateTag
+(0041,"PAPYRUS",a1)	US	ExternalFolderElementDataSetType	1-n	PrivateTag
+(0041,"PAPYRUS",a2)	LT	ExternalFolderElementFileLocation	1-n	PrivateTag
+(0041,"PAPYRUS",a3)	UL	ExternalFolderElementLength	1-n	PrivateTag
+(0041,"PAPYRUS",b0)	LT	InternalFolderElementDSID	1-n	PrivateTag
+(0041,"PAPYRUS",b1)	US	InternalFolderElementDataSetType	1-n	PrivateTag
+(0041,"PAPYRUS",b2)	UL	InternalOffsetToDataSet	1-n	PrivateTag
+(0041,"PAPYRUS",b3)	UL	InternalOffsetToImage	1-n
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips Imaging DD 001", others use "PHILIPS IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2001,"Philips Imaging DD 001",01)	FL	ChemicalShift	1	PrivateTag
+(2001,"Philips Imaging DD 001",02)	IS	ChemicalShiftNumberMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",03)	FL	DiffusionBFactor	1	PrivateTag
+(2001,"Philips Imaging DD 001",04)	CS	DiffusionDirection	1	PrivateTag
+(2001,"Philips Imaging DD 001",06)	CS	ImageEnhanced	1	PrivateTag
+(2001,"Philips Imaging DD 001",07)	CS	ImageTypeEDES	1	PrivateTag
+(2001,"Philips Imaging DD 001",08)	IS	PhaseNumber	1	PrivateTag
+(2001,"Philips Imaging DD 001",09)	FL	ImagePrepulseDelay	1	PrivateTag
+(2001,"Philips Imaging DD 001",0a)	IS	SliceNumberMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",0b)	CS	SliceOrientation	1	PrivateTag
+(2001,"Philips Imaging DD 001",0c)	CS	ArrhythmiaRejection	1	PrivateTag
+(2001,"Philips Imaging DD 001",0e)	CS	CardiacCycled	1	PrivateTag
+(2001,"Philips Imaging DD 001",0f)	SS	CardiacGateWidth	1	PrivateTag
+(2001,"Philips Imaging DD 001",10)	CS	CardiacSync	1	PrivateTag
+(2001,"Philips Imaging DD 001",11)	FL	DiffusionEchoTime	1	PrivateTag
+(2001,"Philips Imaging DD 001",12)	CS	DynamicSeries	1	PrivateTag
+(2001,"Philips Imaging DD 001",13)	SL	EPIFactor	1	PrivateTag
+(2001,"Philips Imaging DD 001",14)	SL	NumberOfEchoes	1	PrivateTag
+(2001,"Philips Imaging DD 001",15)	SS	NumberOfLocations	1	PrivateTag
+(2001,"Philips Imaging DD 001",16)	SS	NumberOfPCDirections	1	PrivateTag
+(2001,"Philips Imaging DD 001",17)	SL	NumberOfPhasesMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",18)	SL	NumberOfSlicesMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",19)	CS	PartialMatrixScanned	1	PrivateTag
+(2001,"Philips Imaging DD 001",1a)	FL	PCVelocity	1-n	PrivateTag
+(2001,"Philips Imaging DD 001",1b)	FL	PrepulseDelay	1	PrivateTag
+(2001,"Philips Imaging DD 001",1c)	CS	PrepulseType	1	PrivateTag
+(2001,"Philips Imaging DD 001",1d)	IS	ReconstructionNumberMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",1f)	CS	RespirationSync	1	PrivateTag
+(2001,"Philips Imaging DD 001",20)	LO	ScanningTechnique	1	PrivateTag
+(2001,"Philips Imaging DD 001",21)	CS	SPIR	1	PrivateTag
+(2001,"Philips Imaging DD 001",22)	FL	WaterFatShift	1	PrivateTag
+(2001,"Philips Imaging DD 001",23)	DS	FlipAnglePhilips	1	PrivateTag
+(2001,"Philips Imaging DD 001",24)	CS	SeriesIsInteractive	1	PrivateTag
+(2001,"Philips Imaging DD 001",25)	SH	EchoTimeDisplayMR	1	PrivateTag
+(2001,"Philips Imaging DD 001",26)	CS	PresentationStateSubtractionActive	1	PrivateTag
+(2001,"Philips Imaging DD 001",2d)	SS	StackNumberOfSlices	1	PrivateTag
+(2001,"Philips Imaging DD 001",32)	FL	StackRadialAngle	1	PrivateTag
+(2001,"Philips Imaging DD 001",33)	CS	StackRadialAxis	1	PrivateTag
+(2001,"Philips Imaging DD 001",35)	SS	StackSliceNumber	1	PrivateTag
+(2001,"Philips Imaging DD 001",36)	CS	StackType	1	PrivateTag
+(2001,"Philips Imaging DD 001",3f)	CS	ZoomMode	1	PrivateTag
+(2001,"Philips Imaging DD 001",58)	UL	ContrastTransferTaste	1	PrivateTag
+(2001,"Philips Imaging DD 001",5f)	SQ	StackSequence	1	PrivateTag
+(2001,"Philips Imaging DD 001",60)	SL	NumberOfStacks	1	PrivateTag
+(2001,"Philips Imaging DD 001",61)	CS	SeriesTransmitted	1	PrivateTag
+(2001,"Philips Imaging DD 001",62)	CS	SeriesCommitted	1	PrivateTag
+(2001,"Philips Imaging DD 001",63)	CS	ExaminationSource	1	PrivateTag
+(2001,"Philips Imaging DD 001",67)	CS	LinearPresentationGLTrafoShapeSub	1	PrivateTag
+(2001,"Philips Imaging DD 001",77)	CS	GLTrafoType	1	PrivateTag
+(2001,"Philips Imaging DD 001",7b)	IS	AcquisitionNumber	1	PrivateTag
+(2001,"Philips Imaging DD 001",81)	IS	NumberOfDynamicScans	1	PrivateTag
+(2001,"Philips Imaging DD 001",9f)	US	PixelProcessingKernelSize	1	PrivateTag
+(2001,"Philips Imaging DD 001",a1)	CS	IsRawImage	1	PrivateTag
+(2001,"Philips Imaging DD 001",f1)	FL	ProspectiveMotionCorrection	1	PrivateTag
+(2001,"Philips Imaging DD 001",f2)	FL	RetrospectiveMotionCorrection	1	PrivateTag
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips Imaging DD 001", others use "PHILIPS IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2001,"PHILIPS IMAGING DD 001",01)	FL	ChemicalShift	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",02)	IS	ChemicalShiftNumberMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",03)	FL	DiffusionBFactor	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",04)	CS	DiffusionDirection	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",06)	CS	ImageEnhanced	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",07)	CS	ImageTypeEDES	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",08)	IS	PhaseNumber	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",09)	FL	ImagePrepulseDelay	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0a)	IS	SliceNumberMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0b)	CS	SliceOrientation	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0c)	CS	ArrhythmiaRejection	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0e)	CS	CardiacCycled	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",0f)	SS	CardiacGateWidth	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",10)	CS	CardiacSync	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",11)	FL	DiffusionEchoTime	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",12)	CS	DynamicSeries	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",13)	SL	EPIFactor	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",14)	SL	NumberOfEchoes	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",15)	SS	NumberOfLocations	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",16)	SS	NumberOfPCDirections	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",17)	SL	NumberOfPhasesMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",18)	SL	NumberOfSlicesMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",19)	CS	PartialMatrixScanned	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1a)	FL	PCVelocity	1-n	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1b)	FL	PrepulseDelay	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1c)	CS	PrepulseType	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1d)	IS	ReconstructionNumberMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",1f)	CS	RespirationSync	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",20)	LO	ScanningTechnique	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",21)	CS	SPIR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",22)	FL	WaterFatShift	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",23)	DS	FlipAnglePhilips	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",24)	CS	SeriesIsInteractive	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",25)	SH	EchoTimeDisplayMR	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",26)	CS	PresentationStateSubtractionActive	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",2d)	SS	StackNumberOfSlices	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",32)	FL	StackRadialAngle	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",33)	CS	StackRadialAxis	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",35)	SS	StackSliceNumber	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",36)	CS	StackType	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",3f)	CS	ZoomMode	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",58)	UL	ContrastTransferTaste	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",5f)	SQ	StackSequence	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",60)	SL	NumberOfStacks	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",61)	CS	SeriesTransmitted	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",62)	CS	SeriesCommitted	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",63)	CS	ExaminationSource	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",67)	CS	LinearPresentationGLTrafoShapeSub	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",77)	CS	GLTrafoType	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",7b)	IS	AcquisitionNumber	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",81)	IS	NumberOfDynamicScans	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",9f)	US	PixelProcessingKernelSize	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",a1)	CS	IsRawImage	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",f1)	FL	ProspectiveMotionCorrection	1	PrivateTag
+(2001,"PHILIPS IMAGING DD 001",f2)	FL	RetrospectiveMotionCorrection	1	PrivateTag
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips MR Imaging DD 001", others use "PHILIPS MR IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2005,"Philips MR Imaging DD 001",05)	CS	SynergyReconstructionType	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",1e)	SH	MIPProtocol	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",1f)	SH	MPRProtocol	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",20)	SL	NumberOfChemicalShifts	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",2d)	SS	NumberOfStackSlices	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",83)	SQ	Unknown	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",a1)	CS	SyncraScanType	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",b0)	FL	DiffusionDirectionRL	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",b1)	FL	DiffusionDirectionAP	1	PrivateTag
+(2005,"Philips MR Imaging DD 001",b2)	FL	DiffusionDirectionFH	1	PrivateTag
+
+(2005,"Philips MR Imaging DD 005",02)	SQ	Unknown	1	PrivateTag
+
+# Note: Some Philips devices use these private tags with reservation value
+# "Philips MR Imaging DD 001", others use "PHILIPS MR IMAGING DD 001". All attributes
+# should thus be present twice in this dictionary, once for each spelling variant.
+#
+(2005,"PHILIPS MR IMAGING DD 001",05)	CS	SynergyReconstructionType	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",1e)	SH	MIPProtocol	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",1f)	SH	MPRProtocol	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",20)	SL	NumberOfChemicalShifts	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",2d)	SS	NumberOfStackSlices	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",83)	SQ	Unknown	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",a1)	CS	SyncraScanType	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",b0)	FL	DiffusionDirectionRL	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",b1)	FL	DiffusionDirectionAP	1	PrivateTag
+(2005,"PHILIPS MR IMAGING DD 001",b2)	FL	DiffusionDirectionFH	1	PrivateTag
+
+(0019,"PHILIPS MR R5.5/PART",1000)	DS	FieldOfView	1	PrivateTag
+(0019,"PHILIPS MR R5.6/PART",1000)	DS	FieldOfView	1	PrivateTag
+
+(0019,"PHILIPS MR SPECTRO;1",01)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",02)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",03)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",04)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",05)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",06)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",07)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",08)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",09)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",10)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",12)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",13)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",14)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",15)	US	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",16)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",17)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",18)	UN	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",20)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",21)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",22)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",23)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",24)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",25)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",26)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",27)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",28)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",29)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",31)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",32)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",41)	LT	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",42)	IS	Unknown	2	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",43)	IS	Unknown	2	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",45)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",46)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",47)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",48)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",49)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",50)	UN	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",60)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",61)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",70)	UN	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",71)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",72)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",73)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",74)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",76)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",77)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",78)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",79)	US	Unknown	1	PrivateTag
+(0019,"PHILIPS MR SPECTRO;1",80)	IS	Unknown	1	PrivateTag
+
+(0009,"PHILIPS MR",10)	LO	SPIRelease	1	PrivateTag
+(0009,"PHILIPS MR",12)	LO	Unknown	1	PrivateTag
+
+(0019,"PHILIPS MR/LAST",09)	DS	MainMagneticField	1	PrivateTag
+(0019,"PHILIPS MR/LAST",0e)	IS	FlowCompensation	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b1)	IS	MinimumRRInterval	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b2)	IS	MaximumRRInterval	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b3)	IS	NumberOfRejections	1	PrivateTag
+(0019,"PHILIPS MR/LAST",b4)	IS	NumberOfRRIntervals	1-n	PrivateTag
+(0019,"PHILIPS MR/LAST",b5)	IS	ArrhythmiaRejection	1	PrivateTag
+(0019,"PHILIPS MR/LAST",c0)	DS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/LAST",c6)	IS	CycledMultipleSlice	1	PrivateTag
+(0019,"PHILIPS MR/LAST",ce)	IS	REST	1	PrivateTag
+(0019,"PHILIPS MR/LAST",d5)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/LAST",d6)	IS	FourierInterpolation	1	PrivateTag
+(0019,"PHILIPS MR/LAST",d9)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/LAST",e0)	IS	Prepulse	1	PrivateTag
+(0019,"PHILIPS MR/LAST",e1)	DS	PrepulseDelay	1	PrivateTag
+(0019,"PHILIPS MR/LAST",e2)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/LAST",e3)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f0)	LT	WSProtocolString1	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f1)	LT	WSProtocolString2	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f2)	LT	WSProtocolString3	1	PrivateTag
+(0019,"PHILIPS MR/LAST",f3)	LT	WSProtocolString4	1	PrivateTag
+(0021,"PHILIPS MR/LAST",00)	IS	Unknown	1	PrivateTag
+(0021,"PHILIPS MR/LAST",10)	IS	Unknown	1	PrivateTag
+(0021,"PHILIPS MR/LAST",20)	IS	Unknown	1	PrivateTag
+(0021,"PHILIPS MR/LAST",21)	DS	SliceGap	1	PrivateTag
+(0021,"PHILIPS MR/LAST",22)	DS	StackRadialAngle	1	PrivateTag
+(0027,"PHILIPS MR/LAST",00)	US	Unknown	1	PrivateTag
+(0027,"PHILIPS MR/LAST",11)	US	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",12)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",13)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",14)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",15)	DS	Unknown	1-n	PrivateTag
+(0027,"PHILIPS MR/LAST",16)	LO	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",10)	DS	FPMin	1	PrivateTag
+(0029,"PHILIPS MR/LAST",20)	DS	FPMax	1	PrivateTag
+(0029,"PHILIPS MR/LAST",30)	DS	ScaledMinimum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",40)	DS	ScaledMaximum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",50)	DS	WindowMinimum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",60)	DS	WindowMaximum	1	PrivateTag
+(0029,"PHILIPS MR/LAST",61)	IS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",70)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",71)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",72)	IS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/LAST",80)	IS	ViewCenter	1	PrivateTag
+(0029,"PHILIPS MR/LAST",81)	IS	ViewSize	1	PrivateTag
+(0029,"PHILIPS MR/LAST",82)	IS	ViewZoom	1	PrivateTag
+(0029,"PHILIPS MR/LAST",83)	IS	ViewTransform	1	PrivateTag
+(6001,"PHILIPS MR/LAST",00)	LT	Unknown	1	PrivateTag
+
+(0019,"PHILIPS MR/PART",1000)	DS	FieldOfView	1	PrivateTag
+(0019,"PHILIPS MR/PART",1005)	DS	CCAngulation	1	PrivateTag
+(0019,"PHILIPS MR/PART",1006)	DS	APAngulation	1	PrivateTag
+(0019,"PHILIPS MR/PART",1007)	DS	LRAngulation	1	PrivateTag
+(0019,"PHILIPS MR/PART",1008)	IS	PatientPosition	1	PrivateTag
+(0019,"PHILIPS MR/PART",1009)	IS	PatientOrientation	1	PrivateTag
+(0019,"PHILIPS MR/PART",100a)	IS	SliceOrientation	1	PrivateTag
+(0019,"PHILIPS MR/PART",100b)	DS	LROffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",100c)	DS	CCOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",100d)	DS	APOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",100e)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",100f)	IS	NumberOfSlices	1	PrivateTag
+(0019,"PHILIPS MR/PART",1010)	DS	SliceFactor	1	PrivateTag
+(0019,"PHILIPS MR/PART",1011)	DS	EchoTimes	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",1015)	IS	DynamicStudy	1	PrivateTag
+(0019,"PHILIPS MR/PART",1018)	DS	HeartbeatInterval	1	PrivateTag
+(0019,"PHILIPS MR/PART",1019)	DS	RepetitionTimeFFE	1	PrivateTag
+(0019,"PHILIPS MR/PART",101a)	DS	FFEFlipAngle	1	PrivateTag
+(0019,"PHILIPS MR/PART",101b)	IS	NumberOfScans	1	PrivateTag
+(0019,"PHILIPS MR/PART",1021)	DS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",1022)	DS	DynamicScanTimeBegin	1	PrivateTag
+(0019,"PHILIPS MR/PART",1024)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",1064)	DS	RepetitionTimeSE	1	PrivateTag
+(0019,"PHILIPS MR/PART",1065)	DS	RepetitionTimeIR	1	PrivateTag
+(0019,"PHILIPS MR/PART",1069)	IS	NumberOfPhases	1	PrivateTag
+(0019,"PHILIPS MR/PART",106a)	IS	CardiacFrequency	1	PrivateTag
+(0019,"PHILIPS MR/PART",106b)	DS	InversionDelay	1	PrivateTag
+(0019,"PHILIPS MR/PART",106c)	DS	GateDelay	1	PrivateTag
+(0019,"PHILIPS MR/PART",106d)	DS	GateWidth	1	PrivateTag
+(0019,"PHILIPS MR/PART",106e)	DS	TriggerDelayTime	1	PrivateTag
+(0019,"PHILIPS MR/PART",1080)	IS	NumberOfChemicalShifts	1	PrivateTag
+(0019,"PHILIPS MR/PART",1081)	DS	ChemicalShift	1	PrivateTag
+(0019,"PHILIPS MR/PART",1084)	IS	NumberOfRows	1	PrivateTag
+(0019,"PHILIPS MR/PART",1085)	IS	NumberOfSamples	1	PrivateTag
+(0019,"PHILIPS MR/PART",1094)	LO	MagnetizationTransferContrast	1	PrivateTag
+(0019,"PHILIPS MR/PART",1095)	LO	SpectralPresaturationWithInversionRecovery	1	PrivateTag
+(0019,"PHILIPS MR/PART",1096)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",1097)	LO	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a0)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a1)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a3)	DS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10a4)	CS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10c8)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10c9)	IS	FoldoverDirectionTransverse	1	PrivateTag
+(0019,"PHILIPS MR/PART",10ca)	IS	FoldoverDirectionSagittal	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cb)	IS	FoldoverDirectionCoronal	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cc)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cd)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10ce)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10cf)	IS	NumberOfEchoes	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d0)	IS	ScanResolution	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d2)	LO	WaterFatShift	2	PrivateTag
+(0019,"PHILIPS MR/PART",10d4)	IS	ArtifactReduction	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d5)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d6)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d7)	DS	ScanPercentage	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d8)	IS	Halfscan	1	PrivateTag
+(0019,"PHILIPS MR/PART",10d9)	IS	EPIFactor	1	PrivateTag
+(0019,"PHILIPS MR/PART",10da)	IS	TurboFactor	1	PrivateTag
+(0019,"PHILIPS MR/PART",10db)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",10e0)	IS	PercentageOfScanCompleted	1	PrivateTag
+(0019,"PHILIPS MR/PART",10e1)	IS	Unknown	1	PrivateTag
+(0019,"PHILIPS MR/PART",1100)	IS	NumberOfStacks	1	PrivateTag
+(0019,"PHILIPS MR/PART",1101)	IS	StackType	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",1102)	IS	Unknown	1-n	PrivateTag
+(0019,"PHILIPS MR/PART",110b)	DS	LROffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",110c)	DS	CCOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",110d)	DS	APOffcenter	1	PrivateTag
+(0019,"PHILIPS MR/PART",1145)	IS	ReconstructionResolution	1	PrivateTag
+(0019,"PHILIPS MR/PART",11fc)	IS	ResonanceFrequency	1	PrivateTag
+(0019,"PHILIPS MR/PART",12c0)	DS	TriggerDelayTimes	1	PrivateTag
+(0019,"PHILIPS MR/PART",12e0)	IS	PrepulseType	1	PrivateTag
+(0019,"PHILIPS MR/PART",12e1)	DS	PrepulseDelay	1	PrivateTag
+(0019,"PHILIPS MR/PART",12e3)	DS	PhaseContrastVelocity	1	PrivateTag
+(0021,"PHILIPS MR/PART",1000)	IS	ReconstructionNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1010)	IS	ImageType	1	PrivateTag
+(0021,"PHILIPS MR/PART",1020)	IS	SliceNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1030)	IS	EchoNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1031)	DS	PatientReferenceID	1	PrivateTag
+(0021,"PHILIPS MR/PART",1035)	IS	ChemicalShiftNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1040)	IS	PhaseNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1050)	IS	DynamicScanNumber	1	PrivateTag
+(0021,"PHILIPS MR/PART",1060)	IS	NumberOfRowsInObject	1	PrivateTag
+(0021,"PHILIPS MR/PART",1061)	IS	RowNumber	1-n	PrivateTag
+(0021,"PHILIPS MR/PART",1062)	IS	Unknown	1-n	PrivateTag
+(0021,"PHILIPS MR/PART",1100)	DA	ScanDate	1	PrivateTag
+(0021,"PHILIPS MR/PART",1110)	TM	ScanTime	1	PrivateTag
+(0021,"PHILIPS MR/PART",1221)	IS	SliceGap	1	PrivateTag
+(0029,"PHILIPS MR/PART",00)	DS	Unknown	2	PrivateTag
+(0029,"PHILIPS MR/PART",04)	US	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",10)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",11)	DS	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",20)	LO	Unknown	1	PrivateTag
+(0029,"PHILIPS MR/PART",31)	DS	Unknown	2	PrivateTag
+(0029,"PHILIPS MR/PART",32)	DS	Unknown	2	PrivateTag
+(0029,"PHILIPS MR/PART",c3)	IS	ScanResolution	1	PrivateTag
+(0029,"PHILIPS MR/PART",c4)	IS	FieldOfView	1	PrivateTag
+(0029,"PHILIPS MR/PART",d5)	LT	SliceThickness	1	PrivateTag
+
+(0019,"PHILIPS-MR-1",11)	IS	ChemicalShiftNumber	1	PrivateTag
+(0019,"PHILIPS-MR-1",12)	IS	PhaseNumber	1	PrivateTag
+(0021,"PHILIPS-MR-1",01)	IS	ReconstructionNumber	1	PrivateTag
+(0021,"PHILIPS-MR-1",02)	IS	SliceNumber	1	PrivateTag
+
+(7001,"Picker NM Private Group",01)	UI	Unknown	1	PrivateTag
+(7001,"Picker NM Private Group",02)	OB	Unknown	1	PrivateTag
+
+(0019,"SIEMENS CM VA0  ACQU",10)	LT	ParameterFileName	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",11)	LO	SequenceFileName	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",12)	LT	SequenceFileOwner	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",13)	LT	SequenceDescription	1	PrivateTag
+(0019,"SIEMENS CM VA0  ACQU",14)	LT	EPIFileName	1	PrivateTag
+
+(0009,"SIEMENS CM VA0  CMS",00)	DS	NumberOfMeasurements	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",10)	LT	StorageMode	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",12)	UL	EvaluationMaskImage	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",26)	DA	LastMoveDate	1	PrivateTag
+(0009,"SIEMENS CM VA0  CMS",27)	TM	LastMoveTime	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",0a)	LT	Unknown	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",10)	DA	RegistrationDate	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",11)	TM	RegistrationTime	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",22)	LT	Unknown	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",23)	DS	UsedPatientWeight	1	PrivateTag
+(0011,"SIEMENS CM VA0  CMS",40)	IS	OrganCode	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",00)	LT	ModifyingPhysician	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",10)	DA	ModificationDate	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",12)	TM	ModificationTime	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",20)	LO	PatientName	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",22)	LO	PatientId	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",30)	DA	PatientBirthdate	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",31)	DS	PatientWeight	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",32)	LT	PatientsMaidenName	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",33)	LT	ReferringPhysician	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",34)	LT	AdmittingDiagnosis	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",35)	LO	PatientSex	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",40)	LO	ProcedureDescription	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",42)	LO	RestDirection	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",44)	LO	PatientPosition	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",46)	LT	ViewDirection	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",50)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",51)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",52)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",53)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",54)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",55)	LT	Unknown	1	PrivateTag
+(0013,"SIEMENS CM VA0  CMS",56)	LT	Unknown	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",10)	DS	NetFrequency	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",20)	LT	MeasurementMode	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",30)	LT	CalculationMode	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",50)	IS	NoiseLevel	1	PrivateTag
+(0019,"SIEMENS CM VA0  CMS",60)	IS	NumberOfDataBytes	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",20)	DS	FoV	2	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",22)	DS	ImageMagnificationFactor	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",24)	DS	ImageScrollOffset	2	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",26)	IS	ImagePixelOffset	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",30)	LT	ViewDirection	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",32)	CS	PatientRestDirection	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",60)	DS	ImagePosition	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",61)	DS	ImageNormal	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",63)	DS	ImageDistance	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",65)	US	ImagePositioningHistoryMask	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",6a)	DS	ImageRow	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",6b)	DS	ImageColumn	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",70)	LT	PatientOrientationSet1	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",71)	LT	PatientOrientationSet2	3	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",80)	LT	StudyName	1	PrivateTag
+(0021,"SIEMENS CM VA0  CMS",82)	LT	StudyType	3	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",10)	LT	WindowStyle	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",11)	LT	Unknown	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",13)	LT	Unknown	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",20)	LT	PixelQualityCode	3	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",22)	IS	PixelQualityValue	3	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",50)	LT	ArchiveCode	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",51)	LT	ExposureCode	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",52)	LT	SortCode	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",53)	LT	Unknown	1	PrivateTag
+(0029,"SIEMENS CM VA0  CMS",60)	LT	Splash	1	PrivateTag
+(0051,"SIEMENS CM VA0  CMS",10)	LT	ImageText	1-n	PrivateTag
+(6021,"SIEMENS CM VA0  CMS",00)	LT	ImageGraphicsFormatCode	1	PrivateTag
+(6021,"SIEMENS CM VA0  CMS",10)	LT	ImageGraphics	1	PrivateTag
+(7fe1,"SIEMENS CM VA0  CMS",00)	OB	BinaryData	1-n	PrivateTag
+
+(0009,"SIEMENS CM VA0  LAB",10)	LT	GeneratorIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",11)	LT	GantryIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",12)	LT	X-RayTubeIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",13)	LT	DetectorIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",14)	LT	DASIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",15)	LT	SMIIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",16)	LT	CPUIdentificationLabel	1	PrivateTag
+(0009,"SIEMENS CM VA0  LAB",20)	LT	HeaderVersion	1	PrivateTag
+
+(0029,"SIEMENS CSA HEADER",08)	CS	CSAImageHeaderType	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",09)	LO	CSAImageHeaderVersion	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",10)	OB	CSAImageHeaderInfo	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",18)	CS	CSASeriesHeaderType	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",19)	LO	CSASeriesHeaderVersion	1	PrivateTag
+(0029,"SIEMENS CSA HEADER",20)	OB	CSASeriesHeaderInfo	1	PrivateTag
+
+(0029,"SIEMENS CSA NON-IMAGE",08)	CS	CSADataType	1	PrivateTag
+(0029,"SIEMENS CSA NON-IMAGE",09)	LO	CSADataVersion	1	PrivateTag
+(0029,"SIEMENS CSA NON-IMAGE",10)	OB	CSADataInfo	1	PrivateTag
+(7FE1,"SIEMENS CSA NON-IMAGE",10)	OB	CSAData	1	PrivateTag
+
+(0019,"SIEMENS CT VA0  COAD",10)	DS	DistanceSourceToSourceSideCollimator	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",11)	DS	DistanceSourceToDetectorSideCollimator	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",20)	IS	NumberOfPossibleChannels	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",21)	IS	MeanChannelNumber	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",22)	DS	DetectorSpacing	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",23)	DS	DetectorCenter	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",24)	DS	ReadingIntegrationTime	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",50)	DS	DetectorAlignment	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",52)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",54)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",60)	DS	FocusAlignment	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",65)	UL	FocalSpotDeflectionAmplitude	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",66)	UL	FocalSpotDeflectionPhase	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",67)	UL	FocalSpotDeflectionOffset	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",70)	DS	WaterScalingFactor	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",71)	DS	InterpolationFactor	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",80)	LT	PatientRegion	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",82)	LT	PatientPhaseOfLife	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",90)	DS	OsteoOffset	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",92)	DS	OsteoRegressionLineSlope	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",93)	DS	OsteoRegressionLineIntercept	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",94)	DS	OsteoStandardizationCode	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",96)	IS	OsteoPhantomNumber	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A3)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A4)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A5)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A6)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A7)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A8)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",A9)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AA)	LT	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AB)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AC)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AD)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AE)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",AF)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",B0)	DS	FeedPerRotation	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",BD)	IS	PulmoTriggerLevel	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",BE)	DS	ExpiratoricReserveVolume	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",BF)	DS	VitalCapacity	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C0)	DS	PulmoWater	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C1)	DS	PulmoAir	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C2)	DA	PulmoDate	1	PrivateTag
+(0019,"SIEMENS CT VA0  COAD",C3)	TM	PulmoTime	1	PrivateTag
+
+(0019,"SIEMENS CT VA0  GEN",10)	DS	SourceSideCollimatorAperture	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",11)	DS	DetectorSideCollimatorAperture	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",20)	DS	ExposureTime	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",21)	DS	ExposureCurrent	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",25)	DS	KVPGeneratorPowerCurrent	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",26)	DS	GeneratorVoltage	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",40)	UL	MasterControlMask	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",42)	US	ProcessingMask	5	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",44)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",45)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",62)	IS	NumberOfVirtuellChannels	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",70)	IS	NumberOfReadings	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",71)	LT	Unknown	1-n	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",74)	IS	NumberOfProjections	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",75)	IS	NumberOfBytes	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",80)	LT	ReconstructionAlgorithmSet	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",81)	LT	ReconstructionAlgorithmIndex	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",82)	LT	RegenerationSoftwareVersion	1	PrivateTag
+(0019,"SIEMENS CT VA0  GEN",88)	DS	Unknown	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",10)	IS	RotationAngle	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",11)	IS	StartAngle	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",20)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",30)	IS	TopogramTubePosition	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",32)	DS	LengthOfTopogram	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",34)	DS	TopogramCorrectionFactor	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",36)	DS	MaximumTablePosition	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",40)	IS	TableMoveDirectionCode	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",45)	IS	VOIStartRow	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",46)	IS	VOIStopRow	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",47)	IS	VOIStartColumn	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",48)	IS	VOIStopColumn	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",49)	IS	VOIStartSlice	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",4a)	IS	VOIStopSlice	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",50)	IS	VectorStartRow	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",51)	IS	VectorRowStep	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",52)	IS	VectorStartColumn	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",53)	IS	VectorColumnStep	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",60)	IS	RangeTypeCode	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",62)	IS	ReferenceTypeCode	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",70)	DS	ObjectOrientation	3	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",72)	DS	LightOrientation	3	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",75)	DS	LightBrightness	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",76)	DS	LightContrast	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",7a)	IS	OverlayThreshold	2	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",7b)	IS	SurfaceThreshold	2	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",7c)	IS	GreyScaleThreshold	2	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",a0)	DS	Unknown	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",a2)	LT	Unknown	1	PrivateTag
+(0021,"SIEMENS CT VA0  GEN",a7)	LT	Unknown	1	PrivateTag
+
+(0009,"SIEMENS CT VA0  IDE",10)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",30)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",31)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",32)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",34)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",40)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",42)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",50)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  IDE",51)	LT	Unknown	1	PrivateTag
+
+(0009,"SIEMENS CT VA0  ORI",20)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS CT VA0  ORI",30)	LT	Unknown	1	PrivateTag
+
+(6021,"SIEMENS CT VA0  OST",00)	LT	OsteoContourComment	1	PrivateTag
+(6021,"SIEMENS CT VA0  OST",10)	US	OsteoContourBuffer	256	PrivateTag
+
+(0021,"SIEMENS CT VA0  RAW",10)	UL	CreationMask	2	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",20)	UL	EvaluationMask	2	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",30)	US	ExtendedProcessingMask	7	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",40)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",41)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",42)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",43)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",44)	US	Unknown	1-n	PrivateTag
+(0021,"SIEMENS CT VA0  RAW",50)	LT	Unknown	1	PrivateTag
+
+(0009,"SIEMENS DICOM",10)	UN	Unknown	1	PrivateTag
+(0009,"SIEMENS DICOM",12)	LT	Unknown	1	PrivateTag
+
+(0019,"SIEMENS DLR.01",10)	LT	MeasurementMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",11)	LT	ImageType	1	PrivateTag
+(0019,"SIEMENS DLR.01",15)	LT	SoftwareVersion	1	PrivateTag
+(0019,"SIEMENS DLR.01",20)	LT	MPMCode	1	PrivateTag
+(0019,"SIEMENS DLR.01",21)	LT	Latitude	1	PrivateTag
+(0019,"SIEMENS DLR.01",22)	LT	Sensitivity	1	PrivateTag
+(0019,"SIEMENS DLR.01",23)	LT	EDR	1	PrivateTag
+(0019,"SIEMENS DLR.01",24)	LT	LFix	1	PrivateTag
+(0019,"SIEMENS DLR.01",25)	LT	SFix	1	PrivateTag
+(0019,"SIEMENS DLR.01",26)	LT	PresetMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",27)	LT	Region	1	PrivateTag
+(0019,"SIEMENS DLR.01",28)	LT	Subregion	1	PrivateTag
+(0019,"SIEMENS DLR.01",30)	LT	Orientation	1	PrivateTag
+(0019,"SIEMENS DLR.01",31)	LT	MarkOnFilm	1	PrivateTag
+(0019,"SIEMENS DLR.01",32)	LT	RotationOnDRC	1	PrivateTag
+(0019,"SIEMENS DLR.01",40)	LT	ReaderType	1	PrivateTag
+(0019,"SIEMENS DLR.01",41)	LT	SubModality	1	PrivateTag
+(0019,"SIEMENS DLR.01",42)	LT	ReaderSerialNumber	1	PrivateTag
+(0019,"SIEMENS DLR.01",50)	LT	CassetteScale	1	PrivateTag
+(0019,"SIEMENS DLR.01",51)	LT	CassetteMatrix	1	PrivateTag
+(0019,"SIEMENS DLR.01",52)	LT	CassetteSubmatrix	1	PrivateTag
+(0019,"SIEMENS DLR.01",53)	LT	Barcode	1	PrivateTag
+(0019,"SIEMENS DLR.01",60)	LT	ContrastType	1	PrivateTag
+(0019,"SIEMENS DLR.01",61)	LT	RotationAmount	1	PrivateTag
+(0019,"SIEMENS DLR.01",62)	LT	RotationCenter	1	PrivateTag
+(0019,"SIEMENS DLR.01",63)	LT	DensityShift	1	PrivateTag
+(0019,"SIEMENS DLR.01",64)	US	FrequencyRank	1	PrivateTag
+(0019,"SIEMENS DLR.01",65)	LT	FrequencyEnhancement	1	PrivateTag
+(0019,"SIEMENS DLR.01",66)	LT	FrequencyType	1	PrivateTag
+(0019,"SIEMENS DLR.01",67)	LT	KernelLength	1	PrivateTag
+(0019,"SIEMENS DLR.01",68)	UL	KernelMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",69)	UL	ConvolutionMode	1	PrivateTag
+(0019,"SIEMENS DLR.01",70)	LT	PLASource	1	PrivateTag
+(0019,"SIEMENS DLR.01",71)	LT	PLADestination	1	PrivateTag
+(0019,"SIEMENS DLR.01",75)	LT	UIDOriginalImage	1	PrivateTag
+(0019,"SIEMENS DLR.01",76)	LT	Unknown	1	PrivateTag
+(0019,"SIEMENS DLR.01",80)	LT	ReaderHeader	1	PrivateTag
+(0019,"SIEMENS DLR.01",90)	LT	PLAOfSecondaryDestination	1	PrivateTag
+(0019,"SIEMENS DLR.01",a0)	DS	Unknown	1	PrivateTag
+(0019,"SIEMENS DLR.01",a1)	DS	Unknown	1	PrivateTag
+(0041,"SIEMENS DLR.01",10)	US	NumberOfHardcopies	1	PrivateTag
+(0041,"SIEMENS DLR.01",20)	LT	FilmFormat	1	PrivateTag
+(0041,"SIEMENS DLR.01",30)	LT	FilmSize	1	PrivateTag
+(0041,"SIEMENS DLR.01",31)	LT	FullFilmFormat	1	PrivateTag
+
+(0003,"SIEMENS ISI",08)	US	ISICommandField	1	PrivateTag
+(0003,"SIEMENS ISI",11)	US	AttachIDApplicationCode	1	PrivateTag
+(0003,"SIEMENS ISI",12)	UL	AttachIDMessageCount	1	PrivateTag
+(0003,"SIEMENS ISI",13)	DA	AttachIDDate	1	PrivateTag
+(0003,"SIEMENS ISI",14)	TM	AttachIDTime	1	PrivateTag
+(0003,"SIEMENS ISI",20)	US	MessageType	1	PrivateTag
+(0003,"SIEMENS ISI",30)	DA	MaxWaitingDate	1	PrivateTag
+(0003,"SIEMENS ISI",31)	TM	MaxWaitingTime	1	PrivateTag
+(0009,"SIEMENS ISI",01)	UN	RISPatientInfoIMGEF	1	PrivateTag
+(0011,"SIEMENS ISI",03)	LT	PatientUID	1	PrivateTag
+(0011,"SIEMENS ISI",04)	LT	PatientID	1	PrivateTag
+(0011,"SIEMENS ISI",0a)	LT	CaseID	1	PrivateTag
+(0011,"SIEMENS ISI",22)	LT	RequestID	1	PrivateTag
+(0011,"SIEMENS ISI",23)	LT	ExaminationUID	1	PrivateTag
+(0011,"SIEMENS ISI",a1)	DA	PatientRegistrationDate	1	PrivateTag
+(0011,"SIEMENS ISI",a2)	TM	PatientRegistrationTime	1	PrivateTag
+(0011,"SIEMENS ISI",b0)	LT	PatientLastName	1	PrivateTag
+(0011,"SIEMENS ISI",b2)	LT	PatientFirstName	1	PrivateTag
+(0011,"SIEMENS ISI",b4)	LT	PatientHospitalStatus	1	PrivateTag
+(0011,"SIEMENS ISI",bc)	TM	CurrentLocationTime	1	PrivateTag
+(0011,"SIEMENS ISI",c0)	LT	PatientInsuranceStatus	1	PrivateTag
+(0011,"SIEMENS ISI",d0)	LT	PatientBillingType	1	PrivateTag
+(0011,"SIEMENS ISI",d2)	LT	PatientBillingAddress	1	PrivateTag
+(0031,"SIEMENS ISI",12)	LT	ExaminationReason	1	PrivateTag
+(0031,"SIEMENS ISI",30)	DA	RequestedDate	1	PrivateTag
+(0031,"SIEMENS ISI",32)	TM	WorklistRequestStartTime	1	PrivateTag
+(0031,"SIEMENS ISI",33)	TM	WorklistRequestEndTime	1	PrivateTag
+(0031,"SIEMENS ISI",4a)	TM	RequestedTime	1	PrivateTag
+(0031,"SIEMENS ISI",80)	LT	RequestedLocation	1	PrivateTag
+(0055,"SIEMENS ISI",46)	LT	CurrentWard	1	PrivateTag
+(0193,"SIEMENS ISI",02)	DS	RISKey	1	PrivateTag
+(0307,"SIEMENS ISI",01)	UN	RISWorklistIMGEF	1	PrivateTag
+(0309,"SIEMENS ISI",01)	UN	RISReportIMGEF	1	PrivateTag
+(4009,"SIEMENS ISI",01)	LT	ReportID	1	PrivateTag
+(4009,"SIEMENS ISI",20)	LT	ReportStatus	1	PrivateTag
+(4009,"SIEMENS ISI",30)	DA	ReportCreationDate	1	PrivateTag
+(4009,"SIEMENS ISI",70)	LT	ReportApprovingPhysician	1	PrivateTag
+(4009,"SIEMENS ISI",e0)	LT	ReportText	1	PrivateTag
+(4009,"SIEMENS ISI",e1)	LT	ReportAuthor	1	PrivateTag
+(4009,"SIEMENS ISI",e3)	LT	ReportingRadiologist	1	PrivateTag
+
+(0029,"SIEMENS MED DISPLAY",04)	LT	PhotometricInterpretation	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",10)	US	RowsOfSubmatrix	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",11)	US	ColumnsOfSubmatrix	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",20)	US	Unknown	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",21)	US	Unknown	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",50)	US	OriginOfSubmatrix	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",99)	LT	ShutterType	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",a0)	US	RowsOfRectangularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",a1)	US	ColumnsOfRectangularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",a2)	US	OriginOfRectangularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",b0)	US	RadiusOfCircularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",b2)	US	OriginOfCircularShutter	1	PrivateTag
+(0029,"SIEMENS MED DISPLAY",c1)	US	ContourOfIrregularShutter	1	PrivateTag
+
+(0029,"SIEMENS MED HG",10)	US	ListOfGroupNumbers	1	PrivateTag
+(0029,"SIEMENS MED HG",15)	LT	ListOfShadowOwnerCodes	1	PrivateTag
+(0029,"SIEMENS MED HG",20)	US	ListOfElementNumbers	1	PrivateTag
+(0029,"SIEMENS MED HG",30)	US	ListOfTotalDisplayLength	1	PrivateTag
+(0029,"SIEMENS MED HG",40)	LT	ListOfDisplayPrefix	1	PrivateTag
+(0029,"SIEMENS MED HG",50)	LT	ListOfDisplayPostfix	1	PrivateTag
+(0029,"SIEMENS MED HG",60)	US	ListOfTextPosition	1	PrivateTag
+(0029,"SIEMENS MED HG",70)	LT	ListOfTextConcatenation	1	PrivateTag
+(0029,"SIEMENS MED MG",10)	US	ListOfGroupNumbers	1	PrivateTag
+(0029,"SIEMENS MED MG",15)	LT	ListOfShadowOwnerCodes	1	PrivateTag
+(0029,"SIEMENS MED MG",20)	US	ListOfElementNumbers	1	PrivateTag
+(0029,"SIEMENS MED MG",30)	US	ListOfTotalDisplayLength	1	PrivateTag
+(0029,"SIEMENS MED MG",40)	LT	ListOfDisplayPrefix	1	PrivateTag
+(0029,"SIEMENS MED MG",50)	LT	ListOfDisplayPostfix	1	PrivateTag
+(0029,"SIEMENS MED MG",60)	US	ListOfTextPosition	1	PrivateTag
+(0029,"SIEMENS MED MG",70)	LT	ListOfTextConcatenation	1	PrivateTag
+
+(0009,"SIEMENS MED",10)	LO	RecognitionCode	1	PrivateTag
+(0009,"SIEMENS MED",30)	UL	ByteOffsetOfOriginalHeader	1	PrivateTag
+(0009,"SIEMENS MED",31)	UL	LengthOfOriginalHeader	1	PrivateTag
+(0009,"SIEMENS MED",40)	UL	ByteOffsetOfPixelmatrix	1	PrivateTag
+(0009,"SIEMENS MED",41)	UL	LengthOfPixelmatrixInBytes	1	PrivateTag
+(0009,"SIEMENS MED",50)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS MED",51)	LT	Unknown	1	PrivateTag
+(0009,"SIEMENS MED",f5)	LT	PDMEFIDPlaceholder	1	PrivateTag
+(0009,"SIEMENS MED",f6)	LT	PDMDataObjectTypeExtension	1	PrivateTag
+(0021,"SIEMENS MED",10)	DS	Zoom	1	PrivateTag
+(0021,"SIEMENS MED",11)	DS	Target	2	PrivateTag
+(0021,"SIEMENS MED",12)	IS	TubeAngle	1	PrivateTag
+(0021,"SIEMENS MED",20)	US	ROIMask	1	PrivateTag
+(7001,"SIEMENS MED",10)	LT	Dummy	1	PrivateTag
+(7003,"SIEMENS MED",10)	LT	Header	1	PrivateTag
+(7005,"SIEMENS MED",10)	LT	Dummy	1	PrivateTag
+
+(0029,"SIEMENS MEDCOM HEADER",08)	CS	MedComHeaderType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",09)	LO	MedComHeaderVersion	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",10)	OB	MedComHeaderInfo	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",20)	OB	MedComHistoryInformation	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",31)	LO	PMTFInformation1	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",32)	UL	PMTFInformation2	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",33)	UL	PMTFInformation3	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",34)	CS	PMTFInformation4	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",35)	UL	PMTFInformation5	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",40)	SQ	ApplicationHeaderSequence	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",41)	CS	ApplicationHeaderType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",42)	LO	ApplicationHeaderID	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",43)	LO	ApplicationHeaderVersion	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",44)	OB	ApplicationHeaderInfo	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",50)	LO	WorkflowControlFlags	8	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",51)	CS	ArchiveManagementFlagKeepOnline	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",52)	CS	ArchiveManagementFlagDoNotArchive	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",53)	CS	ImageLocationStatus	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",54)	DS	EstimatedRetrieveTime	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",55)	DS	DataSizeOfRetrievedImages	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",70)	SQ	SiemensLinkSequence	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",71)	AT	ReferencedTag	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",72)	CS	ReferencedTagType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",73)	UL	ReferencedValueLength	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",74)	CS	ReferencedObjectDeviceType	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",75)	OB	ReferencedObjectDeviceLocation	1	PrivateTag
+(0029,"SIEMENS MEDCOM HEADER",76)	OB	ReferencedObjectDeviceID	1	PrivateTag
+
+(0029,"SIEMENS MEDCOM HEADER2",60)	LO	SeriesWorkflowStatus	1	PrivateTag
+
+(0029,"SIEMENS MEDCOM OOG",08)	CS	MEDCOMOOGType	1	PrivateTag
+(0029,"SIEMENS MEDCOM OOG",09)	LO	MEDCOMOOGVersion	1	PrivateTag
+(0029,"SIEMENS MEDCOM OOG",10)	OB	MEDCOMOOGInfo	1	PrivateTag
+
+(0019,"SIEMENS MR VA0  COAD",12)	DS	MagneticFieldStrength	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",14)	DS	ADCVoltage	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",16)	DS	ADCOffset	2	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",20)	DS	TransmitterAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",21)	IS	NumberOfTransmitterAmplitudes	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",22)	DS	TransmitterAttenuator	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",24)	DS	TransmitterCalibration	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",26)	DS	TransmitterReference	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",50)	DS	ReceiverTotalGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",51)	DS	ReceiverAmplifierGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",52)	DS	ReceiverPreamplifierGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",54)	DS	ReceiverCableAttenuation	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",55)	DS	ReceiverReferenceGain	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",56)	DS	ReceiverFilterFrequency	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",60)	DS	ReconstructionScaleFactor	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",62)	DS	ReferenceScaleFactor	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",70)	DS	PhaseGradientAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",71)	DS	ReadoutGradientAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",72)	DS	SelectionGradientAmplitude	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",80)	DS	GradientDelayTime	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",82)	DS	TotalGradientDelayTime	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",90)	LT	SensitivityCorrectionLabel	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",91)	DS	SaturationPhaseEncodingVectorCoronalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",92)	DS	SaturationReadoutVectorCoronalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a0)	US	RFWatchdogMask	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a1)	DS	EPIReconstructionSlope	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a2)	DS	RFPowerErrorIndicator	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a5)	DS	SpecificAbsorptionRateWholeBody	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",a6)	DS	SpecificEnergyDose	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",b0)	UL	AdjustmentStatusMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c1)	DS	EPICapacity	6	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c2)	DS	EPIInductance	3	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c3)	IS	EPISwitchConfigurationCode	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c4)	IS	EPISwitchHardwareCode	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",c5)	DS	EPISwitchDelayTime	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d1)	DS	FlowSensitivity	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d2)	LT	CalculationSubmode	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d3)	DS	FieldOfViewRatio	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d4)	IS	BaseRawMatrixSize	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d5)	IS	2DOversamplingLines	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d6)	IS	3DPhaseOversamplingPartitions	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d7)	IS	EchoLinePosition	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d8)	IS	EchoColumnPosition	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",d9)	IS	LinesPerSegment	1	PrivateTag
+(0019,"SIEMENS MR VA0  COAD",da)	LT	PhaseCodingDirection	1	PrivateTag
+
+(0019,"SIEMENS MR VA0  GEN",10)	DS	TotalMeasurementTimeNominal	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",11)	DS	TotalMeasurementTimeCurrent	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",12)	DS	StartDelayTime	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",13)	DS	DwellTime	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",14)	IS	NumberOfPhases	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",16)	UL	SequenceControlMask	2	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",18)	UL	MeasurementStatusMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",20)	IS	NumberOfFourierLinesNominal	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",21)	IS	NumberOfFourierLinesCurrent	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",26)	IS	NumberOfFourierLinesAfterZero	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",28)	IS	FirstMeasuredFourierLine	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",30)	IS	AcquisitionColumns	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",31)	IS	ReconstructionColumns	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",40)	IS	ArrayCoilElementNumber	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",41)	UL	ArrayCoilElementSelectMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",42)	UL	ArrayCoilElementDataMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",43)	IS	ArrayCoilElementToADCConnect	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",44)	DS	ArrayCoilElementNoiseLevel	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",45)	IS	ArrayCoilADCPairNumber	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",46)	UL	ArrayCoilCombinationMask	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",50)	IS	NumberOfAverages	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",60)	DS	FlipAngle	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",70)	IS	NumberOfPrescans	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",81)	LT	FilterTypeForRawData	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",82)	DS	FilterParameterForRawData	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",83)	LT	FilterTypeForImageData	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",84)	DS	FilterParameterForImageData	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",85)	LT	FilterTypeForPhaseCorrection	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",86)	DS	FilterParameterForPhaseCorrection	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",87)	LT	NormalizationFilterTypeForImageData	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",88)	DS	NormalizationFilterParameterForImageData	1-n	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",90)	IS	NumberOfSaturationRegions	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",91)	DS	SaturationPhaseEncodingVectorSagittalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",92)	DS	SaturationReadoutVectorSagittalComponent	6	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",93)	DS	EPIStimulationMonitorMode	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",94)	DS	ImageRotationAngle	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",96)	UL	CoilIDMask	3	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",97)	UL	CoilClassMask	2	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",98)	DS	CoilPosition	3	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",a0)	DS	EPIReconstructionPhase	1	PrivateTag
+(0019,"SIEMENS MR VA0  GEN",a1)	DS	EPIReconstructionSlope	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",20)	IS	PhaseCorrectionRowsSequence	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",21)	IS	PhaseCorrectionColumnsSequence	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",22)	IS	PhaseCorrectionRowsReconstruction	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",24)	IS	PhaseCorrectionColumnsReconstruction	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",30)	IS	NumberOf3DRawPartitionsNominal	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",31)	IS	NumberOf3DRawPartitionsCurrent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",34)	IS	NumberOf3DImagePartitions	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",36)	IS	Actual3DImagePartitionNumber	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",39)	DS	SlabThickness	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",40)	IS	NumberOfSlicesNominal	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",41)	IS	NumberOfSlicesCurrent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",42)	IS	CurrentSliceNumber	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",43)	IS	CurrentGroupNumber	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",44)	DS	CurrentSliceDistanceFactor	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",45)	IS	MIPStartRow	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",46)	IS	MIPStopRow	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",47)	IS	MIPStartColumn	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",48)	IS	MIPStartColumn	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",49)	IS	MIPStartSlice Name=	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",4a)	IS	MIPStartSlice	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",4f)	LT	OrderofSlices	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",50)	US	SignalMask	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",52)	DS	DelayAfterTrigger	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",53)	IS	RRInterval	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",54)	DS	NumberOfTriggerPulses	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",56)	DS	RepetitionTimeEffective	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",57)	LT	GatePhase	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",58)	DS	GateThreshold	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",59)	DS	GatedRatio	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",60)	IS	NumberOfInterpolatedImages	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",70)	IS	NumberOfEchoes	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",72)	DS	SecondEchoTime	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",73)	DS	SecondRepetitionTime	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",80)	IS	CardiacCode	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",91)	DS	SaturationPhaseEncodingVectorTransverseComponent	6	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",92)	DS	SaturationReadoutVectorTransverseComponent	6	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",93)	DS	EPIChangeValueOfMagnitude	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",94)	DS	EPIChangeValueOfXComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",95)	DS	EPIChangeValueOfYComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  GEN",96)	DS	EPIChangeValueOfZComponent	1	PrivateTag
+
+(0021,"SIEMENS MR VA0  RAW",00)	LT	SequenceType	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",01)	IS	VectorSizeOriginal	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",02)	IS	VectorSizeExtended	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",03)	DS	AcquiredSpectralRange	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",04)	DS	VOIPosition	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",05)	DS	VOISize	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",06)	IS	CSIMatrixSizeOriginal	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",07)	IS	CSIMatrixSizeExtended	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",08)	DS	SpatialGridShift	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",09)	DS	SignalLimitsMinimum	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",10)	DS	SignalLimitsMaximum	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",11)	DS	SpecInfoMask	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",12)	DS	EPITimeRateOfChangeOfMagnitude	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",13)	DS	EPITimeRateOfChangeOfXComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",14)	DS	EPITimeRateOfChangeOfYComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",15)	DS	EPITimeRateOfChangeOfZComponent	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",16)	DS	EPITimeRateOfChangeLegalLimit1	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",17)	DS	EPIOperationModeFlag	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",18)	DS	EPIFieldCalculationSafetyFactor	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",19)	DS	EPILegalLimit1OfChangeValue	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",20)	DS	EPILegalLimit2OfChangeValue	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",21)	DS	EPIRiseTime	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",30)	DS	ArrayCoilADCOffset	16	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",31)	DS	ArrayCoilPreamplifierGain	16	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",50)	LT	SaturationType	1	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",51)	DS	SaturationNormalVector	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",52)	DS	SaturationPositionVector	3	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",53)	DS	SaturationThickness	6	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",54)	DS	SaturationWidth	6	PrivateTag
+(0021,"SIEMENS MR VA0  RAW",55)	DS	SaturationDistance	6	PrivateTag
+
+(7fe3,"SIEMENS NUMARIS II",00)	LT	ImageGraphicsFormatCode	1	PrivateTag
+(7fe3,"SIEMENS NUMARIS II",10)	OB	ImageGraphics	1	PrivateTag
+(7fe3,"SIEMENS NUMARIS II",20)	OB	ImageGraphicsDummy	1	PrivateTag
+
+(0011,"SIEMENS RA GEN",20)	SL	FluoroTimer	1	PrivateTag
+(0011,"SIEMENS RA GEN",25)	SL	PtopDoseAreaProduct	1	PrivateTag
+(0011,"SIEMENS RA GEN",26)	SL	PtopTotalSkinDose	1	PrivateTag
+(0011,"SIEMENS RA GEN",30)	LT	Unknown	1	PrivateTag
+(0011,"SIEMENS RA GEN",35)	LO	PatientInitialPuckCounter	1	PrivateTag
+(0011,"SIEMENS RA GEN",40)	SS	SPIDataObjectType	1	PrivateTag
+(0019,"SIEMENS RA GEN",15)	LO	AcquiredPlane	1	PrivateTag
+(0019,"SIEMENS RA GEN",1f)	SS	DefaultTableIsoCenterHeight	1	PrivateTag
+(0019,"SIEMENS RA GEN",20)	SL	SceneFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",22)	SL	RefPhotofileFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",24)	LO	SceneName	1	PrivateTag
+(0019,"SIEMENS RA GEN",26)	SS	AcquisitionIndex	1	PrivateTag
+(0019,"SIEMENS RA GEN",28)	SS	MixedPulseMode	1	PrivateTag
+(0019,"SIEMENS RA GEN",2a)	SS	NoOfPositions	1	PrivateTag
+(0019,"SIEMENS RA GEN",2c)	SS	NoOfPhases	1	PrivateTag
+(0019,"SIEMENS RA GEN",2e)	SS	FrameRateForPositions	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",30)	SS	NoOfFramesForPositions	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",32)	SS	SteppingDirection	1	PrivateTag
+(0019,"SIEMENS RA GEN",34)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",36)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",38)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",3a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",3c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",3e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",40)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",42)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",44)	SS	ImageTransferDelay	1	PrivateTag
+(0019,"SIEMENS RA GEN",46)	SL	InversFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",48)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",4a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",4c)	SS	BlankingCircleDiameter	1	PrivateTag
+(0019,"SIEMENS RA GEN",50)	SL	StandDataValid	1	PrivateTag
+(0019,"SIEMENS RA GEN",52)	SS	TableTilt	1	PrivateTag
+(0019,"SIEMENS RA GEN",54)	SS	TableAxisRotation	1	PrivateTag
+(0019,"SIEMENS RA GEN",56)	SS	TableLongitudalPosition	1	PrivateTag
+(0019,"SIEMENS RA GEN",58)	SS	TableSideOffset	1	PrivateTag
+(0019,"SIEMENS RA GEN",5a)	SS	TableIsoCenterHeight	1	PrivateTag
+(0019,"SIEMENS RA GEN",5c)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA GEN",5e)	SL	CollimationDataValid	1	PrivateTag
+(0019,"SIEMENS RA GEN",60)	SL	PeriSequenceNo	1	PrivateTag
+(0019,"SIEMENS RA GEN",62)	SL	PeriTotalScenes	1	PrivateTag
+(0019,"SIEMENS RA GEN",64)	SL	PeriOverlapTop	1	PrivateTag
+(0019,"SIEMENS RA GEN",66)	SL	PeriOverlapBottom	1	PrivateTag
+(0019,"SIEMENS RA GEN",68)	SL	RawImageNumber	1	PrivateTag
+(0019,"SIEMENS RA GEN",6a)	SL	XRayDataValid	1	PrivateTag
+(0019,"SIEMENS RA GEN",70)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",72)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",74)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",76)	SL	FillingAverageFactor	1	PrivateTag
+(0019,"SIEMENS RA GEN",78)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",7a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",7c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",7e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",80)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",82)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",84)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",86)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",88)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",8a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",8c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",8e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",92)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",94)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",96)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",98)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",9a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA GEN",9c)	SL	IntensifierLevelCalibrationFactor	1	PrivateTag
+(0019,"SIEMENS RA GEN",9e)	SL	NativeReviewFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",a2)	SL	SceneNumber	1	PrivateTag
+(0019,"SIEMENS RA GEN",a4)	SS	AcquisitionMode	1	PrivateTag
+(0019,"SIEMENS RA GEN",a5)	SS	AcquisitonFrameRate	1	PrivateTag
+(0019,"SIEMENS RA GEN",a6)	SL	ECGFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",a7)	SL	AdditionalSceneData	1	PrivateTag
+(0019,"SIEMENS RA GEN",a8)	SL	FileCopyFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",a9)	SL	PhlebovisionFlag	1	PrivateTag
+(0019,"SIEMENS RA GEN",aa)	SL	Co2Flag	1	PrivateTag
+(0019,"SIEMENS RA GEN",ab)	SS	MaxSpeed	1	PrivateTag
+(0019,"SIEMENS RA GEN",ac)	SS	StepWidth	1	PrivateTag
+(0019,"SIEMENS RA GEN",ad)	SL	DigitalAcquisitionZoom	1	PrivateTag
+(0019,"SIEMENS RA GEN",ff)	SS	Internal	1-n	PrivateTag
+(0021,"SIEMENS RA GEN",15)	SS	ImagesInStudy	1	PrivateTag
+(0021,"SIEMENS RA GEN",20)	SS	ScenesInStudy	1	PrivateTag
+(0021,"SIEMENS RA GEN",25)	SS	ImagesInPhotofile	1	PrivateTag
+(0021,"SIEMENS RA GEN",27)	SS	PlaneBImagesExist	1	PrivateTag
+(0021,"SIEMENS RA GEN",28)	SS	NoOf2MBChunks	1	PrivateTag
+(0021,"SIEMENS RA GEN",30)	SS	ImagesInAllScenes	1	PrivateTag
+(0021,"SIEMENS RA GEN",40)	SS	ArchiveSWInternalVersion	1	PrivateTag
+
+(0011,"SIEMENS RA PLANE A",28)	SL	FluoroTimerA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",29)	SL	FluoroSkinDoseA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",2a)	SL	TotalSkinDoseA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",2b)	SL	FluoroDoseAreaProductA	1	PrivateTag
+(0011,"SIEMENS RA PLANE A",2c)	SL	TotalDoseAreaProductA	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",15)	LT	OfflineUID	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",18)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",19)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1a)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1b)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1c)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1d)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1e)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",1f)	SS	Internal	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",20)	SS	SystemCalibFactorPlaneA	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",22)	SS	XRayParameterSetNo	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",24)	SS	XRaySystem	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",26)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",28)	SS	AcquiredDisplayMode	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",2a)	SS	AcquisitionDelay	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",2c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",2e)	SS	MaxFramesLimit	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",30)	US	MaximumFrameSizeNIU	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",32)	SS	SubtractedFilterType	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",34)	SS	FilterFactorNative	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",36)	SS	AnatomicBackgroundFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",38)	SS	WindowUpperLimitNative	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",3a)	SS	WindowLowerLimitNative	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",3c)	SS	WindowBrightnessPhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",3e)	SS	WindowBrightnessPhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",40)	SS	WindowContrastPhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",42)	SS	WindowContrastPhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",44)	SS	FilterFactorSub	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",46)	SS	PeakOpacified	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",48)	SL	MaskFrame	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",4a)	SL	BIHFrame	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",4c)	SS	CentBeamAngulationCaudCran	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",4e)	SS	CentBeamAngulationLRAnterior	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",50)	SS	LongitudinalPosition	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",52)	SS	SideOffset	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",54)	SS	IsoCenterHeight	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",56)	SS	ImageTwist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",58)	SS	SourceImageDistance	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",5a)	SS	MechanicalMagnificationFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",5c)	SL	CalibrationFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",5e)	SL	CalibrationAngleCranCaud	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",60)	SL	CalibrationAngleRAOLAO	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",62)	SL	CalibrationTableToFloorDist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",64)	SL	CalibrationIsocenterToFloorDist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",66)	SL	CalibrationIsocenterToSourceDist	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",68)	SL	CalibrationSourceToII	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",6a)	SL	CalibrationIIZoom	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",6c)	SL	CalibrationIIField	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",6e)	SL	CalibrationFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",70)	SL	CalibrationObjectToImageDistance	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",72)	SL	CalibrationSystemFactor	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",74)	SL	CalibrationSystemCorrection	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",76)	SL	CalibrationSystemIIFormats	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",78)	SL	CalibrationGantryDataValid	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",7a)	SS	CollimatorSquareBreadth	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",7c)	SS	CollimatorSquareHeight	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",7e)	SS	CollimatorSquareDiameter	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",80)	SS	CollimaterFingerTurnAngle	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",82)	SS	CollimaterFingerPosition	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",84)	SS	CollimaterDiaphragmTurnAngle	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",86)	SS	CollimaterDiaphragmPosition1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",88)	SS	CollimaterDiaphragmPosition2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",8a)	SS	CollimaterDiaphragmMode	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",8c)	SS	CollimaterBeamLimitBreadth	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",8e)	SS	CollimaterBeamLimitHeight	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",90)	SS	CollimaterBeamLimitDiameter	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",92)	SS	X-RayControlMOde	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",94)	SS	X-RaySystem	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",96)	SS	FocalSpot	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",98)	SS	ExposureControl	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",9a)	SL	XRayVoltage	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",9c)	SL	XRayCurrent	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",9e)	SL	XRayCurrentTimeProduct	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a0)	SL	XRayPulseTime	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a2)	SL	XRaySceneTimeFluoroClock	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a4)	SS	MaximumPulseRate	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a6)	SS	PulsesPerScene	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",a8)	SL	DoseAreaProductOfScene	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",aa)	SS	Dose	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ac)	SS	DoseRate	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ae)	SL	IIToCoverDistance	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b0)	SS	LastFramePhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b1)	SS	FrameRatePhase1	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b2)	SS	LastFramePhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b3)	SS	FrameRatePhase2	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b4)	SS	LastFramePhase3	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b5)	SS	FrameRatePhase3	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b6)	SS	LastFramePhase4	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b7)	SS	FrameRatePhase4	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b8)	SS	GammaOfNativeImage	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",b9)	SS	GammaOfTVSystem	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bb)	SL	PixelshiftX	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bc)	SL	PixelshiftY	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bd)	SL	MaskAverageFactor	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",be)	SL	BlankingCircleFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",bf)	SL	CircleRowStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c0)	SL	CircleRowEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c1)	SL	CircleColumnStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c2)	SL	CircleColumnEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c3)	SL	CircleDiameter	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c4)	SL	RectangularCollimaterFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c5)	SL	RectangleRowStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c6)	SL	RectangleRowEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c7)	SL	RectangleColumnStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c8)	SL	RectangleColumnEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",c9)	SL	RectangleAngulation	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ca)	SL	IrisCollimatorFlag	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cb)	SL	IrisRowStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cc)	SL	IrisRowEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cd)	SL	IrisColumnStart	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",ce)	SL	IrisColumnEnd	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",cf)	SL	IrisAngulation	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d1)	SS	NumberOfFramesPlane	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d2)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d3)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d4)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d5)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d6)	SS	Internal	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",d7)	SS	Internal	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",d8)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",d9)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",da)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",db)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",dc)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",dd)	SL	AnatomicBackground	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",de)	SL	AutoWindowBase	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE A",df)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE A",e0)	SL	Internal	1	PrivateTag
+
+(0011,"SIEMENS RA PLANE B",28)	SL	FluoroTimerB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",29)	SL	FluoroSkinDoseB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",2a)	SL	TotalSkinDoseB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",2b)	SL	FluoroDoseAreaProductB	1	PrivateTag
+(0011,"SIEMENS RA PLANE B",2c)	SL	TotalDoseAreaProductB	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",18)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",19)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1a)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1b)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1c)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1d)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1e)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",1f)	SS	Internal	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",20)	SL	SystemCalibFactorPlaneB	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",22)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",24)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",26)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",28)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",2a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",2c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",2e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",30)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",32)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",34)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",36)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",38)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",3a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",3c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",3e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",40)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",42)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",44)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",46)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",48)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",4a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",4c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",4e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",50)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",52)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",54)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",56)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",58)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",5a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",5c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",5e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",60)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",62)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",64)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",66)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",68)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",6a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",6c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",6e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",70)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",72)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",74)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",76)	UN	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",78)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",7a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",7c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",7e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",80)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",82)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",84)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",86)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",88)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",8a)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",8c)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",8e)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",90)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",92)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",94)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",96)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",98)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",9a)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",9c)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",9e)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",a0)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",a2)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",a4)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",a6)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",a8)	US	Unknown	1-n	PrivateTag
+(0019,"SIEMENS RA PLANE B",aa)	US	Unknown	1	PrivateTag
+(0019,"SIEMENS RA PLANE B",ac)	US	Unknown	1	PrivateTag
+
+(0011,"SIEMENS RIS",10)	LT	PatientUID	1	PrivateTag
+(0011,"SIEMENS RIS",11)	LT	PatientID	1	PrivateTag
+(0011,"SIEMENS RIS",20)	DA	PatientRegistrationDate	1	PrivateTag
+(0011,"SIEMENS RIS",21)	TM	PatientRegistrationTime	1	PrivateTag
+(0011,"SIEMENS RIS",30)	LT	PatientnameRIS	1	PrivateTag
+(0011,"SIEMENS RIS",31)	LT	PatientprenameRIS	1	PrivateTag
+(0011,"SIEMENS RIS",40)	LT	PatientHospitalStatus	1	PrivateTag
+(0011,"SIEMENS RIS",41)	LT	MedicalAlerts	1	PrivateTag
+(0011,"SIEMENS RIS",42)	LT	ContrastAllergies	1	PrivateTag
+(0031,"SIEMENS RIS",10)	LT	RequestUID	1	PrivateTag
+(0031,"SIEMENS RIS",45)	LT	RequestingPhysician	1	PrivateTag
+(0031,"SIEMENS RIS",50)	LT	RequestedPhysician	1	PrivateTag
+(0033,"SIEMENS RIS",10)	LT	PatientStudyUID	1	PrivateTag
+
+(0021,"SIEMENS SMS-AX  ACQ 1.0",00)	US	AcquisitionType	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",01)	US	AcquisitionMode	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",02)	US	FootswitchIndex	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",03)	US	AcquisitionRoom	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",04)	SL	CurrentTimeProduct	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",05)	SL	Dose	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",06)	SL	SkinDosePercent	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",07)	SL	SkinDoseAccumulation	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",08)	SL	SkinDoseRate	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0A)	UL	CopperFilter	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0B)	US	MeasuringField	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0C)	SS	PostBlankingCircle	3	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0D)	SS	DynaAngles	2-2n	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0E)	SS	TotalSteps	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",0F)	SL	DynaXRayInfo	3-3n	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",10)	US	ModalityLUTInputGamma	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",11)	US	ModalityLUTOutputGamma	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",12)	OB	SH_STPAR	1-n	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",13)	US	AcquisitionZoom	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",14)	SS	DynaAngulationStepWidth	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",15)	US	Harmonization	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",16)	US	DRSingleFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",17)	SL	SourceToIsocenter	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",18)	US	PressureData	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",19)	SL	ECGIndexArray	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1A)	US	FDFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1B)	OB	SH_ZOOM	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1C)	OB	SH_COLPAR	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1D)	US	K_Factor	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1E)	US	EVE	8	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",1F)	SL	TotalSceneTime	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",20)	US	RestoreFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",21)	US	StandMovementFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",22)	US	FDRows	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",23)	US	FDColumns	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",24)	US	TableMovementFlag	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",25)	LO	OriginalOrganProgramName	1	PrivateTag
+(0021,"SIEMENS SMS-AX  ACQ 1.0",26)	DS	CrispyXPIFilter	1	PrivateTag
+
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",00)	US	ViewNative	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",01)	US	OriginalSeriesNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",02)	US	OriginalImageNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",03)	US	WinCenter	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",04)	US	WinWidth	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",05)	US	WinBrightness	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",06)	US	WinContrast	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",07)	US	OriginalFrameNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",08)	US	OriginalMaskFrameNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",09)	US	Opac	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0A)	US	OriginalNumberOfFrames	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0B)	DS	OriginalSceneDuration	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0C)	LO	IdentifierLOID	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0D)	SS	OriginalSceneVFRInfo	1-n	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0E)	SS	OriginalFrameECGPosition	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",0F)	SS	OriginalECG1stFrameOffset_retired	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",10)	SS	ZoomFlag	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",11)	US	Flex	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",12)	US	NumberOfMaskFrames	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",13)	US	NumberOfFillFrames	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",14)	US	SeriesNumber	1	PrivateTag
+(0025,"SIEMENS SMS-AX  ORIGINAL IMAGE INFO 1.0",15)	IS	ImageNumber	1	PrivateTag
+
+(0023,"SIEMENS SMS-AX  QUANT 1.0",00)	DS	HorizontalCalibrationPixelSize	2	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",01)	DS	VerticalCalibrationPixelSize	2	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",02)	LO	CalibrationObject	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",03)	DS	CalibrationObjectSize	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",04)	LO	CalibrationMethod	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",05)	ST	Filename	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",06)	IS	FrameNumber	1	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",07)	IS	CalibrationFactorMultiplicity	2	PrivateTag
+(0023,"SIEMENS SMS-AX  QUANT 1.0",08)	IS	CalibrationTODValue	1	PrivateTag
+
+(0019,"SIEMENS SMS-AX  VIEW 1.0",00)	US	ReviewMode	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",01)	US	AnatomicalBackgroundPercent	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",02)	US	NumberOfPhases	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",03)	US	ApplyAnatomicalBackground	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",04)	SS	PixelShiftArray	4-4n	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",05)	US	Brightness	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",06)	US	Contrast	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",07)	US	Enabled	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",08)	US	NativeEdgeEnhancementPercentGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",09)	SS	NativeEdgeEnhancementLUTIndex	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0A)	SS	NativeEdgeEnhancementKernelSize	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0B)	US	SubtrEdgeEnhancementPercentGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0C)	SS	SubtrEdgeEnhancementLUTIndex	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0D)	SS	SubtrEdgeEnhancementKernelSize	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0E)	US	FadePercent	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",0F)	US	FlippedBeforeLateralityApplied	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",10)	US	ApplyFade	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",12)	US	Zoom	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",13)	SS	PanX	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",14)	SS	PanY	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",15)	SS	NativeEdgeEnhancementAdvPercGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",16)	SS	SubtrEdgeEnhancementAdvPercGain	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",17)	US	InvertFlag	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1A)	OB	Quant1KOverlay	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1B)	US	OriginalResolution	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1C)	DS	AutoWindowCenter	1	PrivateTag
+(0019,"SIEMENS SMS-AX  VIEW 1.0",1D)	DS	AutoWindowWidth	1	PrivateTag
+
+(0009,"SIENET",01)	US	SIENETCommandField	1	PrivateTag
+(0009,"SIENET",14)	LT	ReceiverPLA	1	PrivateTag
+(0009,"SIENET",16)	US	TransferPriority	1	PrivateTag
+(0009,"SIENET",29)	LT	ActualUser	1	PrivateTag
+(0095,"SIENET",01)	LT	ExaminationFolderID	1	PrivateTag
+(0095,"SIENET",04)	UL	FolderReportedStatus	1	PrivateTag
+(0095,"SIENET",05)	LT	FolderReportingRadiologist	1	PrivateTag
+(0095,"SIENET",07)	LT	SIENETISAPLA	1	PrivateTag
+(0099,"SIENET",02)	UL	DataObjectAttributes	1	PrivateTag
+
+(0009,"SPI RELEASE 1",10)	LT	Comments	1	PrivateTag
+(0009,"SPI RELEASE 1",15)	LO	SPIImageUID	1	PrivateTag
+(0009,"SPI RELEASE 1",40)	US	DataObjectType	1	PrivateTag
+(0009,"SPI RELEASE 1",41)	LO	DataObjectSubtype	1	PrivateTag
+(0011,"SPI RELEASE 1",10)	LO	Organ	1	PrivateTag
+(0011,"SPI RELEASE 1",15)	LO	AllergyIndication	1	PrivateTag
+(0011,"SPI RELEASE 1",20)	LO	Pregnancy	1	PrivateTag
+(0029,"SPI RELEASE 1",60)	LT	CompressionAlgorithm	1	PrivateTag
+
+(0009,"SPI Release 1",10)	LT	Comments	1	PrivateTag
+(0009,"SPI Release 1",15)	LO	SPIImageUID	1	PrivateTag
+(0009,"SPI Release 1",40)	US	DataObjectType	1	PrivateTag
+(0009,"SPI Release 1",41)	LO	DataObjectSubtype	1	PrivateTag
+(0011,"SPI Release 1",10)	LO	Organ	1	PrivateTag
+(0011,"SPI Release 1",15)	LO	AllergyIndication	1	PrivateTag
+(0011,"SPI Release 1",20)	LO	Pregnancy	1	PrivateTag
+(0029,"SPI Release 1",60)	LT	CompressionAlgorithm	1	PrivateTag
+
+(0009,"SPI",10)	LO	Comments	1	PrivateTag
+(0009,"SPI",15)	LO	SPIImageUID	1	PrivateTag
+(0009,"SPI",40)	US	DataObjectType	1	PrivateTag
+(0009,"SPI",41)	LT	DataObjectSubtype	1	PrivateTag
+(0011,"SPI",10)	LT	Organ	1	PrivateTag
+(0011,"SPI",15)	LT	AllergyIndication	1	PrivateTag
+(0011,"SPI",20)	LT	Pregnancy	1	PrivateTag
+(0029,"SPI",60)	LT	CompressionAlgorithm	1	PrivateTag
+
+(0011,"SPI RELEASE 1",10)	LO	Organ	1	PrivateTag
+(0011,"SPI RELEASE 1",15)	LO	AllergyIndication	1	PrivateTag
+(0011,"SPI RELEASE 1",20)	LO	Pregnancy	1	PrivateTag
+
+(0009,"SPI-P Release 1",00)	LT	DataObjectRecognitionCode	1	PrivateTag
+(0009,"SPI-P Release 1",04)	LO	ImageDataConsistence	1	PrivateTag
+(0009,"SPI-P Release 1",08)	US	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",12)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",15)	LO	UniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",16)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",18)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",21)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",31)	LT	PACSUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",34)	LT	ClusterUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",38)	LT	SystemUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",39)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",51)	LT	StudyUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",61)	LT	SeriesUniqueIdentifier	1	PrivateTag
+(0009,"SPI-P Release 1",91)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f2)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f3)	UN	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f4)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f5)	UN	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1",f7)	LT	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",10)	LT	PatientEntryID	1	PrivateTag
+(0011,"SPI-P Release 1",21)	UN	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",22)	UN	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",31)	UN	Unknown	1	PrivateTag
+(0011,"SPI-P Release 1",32)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",00)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",01)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",02)	UN	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1",10)	US	MainsFrequency	1	PrivateTag
+(0019,"SPI-P Release 1",25)	LT	OriginalPixelDataQuality	1-n	PrivateTag
+(0019,"SPI-P Release 1",30)	US	ECGTriggering	1	PrivateTag
+(0019,"SPI-P Release 1",31)	UN	ECG1Offset	1	PrivateTag
+(0019,"SPI-P Release 1",32)	UN	ECG2Offset1	1	PrivateTag
+(0019,"SPI-P Release 1",33)	UN	ECG2Offset2	1	PrivateTag
+(0019,"SPI-P Release 1",50)	US	VideoScanMode	1	PrivateTag
+(0019,"SPI-P Release 1",51)	US	VideoLineRate	1	PrivateTag
+(0019,"SPI-P Release 1",60)	US	XrayTechnique	1	PrivateTag
+(0019,"SPI-P Release 1",61)	DS	ImageIdentifierFromat	1	PrivateTag
+(0019,"SPI-P Release 1",62)	US	IrisDiaphragm	1	PrivateTag
+(0019,"SPI-P Release 1",63)	CS	Filter	1	PrivateTag
+(0019,"SPI-P Release 1",64)	CS	CineParallel	1	PrivateTag
+(0019,"SPI-P Release 1",65)	CS	CineMaster	1	PrivateTag
+(0019,"SPI-P Release 1",70)	US	ExposureChannel	1	PrivateTag
+(0019,"SPI-P Release 1",71)	UN	ExposureChannelFirstImage	1	PrivateTag
+(0019,"SPI-P Release 1",72)	US	ProcessingChannel	1	PrivateTag
+(0019,"SPI-P Release 1",80)	DS	AcquisitionDelay	1	PrivateTag
+(0019,"SPI-P Release 1",81)	UN	RelativeImageTime	1	PrivateTag
+(0019,"SPI-P Release 1",90)	CS	VideoWhiteCompression	1	PrivateTag
+(0019,"SPI-P Release 1",a0)	US	Angulation	1	PrivateTag
+(0019,"SPI-P Release 1",a1)	US	Rotation	1	PrivateTag
+(0021,"SPI-P Release 1",12)	LT	SeriesUniqueIdentifier	1	PrivateTag
+(0021,"SPI-P Release 1",14)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",00)	DS	Unknown	4	PrivateTag
+(0029,"SPI-P Release 1",20)	DS	PixelAspectRatio	1	PrivateTag
+(0029,"SPI-P Release 1",25)	LO	ProcessedPixelDataQuality	1-n	PrivateTag
+(0029,"SPI-P Release 1",30)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",38)	US	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",60)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",61)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",67)	LT	Unknown	1	PrivateTag
+(0029,"SPI-P Release 1",70)	LT	WindowID	1	PrivateTag
+(0029,"SPI-P Release 1",71)	CS	VideoInvertSubtracted	1	PrivateTag
+(0029,"SPI-P Release 1",72)	CS	VideoInvertNonsubtracted	1	PrivateTag
+(0029,"SPI-P Release 1",77)	CS	WindowSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1",78)	LT	ECGDisplayPrintingID	1	PrivateTag
+(0029,"SPI-P Release 1",79)	CS	ECGDisplayPrinting	1	PrivateTag
+(0029,"SPI-P Release 1",7e)	CS	ECGDisplayPrintingEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1",7f)	CS	ECGDisplayPrintingSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1",80)	LT	PhysiologicalDisplayID	1	PrivateTag
+(0029,"SPI-P Release 1",81)	US	PreferredPhysiologicalChannelDisplay	1	PrivateTag
+(0029,"SPI-P Release 1",8e)	CS	PhysiologicalDisplayEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1",8f)	CS	PhysiologicalDisplaySelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1",c0)	LT	FunctionalShutterID	1	PrivateTag
+(0029,"SPI-P Release 1",c1)	US	FieldOfShutter	1	PrivateTag
+(0029,"SPI-P Release 1",c5)	LT	FieldOfShutterRectangle	1	PrivateTag
+(0029,"SPI-P Release 1",ce)	CS	ShutterEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1",cf)	CS	ShutterSelectStatus	1	PrivateTag
+(7FE1,"SPI-P Release 1",10)	ox	PixelData	1	PrivateTag
+
+(0009,"SPI-P Release 1;1",c0)	LT	Unknown	1	PrivateTag
+(0009,"SPI-P Release 1;1",c1)	LT	Unknown	1	PrivateTag
+(0019,"SPI-P Release 1;1",00)	UN	PhysiologicalDataType	1	PrivateTag
+(0019,"SPI-P Release 1;1",01)	UN	PhysiologicalDataChannelAndKind	1	PrivateTag
+(0019,"SPI-P Release 1;1",02)	US	SampleBitsAllocated	1	PrivateTag
+(0019,"SPI-P Release 1;1",03)	US	SampleBitsStored	1	PrivateTag
+(0019,"SPI-P Release 1;1",04)	US	SampleHighBit	1	PrivateTag
+(0019,"SPI-P Release 1;1",05)	US	SampleRepresentation	1	PrivateTag
+(0019,"SPI-P Release 1;1",06)	UN	SmallestSampleValue	1	PrivateTag
+(0019,"SPI-P Release 1;1",07)	UN	LargestSampleValue	1	PrivateTag
+(0019,"SPI-P Release 1;1",08)	UN	NumberOfSamples	1	PrivateTag
+(0019,"SPI-P Release 1;1",09)	UN	SampleData	1	PrivateTag
+(0019,"SPI-P Release 1;1",0a)	UN	SampleRate	1	PrivateTag
+(0019,"SPI-P Release 1;1",10)	UN	PhysiologicalDataType2	1	PrivateTag
+(0019,"SPI-P Release 1;1",11)	UN	PhysiologicalDataChannelAndKind2	1	PrivateTag
+(0019,"SPI-P Release 1;1",12)	US	SampleBitsAllocated2	1	PrivateTag
+(0019,"SPI-P Release 1;1",13)	US	SampleBitsStored2	1	PrivateTag
+(0019,"SPI-P Release 1;1",14)	US	SampleHighBit2	1	PrivateTag
+(0019,"SPI-P Release 1;1",15)	US	SampleRepresentation2	1	PrivateTag
+(0019,"SPI-P Release 1;1",16)	UN	SmallestSampleValue2	1	PrivateTag
+(0019,"SPI-P Release 1;1",17)	UN	LargestSampleValue2	1	PrivateTag
+(0019,"SPI-P Release 1;1",18)	UN	NumberOfSamples2	1	PrivateTag
+(0019,"SPI-P Release 1;1",19)	UN	SampleData2	1	PrivateTag
+(0019,"SPI-P Release 1;1",1a)	UN	SampleRate2	1	PrivateTag
+(0029,"SPI-P Release 1;1",00)	LT	ZoomID	1	PrivateTag
+(0029,"SPI-P Release 1;1",01)	DS	ZoomRectangle	1-n	PrivateTag
+(0029,"SPI-P Release 1;1",03)	DS	ZoomFactor	1	PrivateTag
+(0029,"SPI-P Release 1;1",04)	US	ZoomFunction	1	PrivateTag
+(0029,"SPI-P Release 1;1",0e)	CS	ZoomEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;1",0f)	CS	ZoomSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1;1",40)	LT	MagnifyingGlassID	1	PrivateTag
+(0029,"SPI-P Release 1;1",41)	DS	MagnifyingGlassRectangle	1-n	PrivateTag
+(0029,"SPI-P Release 1;1",43)	DS	MagnifyingGlassFactor	1	PrivateTag
+(0029,"SPI-P Release 1;1",44)	US	MagnifyingGlassFunction	1	PrivateTag
+(0029,"SPI-P Release 1;1",4e)	CS	MagnifyingGlassEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;1",4f)	CS	MagnifyingGlassSelectStatus	1	PrivateTag
+
+(0029,"SPI-P Release 1;2",00)	LT	SubtractionMaskID	1	PrivateTag
+(0029,"SPI-P Release 1;2",04)	UN	MaskingFunction	1	PrivateTag
+(0029,"SPI-P Release 1;2",0c)	UN	ProprietaryMaskingParameters	1	PrivateTag
+(0029,"SPI-P Release 1;2",1e)	CS	SubtractionMaskEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;2",1f)	CS	SubtractionMaskSelectStatus	1	PrivateTag
+(0029,"SPI-P Release 1;3",00)	LT	ImageEnhancementID	1	PrivateTag
+(0029,"SPI-P Release 1;3",01)	LT	ImageEnhancement	1	PrivateTag
+(0029,"SPI-P Release 1;3",02)	LT	ConvolutionID	1	PrivateTag
+(0029,"SPI-P Release 1;3",03)	LT	ConvolutionType	1	PrivateTag
+(0029,"SPI-P Release 1;3",04)	LT	ConvolutionKernelSizeID	1	PrivateTag
+(0029,"SPI-P Release 1;3",05)	US	ConvolutionKernelSize	2	PrivateTag
+(0029,"SPI-P Release 1;3",06)	US	ConvolutionKernel	1-n	PrivateTag
+(0029,"SPI-P Release 1;3",0c)	DS	EnhancementGain	1	PrivateTag
+(0029,"SPI-P Release 1;3",1e)	CS	ImageEnhancementEnableStatus	1	PrivateTag
+(0029,"SPI-P Release 1;3",1f)	CS	ImageEnhancementSelectStatus	1	PrivateTag
+
+(0011,"SPI-P Release 2;1",18)	LT	Unknown	1	PrivateTag
+(0023,"SPI-P Release 2;1",0d)	UI	Unknown	1	PrivateTag
+(0023,"SPI-P Release 2;1",0e)	UI	Unknown	1	PrivateTag
+
+(0009,"SPI-P-GV-CT Release 1",00)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",10)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",20)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",30)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",40)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",50)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",60)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",70)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",75)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",80)	LO	Unknown	1	PrivateTag
+(0009,"SPI-P-GV-CT Release 1",90)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",08)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",09)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",0a)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",10)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",20)	TM	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",50)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",60)	DS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",61)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",63)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",64)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",65)	IS	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",70)	LT	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",80)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",81)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",90)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a0)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a1)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a2)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",a3)	US	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",b0)	LO	Unknown	1	PrivateTag
+(0019,"SPI-P-GV-CT Release 1",b1)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",20)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",30)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",40)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",50)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",60)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",70)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",80)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",90)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a0)	US	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a1)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a2)	DS	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a3)	LT	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",a4)	LT	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",b0)	LO	Unknown	1	PrivateTag
+(0021,"SPI-P-GV-CT Release 1",c0)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",10)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",30)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",31)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",32)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",33)	UL	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",80)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",90)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",d0)	IS	Unknown	1	PrivateTag
+(0029,"SPI-P-GV-CT Release 1",d1)	IS	Unknown	1	PrivateTag
+
+(0019,"SPI-P-PCR Release 2",30)	US	Unknown	1	PrivateTag
+
+(0021,"SPI-P-Private-CWS Release 1",00)	LT	WindowOfImagesID	1	PrivateTag
+(0021,"SPI-P-Private-CWS Release 1",01)	CS	WindowOfImagesType	1	PrivateTag
+(0021,"SPI-P-Private-CWS Release 1",02)	IS	WindowOfImagesScope	1-n	PrivateTag
+
+(0019,"SPI-P-Private-DCI Release 1",10)	UN	ECGTimeMapDataBitsAllocated	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",11)	UN	ECGTimeMapDataBitsStored	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",12)	UN	ECGTimeMapDataHighBit	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",13)	UN	ECGTimeMapDataRepresentation	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",14)	UN	ECGTimeMapDataSmallestDataValue	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",15)	UN	ECGTimeMapDataLargestDataValue	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",16)	UN	ECGTimeMapDataNumberOfDataValues	1	PrivateTag
+(0019,"SPI-P-Private-DCI Release 1",17)	UN	ECGTimeMapData	1	PrivateTag
+
+(0021,"SPI-P-Private_CDS Release 1",40)	IS	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_CDS Release 1",00)	UN	Unknown	1	PrivateTag
+
+(0019,"SPI-P-Private_ICS Release 1",30)	DS	Unknown	1	PrivateTag
+(0019,"SPI-P-Private_ICS Release 1",31)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",08)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",0f)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",10)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",1b)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",1c)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",21)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",43)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",44)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",4C)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",67)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",68)	US	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",6A)	LO	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1",6B)	US	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;1",00)	SL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",05)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",06)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",20)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",21)	FL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;1",CD)	SQ	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;2",00)	FD	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",01)	FD	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",02)	FD	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",03)	SL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",04)	SL	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;2",05)	SL	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;3",C0)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C1)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C2)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C3)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C4)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;3",C5)	SQ	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;4",02)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;4",9A)	SQ	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;4",E0)	SQ	Unknown	1	PrivateTag
+
+(0029,"SPI-P-Private_ICS Release 1;5",50)	CS	Unknown	1	PrivateTag
+(0029,"SPI-P-Private_ICS Release 1;5",55)	CS	Unknown	1	PrivateTag
+
+(0019,"SPI-P-XSB-DCI Release 1",10)	LT	VideoBeamBoost	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",11)	US	ChannelGeneratingVideoSync	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",12)	US	VideoGain	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",13)	US	VideoOffset	1	PrivateTag
+(0019,"SPI-P-XSB-DCI Release 1",20)	DS	RTDDataCompressionFactor	1	PrivateTag
+
+(0029,"Silhouette Annot V1.0",11)	IS	AnnotationName	1	PrivateTag
+(0029,"Silhouette Annot V1.0",12)	LT	AnnotationFont	1	PrivateTag
+(0029,"Silhouette Annot V1.0",13)	LT	AnnotationTextForegroundColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",14)	LT	AnnotationTextBackgroundColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",15)	UL	AnnotationTextBackingMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",16)	UL	AnnotationTextJustification	1	PrivateTag
+(0029,"Silhouette Annot V1.0",17)	UL	AnnotationTextLocation	1	PrivateTag
+(0029,"Silhouette Annot V1.0",18)	LT	AnnotationTextString	1	PrivateTag
+(0029,"Silhouette Annot V1.0",19)	UL	AnnotationTextAttachMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",20)	UL	AnnotationTextCursorMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",21)	UL	AnnotationTextShadowOffsetX	1	PrivateTag
+(0029,"Silhouette Annot V1.0",22)	UL	AnnotationTextShadowOffsetY	1	PrivateTag
+(0029,"Silhouette Annot V1.0",23)	LT	AnnotationLineColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",24)	UL	AnnotationLineThickness	1	PrivateTag
+(0029,"Silhouette Annot V1.0",25)	UL	AnnotationLineType	1	PrivateTag
+(0029,"Silhouette Annot V1.0",26)	UL	AnnotationLineStyle	1	PrivateTag
+(0029,"Silhouette Annot V1.0",27)	UL	AnnotationLineDashLength	1	PrivateTag
+(0029,"Silhouette Annot V1.0",28)	UL	AnnotationLineAttachMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",29)	UL	AnnotationLinePointCount	1	PrivateTag
+(0029,"Silhouette Annot V1.0",30)	FD	AnnotationLinePoints	1	PrivateTag
+(0029,"Silhouette Annot V1.0",31)	UL	AnnotationLineControlSize	1	PrivateTag
+(0029,"Silhouette Annot V1.0",32)	LT	AnnotationMarkerColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",33)	UL	AnnotationMarkerType	1	PrivateTag
+(0029,"Silhouette Annot V1.0",34)	UL	AnnotationMarkerSize	1	PrivateTag
+(0029,"Silhouette Annot V1.0",35)	FD	AnnotationMarkerLocation	1	PrivateTag
+(0029,"Silhouette Annot V1.0",36)	UL	AnnotationMarkerAttachMode	1	PrivateTag
+(0029,"Silhouette Annot V1.0",37)	LT	AnnotationGeomColor	1	PrivateTag
+(0029,"Silhouette Annot V1.0",38)	UL	AnnotationGeomThickness	1	PrivateTag
+(0029,"Silhouette Annot V1.0",39)	UL	AnnotationGeomLineStyle	1	PrivateTag
+(0029,"Silhouette Annot V1.0",40)	UL	AnnotationGeomDashLength	1	PrivateTag
+(0029,"Silhouette Annot V1.0",41)	UL	AnnotationGeomFillPattern	1	PrivateTag
+(0029,"Silhouette Annot V1.0",42)	UL	AnnotationInteractivity	1	PrivateTag
+(0029,"Silhouette Annot V1.0",43)	FD	AnnotationArrowLength	1	PrivateTag
+(0029,"Silhouette Annot V1.0",44)	FD	AnnotationArrowAngle	1	PrivateTag
+(0029,"Silhouette Annot V1.0",45)	UL	AnnotationDontSave	1	PrivateTag
+
+(0029,"Silhouette Graphics Export V1.0",00)	UI	Unknown	1	PrivateTag
+
+(0029,"Silhouette Line V1.0",11)	IS	LineName	1	PrivateTag
+(0029,"Silhouette Line V1.0",12)	LT	LineNameFont	1	PrivateTag
+(0029,"Silhouette Line V1.0",13)	UL	LineNameDisplay	1	PrivateTag
+(0029,"Silhouette Line V1.0",14)	LT	LineNormalColor	1	PrivateTag
+(0029,"Silhouette Line V1.0",15)	UL	LineType	1	PrivateTag
+(0029,"Silhouette Line V1.0",16)	UL	LineThickness	1	PrivateTag
+(0029,"Silhouette Line V1.0",17)	UL	LineStyle	1	PrivateTag
+(0029,"Silhouette Line V1.0",18)	UL	LineDashLength	1	PrivateTag
+(0029,"Silhouette Line V1.0",19)	UL	LineInteractivity	1	PrivateTag
+(0029,"Silhouette Line V1.0",20)	LT	LineMeasurementColor	1	PrivateTag
+(0029,"Silhouette Line V1.0",21)	LT	LineMeasurementFont	1	PrivateTag
+(0029,"Silhouette Line V1.0",22)	UL	LineMeasurementDashLength	1	PrivateTag
+(0029,"Silhouette Line V1.0",23)	UL	LinePointSpace	1	PrivateTag
+(0029,"Silhouette Line V1.0",24)	FD	LinePoints	1	PrivateTag
+(0029,"Silhouette Line V1.0",25)	UL	LineControlPointSize	1	PrivateTag
+(0029,"Silhouette Line V1.0",26)	UL	LineControlPointSpace	1	PrivateTag
+(0029,"Silhouette Line V1.0",27)	FD	LineControlPoints	1	PrivateTag
+(0029,"Silhouette Line V1.0",28)	LT	LineLabel	1	PrivateTag
+(0029,"Silhouette Line V1.0",29)	UL	LineDontSave	1	PrivateTag
+
+(0029,"Silhouette ROI V1.0",11)	IS	ROIName	1	PrivateTag
+(0029,"Silhouette ROI V1.0",12)	LT	ROINameFont	1	PrivateTag
+(0029,"Silhouette ROI V1.0",13)	LT	ROINormalColor	1	PrivateTag
+(0029,"Silhouette ROI V1.0",14)	UL	ROIFillPattern	1	PrivateTag
+(0029,"Silhouette ROI V1.0",15)	UL	ROIBpSeg	1	PrivateTag
+(0029,"Silhouette ROI V1.0",16)	UN	ROIBpSegPairs	1	PrivateTag
+(0029,"Silhouette ROI V1.0",17)	UL	ROISeedSpace	1	PrivateTag
+(0029,"Silhouette ROI V1.0",18)	UN	ROISeeds	1	PrivateTag
+(0029,"Silhouette ROI V1.0",19)	UL	ROILineThickness	1	PrivateTag
+(0029,"Silhouette ROI V1.0",20)	UL	ROILineStyle	1	PrivateTag
+(0029,"Silhouette ROI V1.0",21)	UL	ROILineDashLength	1	PrivateTag
+(0029,"Silhouette ROI V1.0",22)	UL	ROIInteractivity	1	PrivateTag
+(0029,"Silhouette ROI V1.0",23)	UL	ROINamePosition	1	PrivateTag
+(0029,"Silhouette ROI V1.0",24)	UL	ROINameDisplay	1	PrivateTag
+(0029,"Silhouette ROI V1.0",25)	LT	ROILabel	1	PrivateTag
+(0029,"Silhouette ROI V1.0",26)	UL	ROIShape	1	PrivateTag
+(0029,"Silhouette ROI V1.0",27)	FD	ROIShapeTilt	1	PrivateTag
+(0029,"Silhouette ROI V1.0",28)	UL	ROIShapePointsCount	1	PrivateTag
+(0029,"Silhouette ROI V1.0",29)	UL	ROIShapePointsSpace	1	PrivateTag
+(0029,"Silhouette ROI V1.0",30)	FD	ROIShapePoints	1	PrivateTag
+(0029,"Silhouette ROI V1.0",31)	UL	ROIShapeControlPointsCount	1	PrivateTag
+(0029,"Silhouette ROI V1.0",32)	UL	ROIShapeControlPointsSpace	1	PrivateTag
+(0029,"Silhouette ROI V1.0",33)	FD	ROIShapeControlPoints	1	PrivateTag
+(0029,"Silhouette ROI V1.0",34)	UL	ROIDontSave	1	PrivateTag
+
+(0029,"Silhouette Sequence Ids V1.0",41)	SQ	Unknown	1	PrivateTag
+(0029,"Silhouette Sequence Ids V1.0",42)	SQ	Unknown	1	PrivateTag
+(0029,"Silhouette Sequence Ids V1.0",43)	SQ	Unknown	1	PrivateTag
+
+(0029,"Silhouette V1.0",13)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",14)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",17)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",18)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",19)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1a)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1b)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1c)	UL	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1d)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",1e)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",21)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",22)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",23)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",24)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",25)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",27)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",28)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",29)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",30)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",52)	US	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",53)	LT	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",54)	UN	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",55)	LT	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",56)	LT	Unknown	1	PrivateTag
+(0029,"Silhouette V1.0",57)	UN	Unknown	1	PrivateTag
+
+(0135,"SONOWAND AS",10)	LO	UltrasoundScannerName	1	PrivateTag
+(0135,"SONOWAND AS",11)	LO	TransducerSerial	1	PrivateTag
+(0135,"SONOWAND AS",12)	LO	ProbeApplication	1	PrivateTag
+
+(0017,"SVISION",00)	LO	ExtendedBodyPart	1	PrivateTag
+(0017,"SVISION",10)	LO	ExtendedViewPosition	1	PrivateTag
+(0017,"SVISION",F0)	IS	ImagesSOPClass	1	PrivateTag
+(0019,"SVISION",00)	IS	AECField	1	PrivateTag
+(0019,"SVISION",01)	IS	AECFilmScreen	1	PrivateTag
+(0019,"SVISION",02)	IS	AECDensity	1	PrivateTag
+(0019,"SVISION",10)	IS	PatientThickness	1	PrivateTag
+(0019,"SVISION",18)	IS	BeamDistance	1	PrivateTag
+(0019,"SVISION",20)	IS	WorkstationNumber	1	PrivateTag
+(0019,"SVISION",28)	IS	TubeNumber	1	PrivateTag
+(0019,"SVISION",30)	IS	BuckyGrid	1	PrivateTag
+(0019,"SVISION",34)	IS	Focus	1	PrivateTag
+(0019,"SVISION",38)	IS	Child	1	PrivateTag
+(0019,"SVISION",40)	IS	CollimatorDistanceX	1	PrivateTag
+(0019,"SVISION",41)	IS	CollimatorDistanceY	1	PrivateTag
+(0019,"SVISION",50)	IS	CentralBeamHeight	1	PrivateTag
+(0019,"SVISION",60)	IS	BuckyAngle	1	PrivateTag
+(0019,"SVISION",68)	IS	CArmAngle	1	PrivateTag
+(0019,"SVISION",69)	IS	CollimatorAngle	1	PrivateTag
+(0019,"SVISION",70)	IS	FilterNumber	1	PrivateTag
+(0019,"SVISION",74)	LO	FilterMaterial1	1	PrivateTag
+(0019,"SVISION",75)	LO	FilterMaterial2	1	PrivateTag
+(0019,"SVISION",78)	DS	FilterThickness1	1	PrivateTag
+(0019,"SVISION",79)	DS	FilterThickness2	1	PrivateTag
+(0019,"SVISION",80)	IS	BuckyFormat	1	PrivateTag
+(0019,"SVISION",81)	IS	ObjectPosition	1	PrivateTag
+(0019,"SVISION",90)	LO	DeskCommand	1	PrivateTag
+(0019,"SVISION",A0)	DS	ExtendedExposureTime	1	PrivateTag
+(0019,"SVISION",A1)	DS	ActualExposureTime	1	PrivateTag
+(0019,"SVISION",A8)	DS	ExtendedXRayTubeCurrent	1	PrivateTag
+(0021,"SVISION",00)	DS	NoiseReduction	1	PrivateTag
+(0021,"SVISION",01)	DS	ContrastAmplification	1	PrivateTag
+(0021,"SVISION",02)	DS	EdgeContrastBoosting	1	PrivateTag
+(0021,"SVISION",03)	DS	LatitudeReduction	1	PrivateTag
+(0021,"SVISION",10)	LO	FindRangeAlgorithm	1	PrivateTag
+(0021,"SVISION",11)	DS	ThresholdCAlgorithm	1	PrivateTag
+(0021,"SVISION",20)	LO	SensometricCurve	1	PrivateTag
+(0021,"SVISION",30)	DS	LowerWindowOffset	1	PrivateTag
+(0021,"SVISION",31)	DS	UpperWindowOffset	1	PrivateTag
+(0021,"SVISION",40)	DS	MinPrintableDensity	1	PrivateTag
+(0021,"SVISION",41)	DS	MaxPrintableDensity	1	PrivateTag
+(0021,"SVISION",90)	DS	Brightness	1	PrivateTag
+(0021,"SVISION",91)	DS	Contrast	1	PrivateTag
+(0021,"SVISION",92)	DS	ShapeFactor	1	PrivateTag
+(0023,"SVISION",00)	LO	ImageLaterality	1	PrivateTag
+(0023,"SVISION",01)	IS	LetterPosition	1	PrivateTag
+(0023,"SVISION",02)	IS	BurnedInAnnotation	1	PrivateTag
+(0023,"SVISION",03)	LO	Unknown	1	PrivateTag
+(0023,"SVISION",F0)	IS	ImageSOPClass	1	PrivateTag
+(0025,"SVISION",00)	IS	OriginalImage	1	PrivateTag
+(0025,"SVISION",01)	IS	NotProcessedImage	1	PrivateTag
+(0025,"SVISION",02)	IS	CutOutImage	1	PrivateTag
+(0025,"SVISION",03)	IS	DuplicatedImage	1	PrivateTag
+(0025,"SVISION",04)	IS	StoredImage	1	PrivateTag
+(0025,"SVISION",05)	IS	RetrievedImage	1	PrivateTag
+(0025,"SVISION",06)	IS	RemoteImage	1	PrivateTag
+(0025,"SVISION",07)	IS	MediaStoredImage	1	PrivateTag
+(0025,"SVISION",08)	IS	ImageState	1	PrivateTag
+(0025,"SVISION",20)	LO	SourceImageFile	1	PrivateTag
+(0025,"SVISION",21)	UI	Unknown	1	PrivateTag
+(0027,"SVISION",00)	IS	NumberOfSeries	1	PrivateTag
+(0027,"SVISION",01)	IS	NumberOfStudies	1	PrivateTag
+(0027,"SVISION",10)	DT	OldestSeries	1	PrivateTag
+(0027,"SVISION",11)	DT	NewestSeries	1	PrivateTag
+(0027,"SVISION",12)	DT	OldestStudy	1	PrivateTag
+(0027,"SVISION",13)	DT	NewestStudy	1	PrivateTag
+
+(0009,"TOSHIBA_MEC_1.0",01)	LT	Unknown	1	PrivateTag
+(0009,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0009,"TOSHIBA_MEC_1.0",03)	US	Unknown	1-n	PrivateTag
+(0009,"TOSHIBA_MEC_1.0",04)	US	Unknown	1-n	PrivateTag
+(0011,"TOSHIBA_MEC_1.0",01)	LT	Unknown	1	PrivateTag
+(0011,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_1.0",01)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0021,"TOSHIBA_MEC_1.0",01)	US	Unknown	1-n	PrivateTag
+(0021,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(0021,"TOSHIBA_MEC_1.0",03)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",01)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",02)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",03)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_1.0",10)	US	Unknown	1-n	PrivateTag
+
+(0019,"TOSHIBA_MEC_CT_1.0",01)	IS	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",02)	IS	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",03)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",04)	LT	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",05)	LT	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",06)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",07)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",08)	LT	OrientationHeadFeet	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",09)	LT	ViewDirection	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0a)	LT	OrientationSupineProne	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0b)	DS	Unknown	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0c)	US	Unknown	1-n	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0d)	TM	Time	1	PrivateTag
+(0019,"TOSHIBA_MEC_CT_1.0",0e)	DS	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",01)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",02)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",03)	IS	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",04)	IS	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",05)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",07)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",08)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",09)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0a)	LT	Unknown	1	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0b)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0c)	US	Unknown	1-n	PrivateTag
+(7ff1,"TOSHIBA_MEC_CT_1.0",0d)	US	Unknown	1-n	PrivateTag
+#
+# end of private.dic
+#
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.2.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.2.patch
new file mode 100644
index 0000000..abc0d68
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.2.patch
@@ -0,0 +1,48 @@
+diff -urEb dcmtk-3.6.2.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.2/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.2.orig/CMake/GenerateDCMTKConfigure.cmake	2020-01-06 17:42:52.299540389 +0100
++++ dcmtk-3.6.2/CMake/GenerateDCMTKConfigure.cmake	2020-01-06 17:43:56.707520036 +0100
+@@ -568,12 +568,12 @@
+   ENDIF(HAVE_CSTDDEF)
+ 
+   CHECK_FUNCTIONWITHHEADER_EXISTS(feenableexcept "${HEADERS}" HAVE_PROTOTYPE_FEENABLEEXCEPT)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(isinf "${HEADERS}" HAVE_PROTOTYPE_ISINF)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(isnan "${HEADERS}" HAVE_PROTOTYPE_ISNAN)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(finite "${HEADERS}" HAVE_PROTOTYPE_FINITE)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(std::isinf "${HEADERS}" HAVE_PROTOTYPE_STD__ISINF)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(std::isnan "${HEADERS}" HAVE_PROTOTYPE_STD__ISNAN)
+-  CHECK_FUNCTIONWITHHEADER_EXISTS(std::finite "${HEADERS}" HAVE_PROTOTYPE_STD__FINITE)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("isinf(0.)" "${HEADERS}" HAVE_PROTOTYPE_ISINF)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("isnan(0.)" "${HEADERS}" HAVE_PROTOTYPE_ISNAN)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("finite(0.)" "${HEADERS}" HAVE_PROTOTYPE_FINITE)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("std::isinf(0.)" "${HEADERS}" HAVE_PROTOTYPE_STD__ISINF)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("std::isnan(0.)" "${HEADERS}" HAVE_PROTOTYPE_STD__ISNAN)
++  CHECK_FUNCTIONWITHHEADER_EXISTS("std::finite(0.)" "${HEADERS}" HAVE_PROTOTYPE_STD__FINITE)
+   CHECK_FUNCTIONWITHHEADER_EXISTS(flock "${HEADERS}" HAVE_PROTOTYPE_FLOCK)
+   CHECK_FUNCTIONWITHHEADER_EXISTS(gethostbyname "${HEADERS}" HAVE_PROTOTYPE_GETHOSTBYNAME)
+   CHECK_FUNCTIONWITHHEADER_EXISTS(gethostbyname_r "${HEADERS}" HAVE_PROTOTYPE_GETHOSTBYNAME_R)
+diff -urEb dcmtk-3.6.2.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.2/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.2.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 17:42:52.283540394 +0100
++++ dcmtk-3.6.2/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 17:46:21.711473976 +0100
+@@ -152,6 +152,12 @@
+     /// returns an iterator to the end of the repeating tag dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-3.6.2.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.2/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.2.orig/dcmdata/libsrc/dcdict.cc	2020-01-06 17:42:52.287540392 +0100
++++ dcmtk-3.6.2/dcmdata/libsrc/dcdict.cc	2020-01-06 17:47:18.335299472 +0100
+@@ -876,3 +876,6 @@
+   wrlock().clear();
+   unlock();
+ }
++
++
++#include "dcdict_orthanc.cc"
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.4.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.4.patch
new file mode 100644
index 0000000..cf274da
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.4.patch
@@ -0,0 +1,99 @@
+diff -urEb dcmtk-3.6.4.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.4.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 19:55:12.887153062 +0100
++++ dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 19:55:28.156447233 +0100
+@@ -152,6 +152,12 @@
+     /// returns an iterator to the end of the repeating tag dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-3.6.4.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.4/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.4.orig/dcmdata/libsrc/dcdict.cc	2020-01-06 19:55:12.899154075 +0100
++++ dcmtk-3.6.4/dcmdata/libsrc/dcdict.cc	2020-01-06 19:55:28.156447233 +0100
+@@ -899,3 +899,6 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.4.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.4/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.4.orig/dcmdata/libsrc/dcpxitem.cc	2020-01-06 19:55:12.899154075 +0100
++++ dcmtk-3.6.4/dcmdata/libsrc/dcpxitem.cc	2020-01-06 19:55:28.156447233 +0100
+@@ -36,6 +36,9 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
++
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.4.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.4/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.4.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-01-06 19:55:12.911155088 +0100
++++ dcmtk-3.6.4/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-01-06 19:56:26.991372656 +0100
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)) + 0)
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -148,7 +148,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ FairMutex::FairMutex ()
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)) + 0)
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex) + 0)
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.4.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.4/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.4.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-01-06 19:55:12.951158464 +0100
++++ dcmtk-3.6.4/ofstd/include/dcmtk/ofstd/offile.h	2020-01-06 19:55:28.156447233 +0100
+@@ -575,7 +575,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.5.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.5.patch
new file mode 100644
index 0000000..1a4b40e
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.5.patch
@@ -0,0 +1,152 @@
+diff -urEb dcmtk-3.6.5.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.5/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.5.orig/CMake/GenerateDCMTKConfigure.cmake	2020-11-04 18:27:08.984662119 +0100
++++ dcmtk-3.6.5/CMake/GenerateDCMTKConfigure.cmake	2020-11-04 18:27:48.232609773 +0100
+@@ -169,6 +169,8 @@
+ endif()
+ 
+ # Check the sizes of various types
++if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
++  # This doesn't work for wasm, Orthanc defines the macros manually
+ include (CheckTypeSize)
+ CHECK_TYPE_SIZE("char" SIZEOF_CHAR)
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+@@ -177,6 +179,7 @@
+ CHECK_TYPE_SIZE("long" SIZEOF_LONG)
+ CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
+ CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
++endif()
+ 
+ # Check for include files, libraries, and functions
+ include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+diff -urEb dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-11-04 18:27:08.972662135 +0100
++++ dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-11-04 18:27:48.232609773 +0100
+@@ -152,6 +152,12 @@
+     /// returns an iterator to the end of the repeating tag dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc	2020-11-04 18:27:08.976662131 +0100
++++ dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc	2020-11-04 18:27:48.232609773 +0100
+@@ -900,3 +900,6 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc	2020-11-04 18:27:08.976662131 +0100
++++ dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc	2020-11-04 18:27:48.232609773 +0100
+@@ -36,6 +36,9 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
++
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.5.orig/dcmnet/libsrc/dulparse.cc dcmtk-3.6.5/dcmnet/libsrc/dulparse.cc
+--- dcmtk-3.6.5.orig/dcmnet/libsrc/dulparse.cc	2020-11-04 18:27:09.004662093 +0100
++++ dcmtk-3.6.5/dcmnet/libsrc/dulparse.cc	2020-11-04 18:29:41.832458294 +0100
+@@ -736,7 +736,14 @@
+       << " is larger than maximum allowed UID length " << DICOM_UI_LENGTH << " (will use 64 bytes max)");
+       UIDLength = DICOM_UI_LENGTH;
+     }
+-    OFStandard::strlcpy(role->SOPClassUID, (char*)buf, UIDLength+1 /* +1 for 0-byte */);
++
++    // Patch from: https://github.com/DCMTK/dcmtk/commit/10428a74e74c003b3ff31c992f658d528b626fab
++    // The UID in the source buffer is not necessarily null terminated. Copy with memcpy
++    // and add a zero byte. We have already checked that there is enough data available
++    // in the source source buffer and enough space in the target buffer.
++    (void) memcpy(role->SOPClassUID, buf, UIDLength);
++    role->SOPClassUID[UIDLength] = '\0';
++    
+     buf += UIDLength;
+     role->SCURole = *buf++;
+     role->SCPRole = *buf++;
+diff -urEb dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-11-04 18:27:08.980662125 +0100
++++ dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-11-04 18:27:48.232609773 +0100
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)) + 0)
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -148,7 +148,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ FairMutex::FairMutex ()
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)) + 0)
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex) + 0)
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc dcmtk-3.6.5/oflog/libsrc/oflog.cc
+--- dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc	2020-11-04 18:27:08.984662119 +0100
++++ dcmtk-3.6.5/oflog/libsrc/oflog.cc	2020-11-04 18:27:48.232609773 +0100
+@@ -19,6 +19,10 @@
+  *
+  */
+ 
++#if defined(_WIN32)
++#  include 
++#endif
++
+ #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+ #include "dcmtk/oflog/oflog.h"
+ 
+diff -urEb dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-11-04 18:27:09.008662088 +0100
++++ dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h	2020-11-04 18:27:48.232609773 +0100
+@@ -575,7 +575,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.6.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.6.patch
new file mode 100644
index 0000000..21ada59
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.6.patch
@@ -0,0 +1,183 @@
+diff -urEb dcmtk-3.6.6.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.6/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.6.orig/CMake/GenerateDCMTKConfigure.cmake	2021-01-26 08:51:48.815071681 +0100
++++ dcmtk-3.6.6/CMake/GenerateDCMTKConfigure.cmake	2021-01-26 08:52:06.331135995 +0100
+@@ -169,6 +169,8 @@
+ endif()
+ 
+ # Check the sizes of various types
++if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
++  # This doesn't work for wasm, Orthanc defines the macros manually
+ include (CheckTypeSize)
+ CHECK_TYPE_SIZE("char" SIZEOF_CHAR)
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+@@ -177,6 +179,7 @@
+ CHECK_TYPE_SIZE("long" SIZEOF_LONG)
+ CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
+ CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
++endif()
+ 
+ # Check for include files, libraries, and functions
+ include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+diff -urEb dcmtk-3.6.6.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.6/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.6.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2021-01-26 08:51:48.859071844 +0100
++++ dcmtk-3.6.6/dcmdata/include/dcmtk/dcmdata/dcdict.h	2021-01-26 08:52:06.331135995 +0100
+@@ -152,6 +152,12 @@
+     /// returns an iterator to the end of the repeating tag dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-3.6.6.orig/dcmdata/libsrc/dcdicdir.cc dcmtk-3.6.6/dcmdata/libsrc/dcdicdir.cc
+--- dcmtk-3.6.6.orig/dcmdata/libsrc/dcdicdir.cc	2021-01-26 08:51:48.863071859 +0100
++++ dcmtk-3.6.6/dcmdata/libsrc/dcdicdir.cc	2021-01-26 08:56:03.519887982 +0100
+@@ -1032,7 +1032,14 @@
+     // insert Media Stored SOP Class UID
+     insertMediaSOPUID(metainfo);
+ 
+-    getDirFileFormat().validateMetaInfo(outxfer);
++    /**
++     * Patch for Orthanc: In DCMTK 3.6.6, the default value for the
++     * second argument changed from "EWM_fileformat" to
++     * "EWM_createNewMeta". This sets "MediaStorageSOPClassUID"
++     * (0002,0002) in meta-header to "1.2.276.0.7230010.3.1.0.1"
++     * instead of expected "1.2.840.10008.1.3.10".
++     **/
++    getDirFileFormat().validateMetaInfo(outxfer, EWM_fileformat);
+ 
+     {
+         // it is important that the cache object is destroyed before the file is renamed!
+diff -urEb dcmtk-3.6.6.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.6/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.6.orig/dcmdata/libsrc/dcdict.cc	2021-01-26 08:51:48.863071859 +0100
++++ dcmtk-3.6.6/dcmdata/libsrc/dcdict.cc	2021-01-26 08:52:06.331135995 +0100
+@@ -900,3 +900,6 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.6.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.6/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.6.orig/dcmdata/libsrc/dcpxitem.cc	2021-01-26 08:51:48.863071859 +0100
++++ dcmtk-3.6.6/dcmdata/libsrc/dcpxitem.cc	2021-01-26 08:52:06.335136010 +0100
+@@ -36,6 +36,9 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
++ 
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.6.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.6/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.6.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2021-01-26 08:51:48.847071800 +0100
++++ dcmtk-3.6.6/oflog/include/dcmtk/oflog/thread/syncpub.h	2021-01-26 08:52:06.335136010 +0100
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)) + 0)
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -148,7 +148,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ FairMutex::FairMutex ()
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)) + 0)
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex) + 0)
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.6.orig/oflog/libsrc/oflog.cc dcmtk-3.6.6/oflog/libsrc/oflog.cc
+--- dcmtk-3.6.6.orig/oflog/libsrc/oflog.cc	2021-01-26 08:51:48.847071800 +0100
++++ dcmtk-3.6.6/oflog/libsrc/oflog.cc	2021-01-26 08:52:06.335136010 +0100
+@@ -19,6 +19,10 @@
+  *
+  */
+ 
++#if defined(_WIN32)
++#  include 
++#endif
++
+ #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+ #include "dcmtk/oflog/oflog.h"
+ 
+diff -urEb dcmtk-3.6.6.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.6/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.6.orig/ofstd/include/dcmtk/ofstd/offile.h	2021-01-26 08:51:48.863071859 +0100
++++ dcmtk-3.6.6/ofstd/include/dcmtk/ofstd/offile.h	2021-01-26 08:52:06.335136010 +0100
+@@ -586,7 +586,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
+
+diff -urEb dcmtk-3.6.6.orig/config/tests/arith.cc dcmtk-3.6.6/config/tests/arith.cc
+--- dcmtk-3.6.6.orig/config/tests/arith.cc      2022-03-28 19:17:03.000000000 +0000
++++ dcmtk-3.6.6/config/tests/arith.cc   2022-03-28 19:18:12.000000000 +0000
+@@ -40,7 +40,7 @@
+ #include 
+ #endif
+ 
+-#ifdef __APPLE__
++#if defined(__APPLE__) && !defined(__aarch64__)
+ // For controlling floating point exceptions on OS X.
+ #include 
+ #endif
+@@ -340,7 +340,7 @@
+ #ifdef HAVE_WINDOWS_H
+     _clearfp();
+     _controlfp( _controlfp(0,0) & ~_EM_INVALID, _MCW_EM );
+-#elif defined(__APPLE__)
++#elif defined(__APPLE__) && !defined(__aarch64__)
+     _MM_SET_EXCEPTION_MASK( _MM_GET_EXCEPTION_MASK() & ~_MM_MASK_INVALID );
+ #elif defined(HAVE_FENV_H) && defined(HAVE_PROTOTYPE_FEENABLEEXCEPT)
+     feenableexcept( FE_INVALID );
+@@ -382,7 +382,7 @@
+     _controlfp( _controlfp(0,0) | _EM_INVALID, _MCW_EM );
+ #elif defined(HAVE_FENV_H)
+     feclearexcept( FE_INVALID );
+-#ifdef __APPLE__
++#if defined(__APPLE__) && !defined(__aarch64__)
+     _MM_SET_EXCEPTION_MASK( _MM_GET_EXCEPTION_MASK() | _MM_MASK_INVALID );
+ #elif defined(HAVE_FENV_H) && defined(HAVE_PROTOTYPE_FEENABLEEXCEPT)
+     fedisableexcept( FE_INVALID );
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.7-visual-studio.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.7-visual-studio.patch
new file mode 100644
index 0000000..6efb649
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.7-visual-studio.patch
@@ -0,0 +1,821 @@
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jccoefct.c dcmtk-3.6.7/dcmjpeg/libijg12/jccoefct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jccoefct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jccoefct.c	2022-08-16 12:21:34.000000000 +0200
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jcdiffct.c dcmtk-3.6.7/dcmjpeg/libijg12/jcdiffct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jcdiffct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jcdiffct.c	2022-08-16 12:21:20.000000000 +0200
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jcpred.c dcmtk-3.6.7/dcmjpeg/libijg12/jcpred.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jcpred.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jcpred.c	2022-08-16 12:21:04.000000000 +0200
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jctrans.c dcmtk-3.6.7/dcmjpeg/libijg12/jctrans.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jctrans.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jctrans.c	2022-08-16 12:20:36.000000000 +0200
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdmerge.c dcmtk-3.6.7/dcmjpeg/libijg12/jdmerge.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdmerge.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jdmerge.c	2022-08-16 12:20:14.000000000 +0200
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdpostct.c dcmtk-3.6.7/dcmjpeg/libijg12/jdpostct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdpostct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jdpostct.c	2022-08-16 12:19:54.000000000 +0200
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+ 
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdpred.c dcmtk-3.6.7/dcmjpeg/libijg12/jdpred.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdpred.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jdpred.c	2022-08-16 12:19:22.000000000 +0200
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -195,7 +195,7 @@
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+ 
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdsample.c dcmtk-3.6.7/dcmjpeg/libijg12/jdsample.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdsample.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jdsample.c	2022-08-16 12:18:32.000000000 +0200
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdscale.c dcmtk-3.6.7/dcmjpeg/libijg12/jdscale.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jdscale.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jdscale.c	2022-08-16 12:18:02.000000000 +0200
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jquant1.c dcmtk-3.6.7/dcmjpeg/libijg12/jquant1.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jquant1.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jquant1.c	2022-08-16 12:17:44.000000000 +0200
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg12/jquant2.c dcmtk-3.6.7/dcmjpeg/libijg12/jquant2.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg12/jquant2.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg12/jquant2.c	2022-08-16 12:17:30.000000000 +0200
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jccoefct.c dcmtk-3.6.7/dcmjpeg/libijg16/jccoefct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jccoefct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jccoefct.c	2022-08-16 12:17:02.000000000 +0200
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jcdiffct.c dcmtk-3.6.7/dcmjpeg/libijg16/jcdiffct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jcdiffct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jcdiffct.c	2022-08-16 12:14:16.000000000 +0200
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jcpred.c dcmtk-3.6.7/dcmjpeg/libijg16/jcpred.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jcpred.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jcpred.c	2022-08-16 12:14:00.000000000 +0200
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jctrans.c dcmtk-3.6.7/dcmjpeg/libijg16/jctrans.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jctrans.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jctrans.c	2022-08-16 12:13:42.000000000 +0200
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdmerge.c dcmtk-3.6.7/dcmjpeg/libijg16/jdmerge.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdmerge.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jdmerge.c	2022-08-16 12:13:18.000000000 +0200
+@@ -171,7 +171,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -221,8 +221,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdpostct.c dcmtk-3.6.7/dcmjpeg/libijg16/jdpostct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdpostct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jdpostct.c	2022-08-16 12:12:54.000000000 +0200
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdpred.c dcmtk-3.6.7/dcmjpeg/libijg16/jdpred.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdpred.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jdpred.c	2022-08-16 12:12:26.000000000 +0200
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-   (void)cinfo;
+-  (void)comp_index;
++   //(void)cinfo;
++  //(void)comp_index;
+  UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4A);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -159,8 +159,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -172,8 +172,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5A);
+   JPEG_UNUSED(Rc);
+@@ -185,8 +185,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -198,8 +198,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6A);
+   JPEG_UNUSED(Rc);
+@@ -211,8 +211,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -224,8 +224,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7A);
+   JPEG_UNUSED(Rc);
+@@ -245,7 +245,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdsample.c dcmtk-3.6.7/dcmjpeg/libijg16/jdsample.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdsample.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jdsample.c	2022-08-16 12:10:32.000000000 +0200
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdscale.c dcmtk-3.6.7/dcmjpeg/libijg16/jdscale.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jdscale.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jdscale.c	2022-08-16 12:04:18.000000000 +0200
+@@ -67,8 +67,8 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
+   unsigned int xindex;
++  (void)cinfo;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+     output_buf[xindex] = (JSAMPLE) diff_buf[xindex];
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jquant1.c dcmtk-3.6.7/dcmjpeg/libijg16/jquant1.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jquant1.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jquant1.c	2022-08-16 12:03:56.000000000 +0200
+@@ -744,10 +744,10 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
++  (void) is_pre_scan;
+ 
+   /* Install my colormap. */
+   cinfo->colormap = cquantize->sv_colormap;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg16/jquant2.c dcmtk-3.6.7/dcmjpeg/libijg16/jquant2.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg16/jquant2.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg16/jquant2.c	2022-08-16 12:03:14.000000000 +0200
+@@ -224,7 +224,6 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -232,6 +231,7 @@
+   int row;
+   JDIMENSION col;
+   JDIMENSION width = cinfo->output_width;
++  (void) output_buf;
+ 
+   for (row = 0; row < num_rows; row++) {
+     ptr = input_buf[row];
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jccoefct.c dcmtk-3.6.7/dcmjpeg/libijg8/jccoefct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jccoefct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jccoefct.c	2022-08-16 12:27:04.000000000 +0200
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jcdiffct.c dcmtk-3.6.7/dcmjpeg/libijg8/jcdiffct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jcdiffct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jcdiffct.c	2022-08-16 12:26:48.000000000 +0200
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jcpred.c dcmtk-3.6.7/dcmjpeg/libijg8/jcpred.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jcpred.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jcpred.c	2022-08-16 12:26:32.000000000 +0200
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jctrans.c dcmtk-3.6.7/dcmjpeg/libijg8/jctrans.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jctrans.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jctrans.c	2022-08-16 12:25:56.000000000 +0200
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdmerge.c dcmtk-3.6.7/dcmjpeg/libijg8/jdmerge.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdmerge.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jdmerge.c	2022-08-16 12:25:36.000000000 +0200
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdpostct.c dcmtk-3.6.7/dcmjpeg/libijg8/jdpostct.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdpostct.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jdpostct.c	2022-08-16 12:25:12.000000000 +0200
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdpred.c dcmtk-3.6.7/dcmjpeg/libijg8/jdpred.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdpred.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jdpred.c	2022-08-16 12:23:34.000000000 +0200
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -194,7 +194,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdsample.c dcmtk-3.6.7/dcmjpeg/libijg8/jdsample.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdsample.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jdsample.c	2022-08-16 12:22:36.000000000 +0200
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdscale.c dcmtk-3.6.7/dcmjpeg/libijg8/jdscale.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jdscale.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jdscale.c	2022-08-16 12:22:06.000000000 +0200
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jquant1.c dcmtk-3.6.7/dcmjpeg/libijg8/jquant1.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jquant1.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jquant1.c	2022-08-16 12:21:50.000000000 +0200
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+diff -urEb dcmtk-3.6.7.orig/dcmjpeg/libijg8/jquant2.c dcmtk-3.6.7/dcmjpeg/libijg8/jquant2.c
+--- dcmtk-3.6.7.orig/dcmjpeg/libijg8/jquant2.c	2022-04-28 15:47:25.000000000 +0200
++++ dcmtk-3.6.7/dcmjpeg/libijg8/jquant2.c	2022-08-16 12:03:36.000000000 +0200
+@@ -224,7 +224,6 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -232,6 +231,7 @@
+   int row;
+   JDIMENSION col;
+   JDIMENSION width = cinfo->output_width;
++  (void) output_buf;
+ 
+   for (row = 0; row < num_rows; row++) {
+     ptr = input_buf[row];
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.7.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.7.patch
new file mode 100644
index 0000000..3550b1c
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.7.patch
@@ -0,0 +1,123 @@
+diff -urEb dcmtk-3.6.7.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.7/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.7.orig/CMake/GenerateDCMTKConfigure.cmake	2022-08-15 14:28:32.373922631 +0200
++++ dcmtk-3.6.7/CMake/GenerateDCMTKConfigure.cmake	2022-08-15 14:29:43.341136298 +0200
+@@ -183,6 +183,8 @@
+ 
+ # Check the sizes of various types
+ include (CheckTypeSize)
++if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
++  # This doesn't work for wasm, Orthanc defines the macros manually
+ CHECK_TYPE_SIZE("char" SIZEOF_CHAR)
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+ CHECK_TYPE_SIZE("float" SIZEOF_FLOAT)
+@@ -190,6 +192,7 @@
+ CHECK_TYPE_SIZE("long" SIZEOF_LONG)
+ CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
+ CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
++endif()
+ 
+ # Check for include files, libraries, and functions
+ include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+diff -urEb dcmtk-3.6.7.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.7/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.7.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2022-08-15 14:28:32.421922100 +0200
++++ dcmtk-3.6.7/dcmdata/include/dcmtk/dcmdata/dcdict.h	2022-08-15 14:30:16.224771418 +0200
+@@ -162,6 +162,12 @@
+     /// returns an iterator to the end of the repeating tag dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-3.6.7.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.7/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.7.orig/dcmdata/libsrc/dcdict.cc	2022-08-15 14:28:32.421922100 +0200
++++ dcmtk-3.6.7/dcmdata/libsrc/dcdict.cc	2022-08-15 14:31:15.220116058 +0200
+@@ -892,3 +892,5 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.7.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.7/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.7.orig/dcmdata/libsrc/dcpxitem.cc	2022-08-15 14:28:32.425922056 +0200
++++ dcmtk-3.6.7/dcmdata/libsrc/dcpxitem.cc	2022-08-15 14:31:28.887964099 +0200
+@@ -31,6 +31,9 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
++
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.7.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.7/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.7.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2022-08-15 14:28:32.401922322 +0200
++++ dcmtk-3.6.7/oflog/include/dcmtk/oflog/thread/syncpub.h	2022-08-15 14:31:52.415702413 +0200
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t) + 0))
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial) + 0))
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig) + 0))
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex + 0))
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.7.orig/oflog/libsrc/oflog.cc dcmtk-3.6.7/oflog/libsrc/oflog.cc
+--- dcmtk-3.6.7.orig/oflog/libsrc/oflog.cc	2022-08-15 14:28:32.405922278 +0200
++++ dcmtk-3.6.7/oflog/libsrc/oflog.cc	2022-08-15 14:32:16.815430896 +0200
+@@ -19,6 +19,10 @@
+  *
+  */
+ 
++#if defined(_WIN32)
++#  include 
++#endif
++
+ #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+ #include "dcmtk/oflog/oflog.h"
+ 
+diff -urEb dcmtk-3.6.7.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.7/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.7.orig/ofstd/include/dcmtk/ofstd/offile.h	2022-08-15 14:28:32.425922056 +0200
++++ dcmtk-3.6.7/ofstd/include/dcmtk/ofstd/offile.h	2022-08-15 14:32:41.471156396 +0200
+@@ -570,7 +570,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.8-visual-studio.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.8-visual-studio.patch
new file mode 100644
index 0000000..abc86fb
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.8-visual-studio.patch
@@ -0,0 +1,995 @@
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jccoefct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jccoefct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jccoefct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jccoefct.c	2024-01-09 17:48:28.974677157 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcdiffct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcdiffct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcdiffct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcdiffct.c	2024-01-09 17:48:36.414609533 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jcpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jcpred.c	2024-01-09 17:48:49.766488124 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jctrans.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jctrans.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jctrans.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jctrans.c	2024-01-09 17:49:00.070394388 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdmerge.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdmerge.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdmerge.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdmerge.c	2024-01-09 17:49:11.910286634 +0100
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpostct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpostct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpostct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpostct.c	2024-01-09 17:49:24.910168268 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+ 
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdpred.c	2024-01-09 17:50:00.513843814 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -195,7 +195,7 @@
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+ 
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdsample.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdsample.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdsample.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdsample.c	2024-01-09 17:50:36.545515066 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdscale.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdscale.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jdscale.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jdscale.c	2024-01-09 17:50:42.833457657 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant1.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant1.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant1.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant1.c	2024-01-09 17:51:03.049273013 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant2.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant2.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg12/jquant2.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg12/jquant2.c	2024-01-09 17:51:16.685148405 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jccoefct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jccoefct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jccoefct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jccoefct.c	2024-01-09 17:51:24.997072424 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcdiffct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcdiffct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcdiffct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcdiffct.c	2024-01-09 17:51:31.549012520 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jcpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jcpred.c	2024-01-09 17:51:40.740928459 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jctrans.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jctrans.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jctrans.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jctrans.c	2024-01-09 17:51:49.244850672 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdmerge.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdmerge.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdmerge.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdmerge.c	2024-01-09 17:51:59.852753613 +0100
+@@ -171,7 +171,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -221,8 +221,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpostct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpostct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpostct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpostct.c	2024-01-09 17:52:12.796635145 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdpred.c	2024-01-09 17:53:08.884121363 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-   (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+  UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4A);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -159,8 +159,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -172,8 +172,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5A);
+   JPEG_UNUSED(Rc);
+@@ -185,8 +185,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -198,8 +198,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6A);
+   JPEG_UNUSED(Rc);
+@@ -211,8 +211,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -224,8 +224,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7A);
+   JPEG_UNUSED(Rc);
+@@ -245,7 +245,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdsample.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdsample.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdsample.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdsample.c	2024-01-09 17:53:28.779938946 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdscale.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdscale.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jdscale.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jdscale.c	2024-01-09 17:53:34.795883773 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant1.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant1.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant1.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant1.c	2024-01-09 17:53:53.891708593 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant2.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant2.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg16/jquant2.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg16/jquant2.c	2024-01-09 17:54:05.051606183 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jccoefct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jccoefct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jccoefct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jccoefct.c	2024-01-09 17:54:11.635545753 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcdiffct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcdiffct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcdiffct.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcdiffct.c	2024-01-09 17:54:16.815498204 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jcpred.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jcpred.c	2024-01-09 17:54:25.827415468 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jctrans.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jctrans.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jctrans.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jctrans.c	2024-01-09 17:54:33.939340981 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdmerge.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdmerge.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdmerge.c	2024-01-09 17:13:10.345673450 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdmerge.c	2024-01-09 17:54:41.019275962 +0100
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpostct.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpostct.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpostct.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpostct.c	2024-01-09 17:54:48.891203659 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpred.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpred.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdpred.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdpred.c	2024-01-09 17:55:02.179081586 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -194,7 +194,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdsample.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdsample.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdsample.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdsample.c	2024-01-09 17:55:13.234979994 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdscale.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdscale.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jdscale.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jdscale.c	2024-01-09 17:55:21.722901985 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant1.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant1.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant1.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant1.c	2024-01-09 17:48:22.270738074 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant2.c dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant2.c
+--- dcmtk-DCMTK-3.6.8.orig/dcmjpeg/libijg8/jquant2.c	2024-01-09 17:13:10.349673411 +0100
++++ dcmtk-DCMTK-3.6.8/dcmjpeg/libijg8/jquant2.c	2024-01-09 17:47:42.343100533 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch
new file mode 100644
index 0000000..4532ab8
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch
@@ -0,0 +1,157 @@
+diff -urEb dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake	2024-11-25 16:54:59.036009112 +0100
+@@ -224,6 +224,8 @@
+ 
+ # Check the sizes of various types
+ include (CheckTypeSize)
++if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
++  # This doesn't work for wasm, Orthanc defines the macros manually
+ CHECK_TYPE_SIZE("char" SIZEOF_CHAR)
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+ CHECK_TYPE_SIZE("float" SIZEOF_FLOAT)
+@@ -231,6 +233,7 @@
+ CHECK_TYPE_SIZE("long" SIZEOF_LONG)
+ CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
+ CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
++endif()
+ 
+ # Check for include files, libraries, and functions
+ include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: arith.h
+Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: osconfig.h
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h	2024-11-25 16:54:59.036009112 +0100
+@@ -162,6 +162,12 @@
+     /// returns an iterator to the end of the repeating tag dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++    
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc	2024-11-25 16:54:59.036009112 +0100
+@@ -914,3 +914,5 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++#include "dcdict_orthanc.cc"
+Only in dcmtk-DCMTK-3.6.8/dcmdata/libsrc: dcdict_orthanc.cc
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc	2024-11-25 16:54:59.036009112 +0100
+@@ -31,6 +31,9 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
++
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc
+--- dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc	2024-11-25 16:54:59.036009112 +0100
+@@ -19,6 +19,11 @@
+  *
+  */
+ 
++#if defined(_WIN32)
++#  define __STDC_LIMIT_MACROS   // Get access to UINT16_MAX
++#  include 
++#endif
++
+ #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */
+ 
+ #include "dcmtk/dcmdata/dcostrmf.h" /* for class DcmOutputFileStream */
+diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h	2024-11-25 16:54:59.037009100 +0100
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t) + 0))
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial) + 0))
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig) + 0))
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex + 0))
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc
+--- dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc	2024-11-25 16:54:59.037009100 +0100
+@@ -19,6 +19,11 @@
+  *
+  */
+ 
++
++#if defined(_WIN32)
++#  include 
++#endif
++
+ #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+ #include "dcmtk/oflog/oflog.h"
+ 
+diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h	2024-11-25 16:54:59.037009100 +0100
+@@ -570,7 +570,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
+diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h
+--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h	2023-12-19 11:12:57.000000000 +0100
++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h	2024-11-25 17:00:27.525244000 +0100
+@@ -75,8 +75,8 @@
+         // copy constructor should be fine for primitive types.
+         inline type(const T& pt)
+         : t( pt ) {}
+-        inline type(const OFrvalue_storage& rhs)
+-        : t( rhs.pt ) {}
++        inline type(const type& rhs)
++        : t( rhs.t ) {}
+ 
+         // automatic conversion to the underlying type
+         inline operator T&() const { return OFconst_cast( T&, t ); }
+Only in dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd: ofutil.h~
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.9-visual-studio.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.9-visual-studio.patch
new file mode 100644
index 0000000..c0e9b21
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.9-visual-studio.patch
@@ -0,0 +1,1576 @@
+diff -urEb dcmtk-3.6.9.orig/config/math.cc dcmtk-3.6.9/config/math.cc
+--- dcmtk-3.6.9.orig/config/math.cc	2025-02-18 18:03:13.501406015 +0100
++++ dcmtk-3.6.9/config/math.cc	2025-02-18 18:05:52.950086576 +0100
+@@ -42,11 +42,17 @@
+ #include 
+ #endif
+ 
++#if defined(_MSC_VER)
++#include 
++#endif
++
+ struct dcmtk_config_math
+ {
+   static inline OFBool isnan( float f )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISNAN
++#if defined(_MSC_VER)
++    return _isnan(static_cast(f)) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISNAN)
+     return STD_NAMESPACE isnan(f);
+ #else
+     return ::isnan(f);
+@@ -55,7 +61,9 @@
+ 
+   static inline OFBool isnan( double d )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISNAN
++#if defined(_MSC_VER)
++    return _isnan(d) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISNAN)
+     return STD_NAMESPACE isnan(d);
+ #else
+     return ::isnan(d);
+@@ -64,7 +72,9 @@
+ 
+   static inline OFBool isinf( float f )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISINF
++#if defined(_MSC_VER)
++    return _finite(static_cast(f)) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISINF)
+     return STD_NAMESPACE isinf( f );
+ #else
+     return ::isinf( f );
+@@ -73,7 +83,9 @@
+ 
+   static inline OFBool isinf( double d )
+   {
+-#ifdef HAVE_PROTOTYPE_STD__ISINF
++#if defined(_MSC_VER)
++    return _finite(d) != 0;
++#elif defined(HAVE_PROTOTYPE_STD__ISINF)
+     return STD_NAMESPACE isinf( d );
+ #else
+     return ::isinf( d );
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jccoefct.c dcmtk-3.6.9/dcmjpeg/libijg12/jccoefct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jccoefct.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jccoefct.c	2025-02-18 18:05:52.950086576 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcdiffct.c dcmtk-3.6.9/dcmjpeg/libijg12/jcdiffct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcdiffct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jcdiffct.c	2025-02-18 18:05:52.951086562 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcpred.c dcmtk-3.6.9/dcmjpeg/libijg12/jcpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jcpred.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jcpred.c	2025-02-18 18:05:52.951086562 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jctrans.c dcmtk-3.6.9/dcmjpeg/libijg12/jctrans.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jctrans.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jctrans.c	2025-02-18 18:05:52.951086562 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdmerge.c dcmtk-3.6.9/dcmjpeg/libijg12/jdmerge.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdmerge.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdmerge.c	2025-02-18 18:05:52.951086562 +0100
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpostct.c dcmtk-3.6.9/dcmjpeg/libijg12/jdpostct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpostct.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdpostct.c	2025-02-18 18:05:52.952086549 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+ 
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpred.c dcmtk-3.6.9/dcmjpeg/libijg12/jdpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdpred.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdpred.c	2025-02-18 18:05:52.952086549 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -195,7 +195,7 @@
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+ 
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdsample.c dcmtk-3.6.9/dcmjpeg/libijg12/jdsample.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdsample.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdsample.c	2025-02-18 18:05:52.952086549 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdscale.c dcmtk-3.6.9/dcmjpeg/libijg12/jdscale.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jdscale.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jdscale.c	2025-02-18 18:05:52.953086535 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jerror.c dcmtk-3.6.9/dcmjpeg/libijg12/jerror.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jerror.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jerror.c	2025-02-18 18:05:52.953086535 +0100
+@@ -34,6 +34,10 @@
+ #define EXIT_FAILURE  1
+ #endif
+ 
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
++
+ 
+ /*
+  * Create the message string table.
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant1.c dcmtk-3.6.9/dcmjpeg/libijg12/jquant1.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant1.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jquant1.c	2025-02-18 18:05:52.953086535 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant2.c dcmtk-3.6.9/dcmjpeg/libijg12/jquant2.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg12/jquant2.c	2025-02-18 18:03:13.530405562 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg12/jquant2.c	2025-02-18 18:05:52.954086521 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jccoefct.c dcmtk-3.6.9/dcmjpeg/libijg16/jccoefct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jccoefct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jccoefct.c	2025-02-18 18:05:52.954086521 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcdiffct.c dcmtk-3.6.9/dcmjpeg/libijg16/jcdiffct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcdiffct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jcdiffct.c	2025-02-18 18:05:52.954086521 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcpred.c dcmtk-3.6.9/dcmjpeg/libijg16/jcpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jcpred.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jcpred.c	2025-02-18 18:05:52.954086521 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jctrans.c dcmtk-3.6.9/dcmjpeg/libijg16/jctrans.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jctrans.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jctrans.c	2025-02-18 18:05:52.954086521 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdmerge.c dcmtk-3.6.9/dcmjpeg/libijg16/jdmerge.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdmerge.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdmerge.c	2025-02-18 18:05:52.955086508 +0100
+@@ -169,7 +169,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -219,8 +219,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpostct.c dcmtk-3.6.9/dcmjpeg/libijg16/jdpostct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpostct.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdpostct.c	2025-02-18 18:05:52.955086508 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpred.c dcmtk-3.6.9/dcmjpeg/libijg16/jdpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdpred.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdpred.c	2025-02-18 18:05:52.955086508 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-   (void)cinfo;
+-  (void)comp_index;
++   //(void)cinfo;
++  //(void)comp_index;
+  UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4A);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -159,8 +159,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -172,8 +172,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5A);
+   JPEG_UNUSED(Rc);
+@@ -185,8 +185,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -198,8 +198,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6A);
+   JPEG_UNUSED(Rc);
+@@ -211,8 +211,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -224,8 +224,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7A);
+   JPEG_UNUSED(Rc);
+@@ -245,7 +245,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdsample.c dcmtk-3.6.9/dcmjpeg/libijg16/jdsample.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdsample.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdsample.c	2025-02-18 18:05:52.955086508 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdscale.c dcmtk-3.6.9/dcmjpeg/libijg16/jdscale.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jdscale.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jdscale.c	2025-02-18 18:05:52.955086508 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jerror.c dcmtk-3.6.9/dcmjpeg/libijg16/jerror.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jerror.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jerror.c	2025-02-18 18:05:52.955086508 +0100
+@@ -34,6 +34,10 @@
+ #define EXIT_FAILURE  1
+ #endif
+ 
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
++
+ 
+ /*
+  * Create the message string table.
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant1.c dcmtk-3.6.9/dcmjpeg/libijg16/jquant1.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant1.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jquant1.c	2025-02-18 18:05:52.956086494 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant2.c dcmtk-3.6.9/dcmjpeg/libijg16/jquant2.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg16/jquant2.c	2025-02-18 18:03:13.531405546 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg16/jquant2.c	2025-02-18 18:05:52.956086494 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jccoefct.c dcmtk-3.6.9/dcmjpeg/libijg8/jccoefct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jccoefct.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jccoefct.c	2025-02-18 18:05:52.956086494 +0100
+@@ -343,7 +343,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcdiffct.c dcmtk-3.6.9/dcmjpeg/libijg8/jcdiffct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcdiffct.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jcdiffct.c	2025-02-18 18:05:52.956086494 +0100
+@@ -302,7 +302,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossless_c_ptr losslsc = (j_lossless_c_ptr) cinfo->codec;
+   c_diff_ptr diff = (c_diff_ptr) losslsc->diff_private;
+   /* JDIMENSION MCU_col_num; */ /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcpred.c dcmtk-3.6.9/dcmjpeg/libijg8/jcpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jcpred.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jcpred.c	2025-02-18 18:05:52.957086481 +0100
+@@ -213,7 +213,7 @@
+          const JSAMPROW input_buf, JSAMPROW prev_row,
+          JDIFFROW diff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   DIFFERENCE_1D(INITIAL_PREDICTORx);
+ 
+   /*
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jctrans.c dcmtk-3.6.9/dcmjpeg/libijg8/jctrans.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jctrans.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jctrans.c	2025-02-18 18:05:52.957086481 +0100
+@@ -267,7 +267,7 @@
+ METHODDEF(boolean)
+ compress_output (j_compress_ptr cinfo, JSAMPIMAGE input_buf)
+ {
+-  (void)input_buf;
++  //(void)input_buf;
+   j_lossy_c_ptr lossyc = (j_lossy_c_ptr) cinfo->codec;
+   c_coef_ptr coef = (c_coef_ptr) lossyc->coef_private;
+   JDIMENSION MCU_col_num;   /* index of current MCU within row */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdmerge.c dcmtk-3.6.9/dcmjpeg/libijg8/jdmerge.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdmerge.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdmerge.c	2025-02-18 18:05:52.957086481 +0100
+@@ -148,7 +148,7 @@
+             JDIMENSION out_rows_avail)
+ /* 2:1 vertical sampling case: may need a spare row. */
+ {
+-  (void) in_row_groups_avail;
++  //(void) in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   JSAMPROW work_ptrs[2];
+   JDIMENSION num_rows;      /* number of rows returned to caller */
+@@ -198,8 +198,8 @@
+             JDIMENSION out_rows_avail)
+ /* 1:1 vertical sampling case: much easier, never need a spare row. */
+ {
+-  (void) in_row_groups_avail;
+-  (void) out_rows_avail;
++  //(void) in_row_groups_avail;
++  //(void) out_rows_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+ 
+   /* Just do the upsampling. */
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpostct.c dcmtk-3.6.9/dcmjpeg/libijg8/jdpostct.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpostct.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdpostct.c	2025-02-18 18:05:52.957086481 +0100
+@@ -161,8 +161,8 @@
+               JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+               JDIMENSION out_rows_avail)
+ {
+-  (void) output_buf;
+-  (void) out_rows_avail;
++  //(void) output_buf;
++  //(void) out_rows_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION old_next_row, num_rows;
+ 
+@@ -207,9 +207,9 @@
+             JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+             JDIMENSION out_rows_avail)
+ {
+-  (void) input_buf;
+-  (void) in_row_group_ctr;
+-  (void) in_row_groups_avail;
++  //(void) input_buf;
++  //(void) in_row_group_ctr;
++  //(void) in_row_groups_avail;
+   my_post_ptr post = (my_post_ptr) cinfo->post;
+   JDIMENSION num_rows, max_rows;
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpred.c dcmtk-3.6.9/dcmjpeg/libijg8/jdpred.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdpred.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdpred.c	2025-02-18 18:05:52.957086481 +0100
+@@ -101,8 +101,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_1D(INITIAL_PREDICTOR2);
+ }
+ 
+@@ -111,8 +111,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR2);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -123,8 +123,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR3);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -135,8 +135,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   UNDIFFERENCE_2D(PREDICTOR4);
+   JPEG_UNUSED(Rc);
+   JPEG_UNUSED(Rb);
+@@ -147,8 +147,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR5);
+   JPEG_UNUSED(Rc);
+@@ -160,8 +160,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR6);
+   JPEG_UNUSED(Rc);
+@@ -173,8 +173,8 @@
+            const JDIFFROW diff_buf, const JDIFFROW prev_row,
+            JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)cinfo;
+-  (void)comp_index;
++  //(void)cinfo;
++  //(void)comp_index;
+   SHIFT_TEMPS
+   UNDIFFERENCE_2D(PREDICTOR7);
+   JPEG_UNUSED(Rc);
+@@ -194,7 +194,7 @@
+                 const JDIFFROW diff_buf, JDIFFROW prev_row,
+                 JDIFFROW undiff_buf, JDIMENSION width)
+ {
+-  (void)prev_row;
++  //(void)prev_row;
+   j_lossless_d_ptr losslsd = (j_lossless_d_ptr) cinfo->codec;
+ 
+   UNDIFFERENCE_1D(INITIAL_PREDICTORx);
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdsample.c dcmtk-3.6.9/dcmjpeg/libijg8/jdsample.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdsample.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdsample.c	2025-02-18 18:05:52.957086481 +0100
+@@ -92,7 +92,7 @@
+           JSAMPARRAY output_buf, JDIMENSION *out_row_ctr,
+           JDIMENSION out_rows_avail)
+ {
+-  (void)in_row_groups_avail;
++  //(void)in_row_groups_avail;
+   my_upsample_ptr upsample = (my_upsample_ptr) cinfo->upsample;
+   int ci;
+   jpeg_component_info * compptr;
+@@ -158,8 +158,8 @@
+ fullsize_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
++  //(void)cinfo;
++  //(void)compptr;
+   *output_data_ptr = input_data;
+ }
+ 
+@@ -173,9 +173,9 @@
+ noop_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)cinfo;
+-  (void)compptr;
+-  (void)input_data;
++  //(void)cinfo;
++  //(void)compptr;
++  //(void)input_data;
+   *output_data_ptr = NULL;  /* safety check */
+ }
+ 
+@@ -239,7 +239,7 @@
+ h2v1_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+@@ -268,7 +268,7 @@
+ h2v2_upsample (j_decompress_ptr cinfo, jpeg_component_info * compptr,
+            JSAMPARRAY input_data, JSAMPARRAY * output_data_ptr)
+ {
+-  (void)compptr;
++  //(void)compptr;
+   JSAMPARRAY output_data = *output_data_ptr;
+   register JSAMPROW inptr, outptr;
+   register JSAMPLE invalue;
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdscale.c dcmtk-3.6.9/dcmjpeg/libijg8/jdscale.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jdscale.c	2025-02-18 18:03:13.532405530 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jdscale.c	2025-02-18 18:05:52.958086467 +0100
+@@ -67,7 +67,7 @@
+ 	const JDIFFROW diff_buf, JSAMPROW output_buf,
+ 	JDIMENSION width)
+ {
+-  (void)cinfo;
++  //(void)cinfo;
+   unsigned int xindex;
+ 
+   for (xindex = 0; xindex < width; xindex++)
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jerror.c dcmtk-3.6.9/dcmjpeg/libijg8/jerror.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jerror.c	2025-02-18 18:03:13.533405515 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jerror.c	2025-02-18 18:05:52.958086467 +0100
+@@ -34,6 +34,10 @@
+ #define EXIT_FAILURE  1
+ #endif
+ 
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
++
+ 
+ /*
+  * Create the message string table.
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant1.c dcmtk-3.6.9/dcmjpeg/libijg8/jquant1.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant1.c	2025-02-18 18:03:13.532405530 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jquant1.c	2025-02-18 18:05:52.958086467 +0100
+@@ -251,8 +251,8 @@
+    * (Forcing the upper and lower values to the limits ensures that
+    * dithering can't produce a color outside the selected gamut.)
+    */
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   return (int) (((IJG_INT32) j * MAXJSAMPLE + maxj/2) / maxj);
+ }
+ 
+@@ -262,8 +262,8 @@
+ /* Return largest input value that should map to j'th output value */
+ /* Must have largest(j=0) >= 0, and largest(j=maxj) >= MAXJSAMPLE */
+ {
+-  (void) cinfo;
+-  (void) ci;
++  //(void) cinfo;
++  //(void) ci;
+   /* Breakpoints are halfway between values returned by output_value */
+   return (int) (((IJG_INT32) (2*j + 1) * MAXJSAMPLE + maxj) / (2*maxj));
+ }
+@@ -744,7 +744,7 @@
+ METHODDEF(void)
+ start_pass_1_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+ {
+-  (void) is_pre_scan;
++  //(void) is_pre_scan;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   size_t arraysize;
+   int i;
+@@ -802,7 +802,7 @@
+ METHODDEF(void)
+ finish_pass_1_quant (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work in 1-pass case */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant2.c dcmtk-3.6.9/dcmjpeg/libijg8/jquant2.c
+--- dcmtk-3.6.9.orig/dcmjpeg/libijg8/jquant2.c	2025-02-18 18:03:13.532405530 +0100
++++ dcmtk-3.6.9/dcmjpeg/libijg8/jquant2.c	2025-02-18 18:05:52.958086467 +0100
+@@ -224,7 +224,7 @@
+ prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+           JSAMPARRAY output_buf, int num_rows)
+ {
+-  (void) output_buf;
++  //(void) output_buf;
+   my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+   register JSAMPROW ptr;
+   register histptr histp;
+@@ -1156,7 +1156,7 @@
+ METHODDEF(void)
+ finish_pass2 (j_decompress_ptr cinfo)
+ {
+-  (void) cinfo;
++  //(void) cinfo;
+   /* no work */
+ }
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_bcs.h dcmtk-3.6.9/oficonv/libsrc/citrus_bcs.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_bcs.h	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_bcs.h	2025-02-18 18:05:52.958086467 +0100
+@@ -39,7 +39,21 @@
+ #include 
+ #include 
+ #include 
++
++#undef EOPNOTSUPP
++#define EOPNOTSUPP 130   // https://learn.microsoft.com/fr-fr/cpp/c-runtime-library/errno-constants
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
++#if defined(_MSC_VER) && _MSC_VER < 1900
++#define snprintf _snprintf
++#endif
+ 
+ #define CITRUS_DECONST(type, var)    ((type)(uintptr_t)(const void *)(var))
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_big5.c dcmtk-3.6.9/oficonv/libsrc/citrus_big5.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_big5.c	2025-02-18 18:03:13.511405858 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_big5.c	2025-02-18 18:05:52.959086454 +0100
+@@ -218,13 +218,6 @@
+     return (0);
+ }
+ 
+-static const _citrus_prop_hint_t root_hints[] = {
+-    _CITRUS_PROP_HINT_NUM("row", &_citrus_BIG5_fill_rowcol),
+-    _CITRUS_PROP_HINT_NUM("col", &_citrus_BIG5_fill_rowcol),
+-    _CITRUS_PROP_HINT_NUM("excludes", &_citrus_BIG5_fill_excludes),
+-    _CITRUS_PROP_HINT_END
+-};
+-
+ static void
+ /*ARGSUSED*/
+ _citrus_BIG5_encoding_module_uninit(_BIG5EncodingInfo *ei)
+@@ -245,6 +238,18 @@
+     const char *s;
+     int err;
+ 
++    _citrus_prop_hint_t root_hints[4];
++    root_hints[0].name = "row";
++    root_hints[0].type = _CITRUS_PROP_NUM;
++    root_hints[0].cb.num.func = _citrus_BIG5_fill_rowcol;
++    root_hints[1].name = "col";
++    root_hints[1].type = _CITRUS_PROP_NUM;
++    root_hints[1].cb.num.func = _citrus_BIG5_fill_rowcol;
++    root_hints[2].name = "excludes";
++    root_hints[2].type = _CITRUS_PROP_NUM;
++    root_hints[2].cb.num.func = _citrus_BIG5_fill_excludes;
++    root_hints[3].name = NULL;  // _CITRUS_PROP_HINT_END
++
+     memset((void *)ei, 0, sizeof(*ei));
+     TAILQ_INIT(&ei->excludes);
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_db_hash.h dcmtk-3.6.9/oficonv/libsrc/citrus_db_hash.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_db_hash.h	2025-02-18 18:03:13.510405874 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_db_hash.h	2025-02-18 18:05:52.959086454 +0100
+@@ -29,7 +29,8 @@
+ 
+ #include "dcmtk/config/osconfig.h"
+ #include "dcmtk/oficonv/oidefine.h"
+-#include 
++
++#include 
+ 
+ struct _citrus_region;
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.c dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.c	2025-02-18 18:03:13.511405858 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.c	2025-02-18 18:05:52.959086454 +0100
+@@ -34,7 +34,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.h dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_esdb.h	2025-02-18 18:03:13.512405843 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_esdb.h	2025-02-18 18:05:52.959086454 +0100
+@@ -29,7 +29,14 @@
+ 
+ #include "dcmtk/config/osconfig.h"
+ #include "citrus_types.h"
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
+ 
+ struct _citrus_esdb_charset {
+     _citrus_csid_t           ec_csid;
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_gbk2k.c dcmtk-3.6.9/oficonv/libsrc/citrus_gbk2k.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_gbk2k.c	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_gbk2k.c	2025-02-18 18:05:52.959086454 +0100
+@@ -34,7 +34,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_hz.c dcmtk-3.6.9/oficonv/libsrc/citrus_hz.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_hz.c	2025-02-18 18:03:13.518405749 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_hz.c	2025-02-18 18:05:52.959086454 +0100
+@@ -571,13 +571,6 @@
+     return (0);
+ }
+ 
+-static const _citrus_prop_hint_t escape_hints[] = {
+-_CITRUS_PROP_HINT_STR("CH", &_citrus_HZ_parse_char),
+-_CITRUS_PROP_HINT_STR("GL", &_citrus_HZ_parse_graphic),
+-_CITRUS_PROP_HINT_STR("GR", &_citrus_HZ_parse_graphic),
+-_CITRUS_PROP_HINT_END
+-};
+-
+ static int
+ _citrus_HZ_parse_escape(void *context, const char *name, const char *s)
+ {
+@@ -585,6 +578,18 @@
+     escape_t *escape;
+     void *p[2];
+ 
++    _citrus_prop_hint_t escape_hints[4];
++    escape_hints[0].name = "CH";
++    escape_hints[0].type = _CITRUS_PROP_STR;
++    escape_hints[0].cb.str.func = _citrus_HZ_parse_char;
++    escape_hints[1].name = "GL";
++    escape_hints[1].type = _CITRUS_PROP_STR;
++    escape_hints[1].cb.str.func = _citrus_HZ_parse_graphic;
++    escape_hints[2].name = "GR";
++    escape_hints[2].type = _CITRUS_PROP_STR;
++    escape_hints[2].cb.str.func = _citrus_HZ_parse_graphic;
++    escape_hints[3].name = NULL;  // _CITRUS_PROP_HINT_END
++
+     ei = (_HZEncodingInfo *)context;
+     escape = calloc(1, sizeof(*escape));
+     if (escape == NULL)
+@@ -605,18 +610,21 @@
+         escape_hints, (void *)&p[0], s, strlen(s)));
+ }
+ 
+-static const _citrus_prop_hint_t root_hints[] = {
+-_CITRUS_PROP_HINT_STR("0", &_citrus_HZ_parse_escape),
+-_CITRUS_PROP_HINT_STR("1", &_citrus_HZ_parse_escape),
+-_CITRUS_PROP_HINT_END
+-};
+-
+ static int
+ _citrus_HZ_encoding_module_init(_HZEncodingInfo * ei,
+     const void * var, size_t lenvar)
+ {
+     int errnum;
+ 
++    _citrus_prop_hint_t root_hints[3];
++    root_hints[0].name = "0";
++    root_hints[0].type = _CITRUS_PROP_STR;
++    root_hints[0].cb.str.func = _citrus_HZ_parse_escape;
++    root_hints[1].name = "1";
++    root_hints[1].type = _CITRUS_PROP_STR;
++    root_hints[1].cb.str.func = _citrus_HZ_parse_escape;
++    root_hints[2].name = NULL;  // _CITRUS_PROP_HINT_END
++
+     memset(ei, 0, sizeof(*ei));
+     TAILQ_INIT(E0SET(ei));
+     TAILQ_INIT(E1SET(ei));
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_local.h dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_local.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_local.h	2025-02-18 18:03:13.520405718 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_local.h	2025-02-18 18:05:52.960086440 +0100
+@@ -29,7 +29,15 @@
+ 
+ #include "dcmtk/config/osconfig.h"
+ #include "dcmtk/oficonv/iconv.h"
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ 
+ #ifdef HAVE_SYS_QUEUE_H
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_none.c dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_none.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_none.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_none.c	2025-02-18 18:05:52.960086440 +0100
+@@ -35,7 +35,15 @@
+ 
+ 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_std.c dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_std.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv_std.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv_std.c	2025-02-18 18:05:52.960086440 +0100
+@@ -36,7 +36,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iso2022.c dcmtk-3.6.9/oficonv/libsrc/citrus_iso2022.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iso2022.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iso2022.c	2025-02-18 18:05:52.960086440 +0100
+@@ -35,7 +35,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_johab.c dcmtk-3.6.9/oficonv/libsrc/citrus_johab.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_johab.c	2025-02-18 18:03:13.518405749 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_johab.c	2025-02-18 18:05:52.961086427 +0100
+@@ -34,7 +34,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_module.c dcmtk-3.6.9/oficonv/libsrc/citrus_module.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_module.c	2025-02-18 18:03:13.520405718 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_module.c	2025-02-18 18:05:52.961086427 +0100
+@@ -98,7 +98,15 @@
+ #endif
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mskanji.c dcmtk-3.6.9/oficonv/libsrc/citrus_mskanji.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mskanji.c	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_mskanji.c	2025-02-18 18:05:52.961086427 +0100
+@@ -67,7 +67,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_prop.c dcmtk-3.6.9/oficonv/libsrc/citrus_prop.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_prop.c	2025-02-18 18:03:13.514405812 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_prop.c	2025-02-18 18:05:52.961086427 +0100
+@@ -30,7 +30,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_region.h dcmtk-3.6.9/oficonv/libsrc/citrus_region.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_region.h	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_region.h	2025-02-18 18:05:52.961086427 +0100
+@@ -31,7 +31,14 @@
+ #include "dcmtk/config/osconfig.h"
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
+ 
+ #ifdef HAVE_SYS_TYPES_H
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_utf8.c dcmtk-3.6.9/oficonv/libsrc/citrus_utf8.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_utf8.c	2025-02-18 18:03:13.519405733 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_utf8.c	2025-02-18 18:05:52.961086427 +0100
+@@ -66,7 +66,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/oficonv_iconv.c dcmtk-3.6.9/oficonv/libsrc/oficonv_iconv.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/oficonv_iconv.c	2025-02-18 18:03:13.512405843 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/oficonv_iconv.c	2025-02-18 18:05:52.962086413 +0100
+@@ -40,7 +40,15 @@
+ 
+ #include 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/windows_mmap.h dcmtk-3.6.9/oficonv/libsrc/windows_mmap.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/windows_mmap.h	2025-02-18 18:03:13.511405858 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/windows_mmap.h	2025-02-18 18:05:52.962086413 +0100
+@@ -74,6 +74,12 @@
+ 
+ static void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
+ {
++  DWORD flProtect;
++  DWORD dwDesiredAccess;
++  HANDLE mmap_fd, h;
++  off_t end;
++  void *ret;
++
+     (void) start;
+ 
+ 	if (prot & ~(PROT_READ | PROT_WRITE | PROT_EXEC))
+@@ -84,7 +90,6 @@
+ 	} else if (flags & MAP_ANON)
+ 		return MAP_FAILED;
+ 
+-	DWORD flProtect;
+     flProtect = 0;
+ 	if (prot & PROT_WRITE) {
+ 		if (prot & PROT_EXEC)
+@@ -99,8 +104,7 @@
+ 	} else
+ 		flProtect = PAGE_READONLY;
+ 
+-	off_t end = (off_t)(length + offset);
+-	HANDLE mmap_fd, h;
++	end = (off_t)(length + offset);
+ 	if (fd == -1)
+ 		mmap_fd = INVALID_HANDLE_VALUE;
+ 	else
+@@ -109,7 +113,6 @@
+ 	if (h == NULL)
+ 		return MAP_FAILED;
+ 
+-	DWORD dwDesiredAccess;
+ 	if (prot & PROT_WRITE)
+ 		dwDesiredAccess = FILE_MAP_WRITE;
+ 	else
+@@ -118,7 +121,7 @@
+ 		dwDesiredAccess |= FILE_MAP_EXECUTE;
+ 	if (flags & MAP_PRIVATE)
+ 		dwDesiredAccess |= FILE_MAP_COPY;
+-	void *ret = MapViewOfFile(h, dwDesiredAccess, MM_DWORD_HI(offset), MM_DWORD_LO(offset), length);
++	ret = MapViewOfFile(h, dwDesiredAccess, MM_DWORD_HI(offset), MM_DWORD_LO(offset), length);
+ 	if (ret == NULL) {
+ 		CloseHandle(h);
+ 		ret = MAP_FAILED;
+@@ -140,11 +143,13 @@
+ 
+ static void munmap(void *addr, size_t length)
+ {
++  mmap_cleanup_t **prevPtr;
++  mmap_cleanup_t *mc;
++
+     (void) length;
+ 	UnmapViewOfFile(addr);
+ 	// Look up through the tracking elements to close the handle
+-	mmap_cleanup_t **prevPtr = &mmap_cleanup;
+-	mmap_cleanup_t *mc;
++	prevPtr = &mmap_cleanup;
+ 	for (mc = *prevPtr; mc != NULL; prevPtr = &mc->next, mc = *prevPtr)
+ 	{
+ 		if (mc->addr == addr)
+diff -urEb dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/oftypes.h dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/oftypes.h
+--- dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/oftypes.h	2025-02-18 18:03:13.523405671 +0100
++++ dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/oftypes.h	2025-02-18 18:05:52.962086413 +0100
+@@ -79,10 +79,9 @@
+ 
+ #include 
+ BEGIN_EXTERN_C
+-#ifdef HAVE_STDINT_H
++#if defined(HAVE_STDINT_H) || _MSC_VER >= 1600
+ #include 
+ #endif
+-#include 
+ END_EXTERN_C
+ 
+ #include "dcmtk/ofstd/ofstream.h"
diff --git a/OrthancFramework/Resources/Patches/dcmtk-3.6.9.patch b/OrthancFramework/Resources/Patches/dcmtk-3.6.9.patch
new file mode 100644
index 0000000..8dff08a
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.9.patch
@@ -0,0 +1,296 @@
+diff -urEb dcmtk-3.6.9.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.9/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.9.orig/CMake/GenerateDCMTKConfigure.cmake	2025-02-18 18:03:13.505405952 +0100
++++ dcmtk-3.6.9/CMake/GenerateDCMTKConfigure.cmake	2025-02-18 18:06:53.925278621 +0100
+@@ -227,12 +227,15 @@
+ 
+ # Check the sizes of various types
+ include (CheckTypeSize)
++if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
++  # This doesn't work for wasm, Orthanc defines the macros manually
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+ CHECK_TYPE_SIZE("float" SIZEOF_FLOAT)
+ CHECK_TYPE_SIZE("int" SIZEOF_INT)
+ CHECK_TYPE_SIZE("long" SIZEOF_LONG)
+ CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
+ CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
++endif()
+ 
+ # Check for include files, libraries, and functions
+ include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+diff -urEb dcmtk-3.6.9.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.9/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.9.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2025-02-18 18:03:13.497406077 +0100
++++ dcmtk-3.6.9/dcmdata/include/dcmtk/dcmdata/dcdict.h	2025-02-18 18:06:53.925278621 +0100
+@@ -163,6 +163,12 @@
+     /// returns an iterator to the end of the repeating groups data dictionary
+     DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
+ 
++    // Function by the Orthanc project to load a dictionary from a
++    // memory buffer, which is necessary in sandboxed
++    // environments. This is an adapted version of
++    // DcmDataDictionary::loadDictionary().
++    OFBool loadFromMemory(const std::string& content, OFBool errorIfAbsent = OFTrue);
++    
+ private:
+ 
+     /** private undefined assignment operator
+diff -urEb dcmtk-3.6.9.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.9/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.9.orig/dcmdata/libsrc/dcdict.cc	2025-02-18 18:03:13.499406046 +0100
++++ dcmtk-3.6.9/dcmdata/libsrc/dcdict.cc	2025-02-18 18:06:53.926278608 +0100
+@@ -904,3 +904,5 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.9.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.9/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.9.orig/dcmdata/libsrc/dcpxitem.cc	2025-02-18 18:03:13.497406077 +0100
++++ dcmtk-3.6.9/dcmdata/libsrc/dcpxitem.cc	2025-02-18 18:06:53.926278608 +0100
+@@ -31,6 +31,8 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.9.orig/dcmnet/libsrc/scu.cc dcmtk-3.6.9/dcmnet/libsrc/scu.cc
+--- dcmtk-3.6.9.orig/dcmnet/libsrc/scu.cc	2025-02-18 18:03:13.525405640 +0100
++++ dcmtk-3.6.9/dcmnet/libsrc/scu.cc	2025-02-18 18:06:53.927278595 +0100
+@@ -19,6 +19,11 @@
+  *
+  */
+ 
++#if defined(_WIN32)
++#  define __STDC_LIMIT_MACROS   // Get access to UINT16_MAX
++#  include 
++#endif
++
+ #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */
+ 
+ #include "dcmtk/dcmdata/dcostrmf.h" /* for class DcmOutputFileStream */
+diff -urEb dcmtk-3.6.9.orig/oficonv/include/dcmtk/oficonv/iconv.h dcmtk-3.6.9/oficonv/include/dcmtk/oficonv/iconv.h
+--- dcmtk-3.6.9.orig/oficonv/include/dcmtk/oficonv/iconv.h	2025-02-18 18:03:13.510405874 +0100
++++ dcmtk-3.6.9/oficonv/include/dcmtk/oficonv/iconv.h	2025-02-18 18:06:53.927278595 +0100
+@@ -55,7 +55,12 @@
+ #endif
+ 
+ struct __tag_iconv_t;
++
++#if defined(__LSB_VERSION__)
++typedef void *iconv_t;
++#else
+ typedef struct __tag_iconv_t *iconv_t;
++#endif
+ 
+ #ifndef OFICONV_CITRUS_WC_T_DEFINED
+ #define OFICONV_CITRUS_WC_T_DEFINED
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_csmapper.c dcmtk-3.6.9/oficonv/libsrc/citrus_csmapper.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_csmapper.c	2025-02-18 18:03:13.510405874 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_csmapper.c	2025-02-18 18:06:53.927278595 +0100
+@@ -63,7 +63,8 @@
+ 
+ #ifdef WITH_THREADS
+ #ifdef HAVE_WINDOWS_H
+-static SRWLOCK ma_lock = SRWLOCK_INIT;
++static int ma_lock_initialized = 0;
++static CRITICAL_SECTION ma_lock;
+ #elif defined(HAVE_PTHREAD_H)
+ static pthread_rwlock_t ma_lock = PTHREAD_RWLOCK_INITIALIZER;
+ #endif
+@@ -382,6 +383,14 @@
+     char mapper_path[OFICONV_PATH_MAX];
+     unsigned long norm;
+     int ret;
++
++#if defined(WITH_THREADS) && defined(HAVE_WINDOWS_H)
++    if (ma_lock_initialized == 0) { /* Very minor risk of race condition here */
++      InitializeCriticalSection(&ma_lock);
++      ma_lock_initialized = 1;
++    }
++#endif
++
+     norm = 0;
+ 
+     getCSMapperPath(mapper_path, sizeof(mapper_path), NULL);
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv.c dcmtk-3.6.9/oficonv/libsrc/citrus_iconv.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_iconv.c	2025-02-18 18:03:13.520405718 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_iconv.c	2025-02-18 18:10:35.928614598 +0100
+@@ -49,7 +49,15 @@
+ #endif
+ 
+ #include 
++
++#if (_MSC_VER >= 1900)
+ #include 
++#else
++#define bool int
++#define false 0
++#define true 1
++#endif
++
+ #include 
+ #include 
+ #include 
+@@ -80,7 +88,8 @@
+ 
+ #ifdef WITH_THREADS
+ #ifdef HAVE_WINDOWS_H
+-static SRWLOCK ci_lock = SRWLOCK_INIT;
++static int ci_lock_initialized = 0;
++static CRITICAL_SECTION ci_lock;
+ #elif defined(HAVE_PTHREAD_H)
+ static pthread_rwlock_t ci_lock = PTHREAD_RWLOCK_INITIALIZER;
+ #endif
+@@ -299,14 +308,24 @@
+ _citrus_iconv_open(struct _citrus_iconv * * rcv,
+     const char * src, const char * dst)
+ {
+-struct _citrus_iconv *cv = NULL;
++#ifdef HAVE_WINDOWS_H
++    char current_codepage[20];
++#endif
++
++    struct _citrus_iconv *cv = NULL;
+     struct _citrus_iconv_shared *ci = NULL;
+     char realdst[OFICONV_PATH_MAX], realsrc[OFICONV_PATH_MAX];
+     int ret;
+ 
++#if defined(WITH_THREADS) && defined(HAVE_WINDOWS_H)
++    if (ci_lock_initialized == 0) { /* Very minor risk of race condition here */
++      InitializeCriticalSection(&ci_lock);
++      ci_lock_initialized = 1;
++    }
++#endif
++
+     init_cache();
+ #ifdef HAVE_WINDOWS_H
+-    char current_codepage[20];
+     snprintf(current_codepage, sizeof(current_codepage), "%lu", (unsigned long) GetConsoleOutputCP());
+ #endif
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_lock.h dcmtk-3.6.9/oficonv/libsrc/citrus_lock.h
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_lock.h	2025-02-18 18:03:13.518405749 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_lock.h	2025-02-18 18:06:53.927278595 +0100
+@@ -31,11 +31,11 @@
+ 
+ #ifdef WITH_THREADS
+ 
+-#ifdef HAVE_WINDOWS_H
++#if defined(HAVE_WINDOWS_H)
+ 
+ #include 
+-#define WLOCK(lock)  AcquireSRWLockExclusive(lock);
+-#define UNLOCK(lock) ReleaseSRWLockExclusive(lock);
++#define WLOCK(lock)  EnterCriticalSection(lock);
++#define UNLOCK(lock) LeaveCriticalSection(lock);
+ 
+ #else /* HAVE_WINDOWS_H */
+ 
+diff -urEb dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mapper.c dcmtk-3.6.9/oficonv/libsrc/citrus_mapper.c
+--- dcmtk-3.6.9.orig/oficonv/libsrc/citrus_mapper.c	2025-02-18 18:03:13.516405780 +0100
++++ dcmtk-3.6.9/oficonv/libsrc/citrus_mapper.c	2025-02-18 18:06:53.928278582 +0100
+@@ -64,7 +64,8 @@
+ 
+ #ifdef WITH_THREADS
+ #ifdef HAVE_WINDOWS_H
+-static SRWLOCK cm_lock = SRWLOCK_INIT;
++static int cm_lock_initialized = 0;
++static CRITICAL_SECTION cm_lock;
+ #elif defined(HAVE_PTHREAD_H)
+ static pthread_rwlock_t cm_lock = PTHREAD_RWLOCK_INITIALIZER;
+ #endif
+@@ -355,6 +356,13 @@
+     const char *module, *variable;
+     int hashval, ret;
+ 
++#if defined(WITH_THREADS) && defined(HAVE_WINDOWS_H)
++    if (cm_lock_initialized == 0) { /* Very minor risk of race condition here */
++      InitializeCriticalSection(&cm_lock);
++      cm_lock_initialized = 1;
++    }
++#endif
++
+     variable = NULL;
+ 
+     WLOCK(&cm_lock);
+diff -urEb dcmtk-3.6.9.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.9/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.9.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2025-02-18 18:03:13.473406452 +0100
++++ dcmtk-3.6.9/oflog/include/dcmtk/oflog/thread/syncpub.h	2025-02-18 18:06:53.928278582 +0100
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t) + 0))
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial) + 0))
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig) + 0))
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex + 0))
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.9.orig/oflog/libsrc/oflog.cc dcmtk-3.6.9/oflog/libsrc/oflog.cc
+--- dcmtk-3.6.9.orig/oflog/libsrc/oflog.cc	2025-02-18 18:03:13.475406421 +0100
++++ dcmtk-3.6.9/oflog/libsrc/oflog.cc	2025-02-18 18:06:53.928278582 +0100
+@@ -19,6 +19,11 @@
+  *
+  */
+ 
++
++#if defined(_WIN32)
++#  include 
++#endif
++
+ #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+ #include "dcmtk/oflog/oflog.h"
+ 
+diff -urEb dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.9.orig/ofstd/include/dcmtk/ofstd/offile.h	2025-02-18 18:03:13.523405671 +0100
++++ dcmtk-3.6.9/ofstd/include/dcmtk/ofstd/offile.h	2025-02-18 18:06:53.929278570 +0100
+@@ -569,7 +569,7 @@
+    */
+   void setlinebuf()
+   {
+-#if defined(_WIN32) || defined(__hpux)
++#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
+     this->setvbuf(NULL, _IOLBF, 0);
+ #else
+     :: setlinebuf(file_);
+diff -urEb dcmtk-3.6.9.orig/ofstd/libsrc/ofstub.cc dcmtk-3.6.9/ofstd/libsrc/ofstub.cc
+--- dcmtk-3.6.9.orig/ofstd/libsrc/ofstub.cc	2025-02-18 18:03:13.523405671 +0100
++++ dcmtk-3.6.9/ofstd/libsrc/ofstub.cc	2025-02-18 18:06:53.929278570 +0100
+@@ -35,6 +35,10 @@
+ #include 
+ #endif /* HAVE_WINDOWS_H */
+ 
++#if defined(__LSB_VERSION__)
++#include 
++#endif
++
+ #define EXITCODE_CANNOT_DETERMINE_DIR        90
+ #define EXITCODE_EXEC_FAILED                 91
+ #define EXITCODE_ILLEGAL_PARAMS              92
diff --git a/OrthancFramework/Resources/Patches/dcmtk-dcdict_orthanc.cc b/OrthancFramework/Resources/Patches/dcmtk-dcdict_orthanc.cc
new file mode 100644
index 0000000..b4bf8c2
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk-dcdict_orthanc.cc
@@ -0,0 +1,205 @@
+// Function by the Orthanc project to load a dictionary from a memory
+// buffer, which is necessary in sandboxed environments. This is an
+// adapted version of DcmDataDictionary::loadDictionary().
+
+#include 
+#include 
+
+struct OrthancLinesIterator;
+
+// This plain old C class is implemented in "../../Core/Toolbox.h"
+OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content);
+
+bool OrthancLinesIterator_GetLine(std::string& target,
+                                  const OrthancLinesIterator* iterator);
+
+void OrthancLinesIterator_Next(OrthancLinesIterator* iterator);
+
+void OrthancLinesIterator_Free(OrthancLinesIterator* iterator);
+
+
+class LinesIterator : public boost::noncopyable
+{
+private:
+  OrthancLinesIterator* iterator_;
+  
+public:
+  LinesIterator(const std::string& content) :
+    iterator_(NULL)
+  {
+    iterator_ = OrthancLinesIterator_Create(content);
+  }
+
+  ~LinesIterator()
+  {
+    if (iterator_ != NULL)
+    {
+      OrthancLinesIterator_Free(iterator_);
+      iterator_ = NULL;
+    }
+  }
+  
+  bool GetLine(std::string& target) const
+  {
+    if (iterator_ != NULL)
+    {
+      return OrthancLinesIterator_GetLine(target, iterator_);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  void Next()
+  {
+    if (iterator_ != NULL)
+    {
+      OrthancLinesIterator_Next(iterator_);
+    }
+  }
+};
+
+
+
+OFBool
+DcmDataDictionary::loadFromMemory(const std::string& content, OFBool errorIfAbsent)
+{
+  int lineNumber = 0;
+  char* lineFields[DCM_MAXDICTFIELDS + 1];
+  int fieldsPresent;
+  DcmDictEntry* e;
+  int errorsEncountered = 0;
+  OFBool errorOnThisLine = OFFalse;
+  int i;
+
+  DcmTagKey key, upperKey;
+  DcmDictRangeRestriction groupRestriction = DcmDictRange_Unspecified;
+  DcmDictRangeRestriction elementRestriction = DcmDictRange_Unspecified;
+  DcmVR vr;
+  char* vrName;
+  char* tagName;
+  char* privCreator;
+  int vmMin, vmMax = 1;
+  const char* standardVersion;
+
+  LinesIterator iterator(content);
+
+  std::string line;
+  while (iterator.GetLine(line)) {
+    iterator.Next();
+
+    if (line.size() >= DCM_MAXDICTLINESIZE) {
+      DCMDATA_ERROR("DcmDataDictionary: Too long line: " << line);
+      continue;
+    }
+
+    lineNumber++;
+
+    if (onlyWhitespace(line.c_str())) {
+      continue; /* ignore this line */
+    }
+    if (isaCommentLine(line.c_str())) {
+      continue; /* ignore this line */
+    }
+
+    errorOnThisLine = OFFalse;
+
+    /* fields are tab separated */
+    fieldsPresent = splitFields(line.c_str(), lineFields,
+                                DCM_MAXDICTFIELDS,
+                                DCM_DICT_FIELD_SEPARATOR_CHAR);
+
+    /* initialize dict entry fields */
+    vrName = NULL;
+    tagName = NULL;
+    privCreator = NULL;
+    vmMin = vmMax = 1;
+    standardVersion = "DICOM";
+
+    switch (fieldsPresent) {
+      case 0:
+      case 1:
+      case 2:
+        DCMDATA_ERROR("DcmDataDictionary: "
+                      << "too few fields (line " << lineNumber << ")");
+        errorOnThisLine = OFTrue;
+        break;
+      default:
+        DCMDATA_ERROR("DcmDataDictionary: "
+                      << "too many fields (line " << lineNumber << "): ");
+        errorOnThisLine = OFTrue;
+        break;
+      case 5:
+        stripWhitespace(lineFields[4]);
+        standardVersion = lineFields[4];
+        /* drop through to next case label */
+      case 4:
+        /* the VM field is present */
+        if (!parseVMField(lineFields[3], vmMin, vmMax)) {
+          DCMDATA_ERROR("DcmDataDictionary: "
+                        << "bad VM field (line " << lineNumber << "): " << lineFields[3]);
+          errorOnThisLine = OFTrue;
+        }
+        /* drop through to next case label */
+      case 3:
+        if (!parseWholeTagField(lineFields[0], key, upperKey,
+                                groupRestriction, elementRestriction, privCreator))
+        {
+          DCMDATA_ERROR("DcmDataDictionary: "
+                        << "bad Tag field (line " << lineNumber << "): " << lineFields[0]);
+          errorOnThisLine = OFTrue;
+        } else {
+          /* all is OK */
+          vrName = lineFields[1];
+          stripWhitespace(vrName);
+
+          tagName = lineFields[2];
+          stripWhitespace(tagName);
+        }
+    }
+
+    if (!errorOnThisLine) {
+      /* check the VR Field */
+      vr.setVR(vrName);
+      if (vr.getEVR() == EVR_UNKNOWN) {
+        DCMDATA_ERROR("DcmDataDictionary: "
+                      << "bad VR field (line " << lineNumber << "): " << vrName);
+        errorOnThisLine = OFTrue;
+      }
+    }
+
+    if (!errorOnThisLine) {
+      e = new DcmDictEntry(
+        key.getGroup(), key.getElement(),
+        upperKey.getGroup(), upperKey.getElement(),
+        vr, tagName, vmMin, vmMax, standardVersion, OFTrue,
+        privCreator);
+
+      e->setGroupRangeRestriction(groupRestriction);
+      e->setElementRangeRestriction(elementRestriction);
+      addEntry(e);
+    }
+
+    for (i = 0; i < fieldsPresent; i++) {
+      free(lineFields[i]);
+      lineFields[i] = NULL;
+    }
+
+    delete[] privCreator;
+
+    if (errorOnThisLine) {
+      errorsEncountered++;
+    }
+  }
+
+  /* return OFFalse in case of errors and set internal state accordingly */
+  if (errorsEncountered == 0) {
+    dictionaryLoaded = OFTrue;
+    return OFTrue;
+  }
+  else {
+    dictionaryLoaded = OFFalse;
+    return OFFalse;
+  }
+}
diff --git a/OrthancFramework/Resources/Patches/dcmtk.txt b/OrthancFramework/Resources/Patches/dcmtk.txt
new file mode 100644
index 0000000..5bd81dd
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/dcmtk.txt
@@ -0,0 +1,15 @@
+Generate some patch
+===================
+
+diff -urEb dcmtk-3.6.0.orig/ dcmtk-3.6.0
+diff -urEb dcmtk-3.6.2.orig/ dcmtk-3.6.2
+diff -urEb dcmtk-3.6.4.orig/ dcmtk-3.6.4
+diff -urEb dcmtk-3.6.5.orig/ dcmtk-3.6.5
+diff -urEb dcmtk-3.6.6.orig/ dcmtk-3.6.6
+diff -urEb dcmtk-3.6.7.orig/ dcmtk-3.6.7
+
+
+For "dcmtk-3.6.2-private.dic" (only used with DCMTK 3.6.0)
+=============================
+
+# cp ../../ThirdPartyDownloads/dcmtk-3.6.2/dcmdata/data/private.dic dcmtk-3.6.2-private.dic
diff --git a/OrthancFramework/Resources/Patches/e2fsprogs-1.43.8-apple.patch b/OrthancFramework/Resources/Patches/e2fsprogs-1.43.8-apple.patch
new file mode 100644
index 0000000..b38e3c1
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/e2fsprogs-1.43.8-apple.patch
@@ -0,0 +1,24 @@
+diff -urEb e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in e2fsprogs-1.43.8/lib/uuid/uuid.h.in
+--- e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in	2018-01-02 05:52:58.000000000 +0100
++++ e2fsprogs-1.43.8/lib/uuid/uuid.h.in	2018-11-05 12:18:29.962235770 +0100
+@@ -35,6 +35,20 @@
+ #ifndef _UUID_UUID_H
+ #define _UUID_UUID_H
+ 
++
++#if defined(__APPLE__)
++// This patch defines the "uuid_string_t" type on OS X, which is
++// required if linking against Cocoa (this occurs in Stone of Orthanc)
++#include 
++#include 
++
++#ifndef _UUID_STRING_T
++#define _UUID_STRING_T
++typedef __darwin_uuid_string_t  uuid_string_t;
++#endif /* _UUID_STRING_T */
++#endif
++
++
+ #include 
+ #ifndef _WIN32
+ #include 
diff --git a/OrthancFramework/Resources/Patches/e2fsprogs-1.44.5.patch b/OrthancFramework/Resources/Patches/e2fsprogs-1.44.5.patch
new file mode 100644
index 0000000..724c2d1
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/e2fsprogs-1.44.5.patch
@@ -0,0 +1,42 @@
+diff -urEb e2fsprogs-1.44.5.orig/lib/uuid/gen_uuid.c e2fsprogs-1.44.5/lib/uuid/gen_uuid.c
+--- e2fsprogs-1.44.5.orig/lib/uuid/gen_uuid.c	2020-11-24 15:47:40.950897761 +0100
++++ e2fsprogs-1.44.5/lib/uuid/gen_uuid.c	2020-11-24 15:48:51.234732050 +0100
+@@ -147,12 +147,14 @@
+ 		fd = open("/dev/urandom", O_RDONLY);
+ 		if (fd == -1)
+ 			fd = open("/dev/random", O_RDONLY | O_NONBLOCK);
++#if !defined(__EMSCRIPTEN__)  // By SJO for Stone
+ 		if (fd >= 0) {
+ 			i = fcntl(fd, F_GETFD);
+ 			if (i >= 0)
+ 				fcntl(fd, F_SETFD, i | FD_CLOEXEC);
+ 		}
+ #endif
++#endif
+ 		srand(((unsigned)getpid() << 16) ^ getuid() ^ tv.tv_sec ^ tv.tv_usec);
+ #ifdef DO_JRAND_MIX
+ 		jrand_seed[0] = getpid() ^ (tv.tv_sec & 0xFFFF);
+diff -urEb e2fsprogs-1.44.5.orig/lib/uuid/uuid.h.in e2fsprogs-1.44.5/lib/uuid/uuid.h.in
+--- e2fsprogs-1.44.5.orig/lib/uuid/uuid.h.in	2020-11-24 15:47:40.950897761 +0100
++++ e2fsprogs-1.44.5/lib/uuid/uuid.h.in	2020-11-24 15:48:00.946849227 +0100
+@@ -35,6 +35,20 @@
+ #ifndef _UUID_UUID_H
+ #define _UUID_UUID_H
+ 
++
++#if defined(__APPLE__)
++// This patch defines the "uuid_string_t" type on OS X, which is
++// required if linking against Cocoa (this occurs in Stone of Orthanc)
++#include 
++#include 
++
++#ifndef _UUID_STRING_T
++#define _UUID_STRING_T
++typedef __darwin_uuid_string_t  uuid_string_t;
++#endif /* _UUID_STRING_T */
++#endif
++
++
+ #include 
+ #ifndef _WIN32
+ #include 
diff --git a/OrthancFramework/Resources/Patches/libp11-0.4.0.patch b/OrthancFramework/Resources/Patches/libp11-0.4.0.patch
new file mode 100644
index 0000000..c3cc666
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/libp11-0.4.0.patch
@@ -0,0 +1,68 @@
+diff -urEb libp11-0.4.0.orig/src/atfork.c libp11-0.4.0/src/atfork.c
+--- libp11-0.4.0.orig/src/atfork.c	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/atfork.c	2020-04-02 17:04:10.152619121 +0200
+@@ -25,7 +25,7 @@
+ #include 
+ #include 
+ #include 
+-#include 
++#include "atfork.h"
+ 
+ #ifdef __sun
+ # pragma fini(lib_deinit)
+diff -urEb libp11-0.4.0.orig/src/engine.h libp11-0.4.0/src/engine.h
+--- libp11-0.4.0.orig/src/engine.h	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/engine.h	2020-04-02 17:04:10.152619121 +0200
+@@ -29,7 +29,7 @@
+ #define _ENGINE_PKCS11_H
+ 
+ #ifndef _WIN32
+-#include "config.h"
++//#include "config.h"
+ #endif
+ 
+ #include "libp11.h"
+diff -urEb libp11-0.4.0.orig/src/libp11-int.h libp11-0.4.0/src/libp11-int.h
+--- libp11-0.4.0.orig/src/libp11-int.h	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/libp11-int.h	2020-04-02 17:04:10.152619121 +0200
+@@ -20,7 +20,7 @@
+ #define _LIBP11_INT_H
+ 
+ #ifndef _WIN32
+-#include "config.h"
++//#include "config.h"
+ #endif
+ 
+ #include "libp11.h"
+diff -urEb libp11-0.4.0.orig/src/p11_key.c libp11-0.4.0/src/p11_key.c
+--- libp11-0.4.0.orig/src/p11_key.c	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/p11_key.c	2020-04-02 17:05:39.892516032 +0200
+@@ -21,6 +21,12 @@
+ #include 
+ #include 
+ 
++#if OPENSSL_VERSION_NUMBER >= 0x10100105L // File renamed in OpenSSL 1.1.1e
++#  include 
++#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // OpenSSL 1.0.2
++#  include 
++#endif
++
+ #ifdef _WIN32
+ #define strncasecmp strnicmp
+ #endif
+diff -urEb libp11-0.4.0.orig/src/p11_rsa.c libp11-0.4.0/src/p11_rsa.c
+--- libp11-0.4.0.orig/src/p11_rsa.c	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/p11_rsa.c	2020-04-02 17:05:49.176504198 +0200
+@@ -27,6 +27,12 @@
+ #include 
+ #include 
+ 
++#if OPENSSL_VERSION_NUMBER >= 0x10100105L // File renamed in OpenSSL 1.1.1e
++#  include 
++#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // OpenSSL 1.0.2
++#  include 
++#endif
++
+ static int rsa_ex_index = 0;
+ 
+ #if OPENSSL_VERSION_NUMBER < 0x10100003L
diff --git a/OrthancFramework/Resources/Patches/mongoose-3.1-patch.diff b/OrthancFramework/Resources/Patches/mongoose-3.1-patch.diff
new file mode 100644
index 0000000..7e6e7ba
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/mongoose-3.1-patch.diff
@@ -0,0 +1,54 @@
+--- /home/jodogne/Subversion/Orthanc/ThirdPartyDownloads/mongoose/mongoose.c	2012-03-11 23:41:35.000000000 +0100
++++ mongoose.c	2013-03-07 10:07:00.566266153 +0100
+@@ -92,8 +92,9 @@
+ #define strtoll(x, y, z) strtol(x, y, z)
+ #else
+ #define __func__  __FUNCTION__
+-#define strtoull(x, y, z) _strtoui64(x, y, z)
+-#define strtoll(x, y, z) _strtoi64(x, y, z)
++#include 
++//#define strtoull(x, y, z) _strtoui64(x, y, z)
++//#define strtoll(x, y, z) _strtoi64(x, y, z)
+ #endif // _MSC_VER
+ 
+ #define ERRNO   GetLastError()
+@@ -253,6 +254,14 @@
+ #define MSG_NOSIGNAL 0
+ #endif
+ 
++#if __gnu_hurd__ == 1
++/**
++ * There is no limit on the length on a path under GNU Hurd, so we set
++ * it to an arbitrary constant.
++ **/
++#define PATH_MAX 4096
++#endif
++
+ typedef void * (*mg_thread_func_t)(void *);
+ 
+ static const char *http_500_error = "Internal Server Error";
+@@ -3844,10 +3853,8 @@
+ }
+ 
+ static void discard_current_request_from_buffer(struct mg_connection *conn) {
+-  char *buffered;
+   int buffered_len, body_len;
+ 
+-  buffered = conn->buf + conn->request_len;
+   buffered_len = conn->data_len - conn->request_len;
+   assert(buffered_len >= 0);
+ 
+@@ -4148,7 +4155,13 @@
+ 
+   // Wait until mg_fini() stops
+   while (ctx->stop_flag != 2) {
++#if defined(__linux__)
++    usleep(100000);
++#elif defined(_WIN32)
++    Sleep(100);
++#else
+     (void) sleep(0);
++#endif
+   }
+   free_context(ctx);
+ 
diff --git a/OrthancFramework/Resources/Patches/mongoose-3.8-patch.diff b/OrthancFramework/Resources/Patches/mongoose-3.8-patch.diff
new file mode 100644
index 0000000..f43c4e5
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/mongoose-3.8-patch.diff
@@ -0,0 +1,106 @@
+--- mongoose.c.orig	2019-07-31 14:06:36.043726677 +0200
++++ mongoose.c	2019-07-31 14:10:06.767727652 +0200
+@@ -50,6 +50,14 @@
+ #define PATH_MAX FILENAME_MAX
+ #endif // __SYMBIAN32__
+ 
++#if __gnu_hurd__ == 1
++/**
++ * There is no limit on the length on a path under GNU Hurd, so we set
++ * it to an arbitrary constant.
++ **/
++#define PATH_MAX 4096
++#endif
++
+ #ifndef _WIN32_WCE // Some ANSI #includes are not available on Windows CE
+ #include 
+ #include 
+@@ -108,8 +116,9 @@
+ #define strtoll(x, y, z) _atoi64(x)
+ #else
+ #define __func__  __FUNCTION__
+-#define strtoull(x, y, z) _strtoui64(x, y, z)
+-#define strtoll(x, y, z) _strtoi64(x, y, z)
++#include 
++//#define strtoull(x, y, z) _strtoui64(x, y, z)
++//#define strtoll(x, y, z) _strtoi64(x, y, z)
+ #endif // _MSC_VER
+ 
+ #define ERRNO   GetLastError()
+@@ -2997,19 +3006,19 @@
+   }
+ }
+ 
+-static int is_valid_http_method(const char *method) {
+-  return !strcmp(method, "GET") || !strcmp(method, "POST") ||
++static int is_valid_http_method(const char *method, int *isValidHttpMethod) {
++  *isValidHttpMethod = !strcmp(method, "GET") || !strcmp(method, "POST") ||
+     !strcmp(method, "HEAD") || !strcmp(method, "CONNECT") ||
+     !strcmp(method, "PUT") || !strcmp(method, "DELETE") ||
+     !strcmp(method, "OPTIONS") || !strcmp(method, "PROPFIND")
+-    || !strcmp(method, "MKCOL")
+-          ;
++    || !strcmp(method, "MKCOL");
++  return *isValidHttpMethod;
+ }
+ 
+ // Parse HTTP request, fill in mg_request_info structure.
+ // This function modifies the buffer by NUL-terminating
+ // HTTP request components, header names and header values.
+-static int parse_http_message(char *buf, int len, struct mg_request_info *ri) {
++static int parse_http_message(char *buf, int len, struct mg_request_info *ri, int *isValidHttpMethod) {
+   int is_request, request_length = get_request_len(buf, len);
+   if (request_length > 0) {
+     // Reset attributes. DO NOT TOUCH is_ssl, remote_ip, remote_port
+@@ -3025,7 +3034,7 @@
+     ri->request_method = skip(&buf, " ");
+     ri->uri = skip(&buf, " ");
+     ri->http_version = skip(&buf, "\r\n");
+-    if (((is_request = is_valid_http_method(ri->request_method)) &&
++    if (((is_request = is_valid_http_method(ri->request_method, isValidHttpMethod)) &&
+          memcmp(ri->http_version, "HTTP/", 5) != 0) ||
+         (!is_request && memcmp(ri->request_method, "HTTP/", 5)) != 0) {
+       request_length = -1;
+@@ -4930,7 +4939,7 @@
+   return uri[0] == '/' || (uri[0] == '*' && uri[1] == '\0');
+ }
+ 
+-static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len) {
++static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int *isValidHttpMethod) {
+   const char *cl;
+ 
+   ebuf[0] = '\0';
+@@ -4944,7 +4953,7 @@
+   } else if (conn->request_len <= 0) {
+     snprintf(ebuf, ebuf_len, "%s", "Client closed connection");
+   } else if (parse_http_message(conn->buf, conn->buf_size,
+-                                &conn->request_info) <= 0) {
++                                &conn->request_info, isValidHttpMethod) <= 0) {
+     snprintf(ebuf, ebuf_len, "Bad request: [%.*s]", conn->data_len, conn->buf);
+   } else {
+     // Request is valid
+@@ -4973,7 +4982,8 @@
+   } else if (mg_vprintf(conn, fmt, ap) <= 0) {
+     snprintf(ebuf, ebuf_len, "%s", "Error sending request");
+   } else {
+-    getreq(conn, ebuf, ebuf_len);
++    int isValidHttpMethod = 1; /* unused in this case */
++    getreq(conn, ebuf, ebuf_len, &isValidHttpMethod);
+   }
+   if (ebuf[0] != '\0' && conn != NULL) {
+     mg_close_connection(conn);
+@@ -4995,8 +5005,13 @@
+   // to crule42.
+   conn->data_len = 0;
+   do {
+-    if (!getreq(conn, ebuf, sizeof(ebuf))) {
++    int isValidHttpMethod = 1;
++    if (!getreq(conn, ebuf, sizeof(ebuf), &isValidHttpMethod)) {
++      if (isValidHttpMethod) {
+       send_http_error(conn, 500, "Server Error", "%s", ebuf);
++      } else {
++        send_http_error(conn, 400, "Bad Request", "%s", ebuf);
++      }
+       conn->must_close = 1;
+     } else if (!is_valid_uri(conn->request_info.uri)) {
+       snprintf(ebuf, sizeof(ebuf), "Invalid URI: [%s]", ri->uri);
diff --git a/OrthancFramework/Resources/Patches/openssl-1.1.1-conf.h.in b/OrthancFramework/Resources/Patches/openssl-1.1.1-conf.h.in
new file mode 100644
index 0000000..10b942a
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/openssl-1.1.1-conf.h.in
@@ -0,0 +1,126 @@
+/*
+ * {- join("\n * ", @autowarntext) -}
+ *
+ * Copyright 2016-2020 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include 
+
+#ifdef  __cplusplus
+extern "C" {
+#endif
+
+#ifdef OPENSSL_ALGORITHM_DEFINES
+# error OPENSSL_ALGORITHM_DEFINES no longer supported
+#endif
+
+/*
+ * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers
+ * don't like that.  This will hopefully silence them.
+ */
+#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy;
+
+/*
+ * Applications should use -DOPENSSL_API_COMPAT= to suppress the
+ * declarations of functions deprecated in or before . Otherwise, they
+ * still won't see them if the library has been built to disable deprecated
+ * functions.
+ */
+#ifndef DECLARE_DEPRECATED
+# define DECLARE_DEPRECATED(f)   f;
+# ifdef __GNUC__
+#  if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0)
+#   undef DECLARE_DEPRECATED
+#   define DECLARE_DEPRECATED(f)    f __attribute__ ((deprecated));
+#  endif
+# elif defined(__SUNPRO_C)
+#  if (__SUNPRO_C >= 0x5130)
+#   undef DECLARE_DEPRECATED
+#   define DECLARE_DEPRECATED(f)    f __attribute__ ((deprecated));
+#  endif
+# endif
+#endif
+
+#ifndef OPENSSL_FILE
+# ifdef OPENSSL_NO_FILENAMES
+#  define OPENSSL_FILE ""
+#  define OPENSSL_LINE 0
+# else
+#  define OPENSSL_FILE __FILE__
+#  define OPENSSL_LINE __LINE__
+# endif
+#endif
+
+#ifndef OPENSSL_MIN_API
+# define OPENSSL_MIN_API 0
+#endif
+
+#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API
+# undef OPENSSL_API_COMPAT
+# define OPENSSL_API_COMPAT OPENSSL_MIN_API
+#endif
+
+/*
+ * Do not deprecate things to be deprecated in version 1.2.0 before the
+ * OpenSSL version number matches.
+ */
+#if OPENSSL_VERSION_NUMBER < 0x10200000L
+# define DEPRECATEDIN_1_2_0(f)   f;
+#elif OPENSSL_API_COMPAT < 0x10200000L
+# define DEPRECATEDIN_1_2_0(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_1_2_0(f)
+#endif
+
+#if OPENSSL_API_COMPAT < 0x10100000L
+# define DEPRECATEDIN_1_1_0(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_1_1_0(f)
+#endif
+
+#if OPENSSL_API_COMPAT < 0x10000000L
+# define DEPRECATEDIN_1_0_0(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_1_0_0(f)
+#endif
+
+#if OPENSSL_API_COMPAT < 0x00908000L
+# define DEPRECATEDIN_0_9_8(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_0_9_8(f)
+#endif
+
+
+#define OPENSSL_UNISTD 
+
+#if 0
+/* Generate 80386 code? */
+{- ${processor} eq "386" ? "#define" : "#undef" -} I386_ONLY
+
+#undef OPENSSL_UNISTD
+#define OPENSSL_UNISTD {- ${unistd} -}
+
+{- ${export_var_as_fn} ? "#define" : "#undef" -} OPENSSL_EXPORT_VAR_AS_FUNCTION
+
+/*
+ * The following are cipher-specific, but are part of the public API.
+ */
+#if !defined(OPENSSL_SYS_UEFI)
+{- ${bn_ll} ? "# define" : "# undef" -} BN_LLONG
+/* Only one for the following should be defined */
+{- ${b64l} ? "# define" : "# undef" -} SIXTY_FOUR_BIT_LONG
+{- ${b64}  ? "# define" : "# undef" -} SIXTY_FOUR_BIT
+{- ${b32}  ? "# define" : "# undef" -} THIRTY_TWO_BIT
+#endif
+
+#define RC4_INT {- ${rc4_int} -}
+#endif
+
+#ifdef  __cplusplus
+}
+#endif
diff --git a/OrthancFramework/Resources/Patches/openssl-1.1.1k.patch b/OrthancFramework/Resources/Patches/openssl-1.1.1k.patch
new file mode 100644
index 0000000..8f98853
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/openssl-1.1.1k.patch
@@ -0,0 +1,19 @@
+diff -urEb openssl-1.1.1k.orig/crypto/rand/rand_unix.c openssl-1.1.1k/crypto/rand/rand_unix.c
+--- openssl-1.1.1k.orig/crypto/rand/rand_unix.c	2021-04-21 11:33:05.241258372 +0200
++++ openssl-1.1.1k/crypto/rand/rand_unix.c	2021-04-21 11:34:48.705287133 +0200
+@@ -455,6 +455,7 @@
+              * system call and this should always succeed which renders
+              * this alternative but essentially identical source moot.
+              */
++#if !defined(__LSB_VERSION__)  // "syscall()" is not available in LSB
+             if (uname(&un) == 0) {
+                 kernel[0] = atoi(un.release);
+                 p = strchr(un.release, '.');
+@@ -465,6 +466,7 @@
+                     return 0;
+                 }
+             }
++#endif
+             /* Open /dev/random and wait for it to be readable */
+             if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) {
+                 if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) {
diff --git a/OrthancFramework/Resources/Patches/openssl-3.1.4.patch b/OrthancFramework/Resources/Patches/openssl-3.1.4.patch
new file mode 100644
index 0000000..4bf024b
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/openssl-3.1.4.patch
@@ -0,0 +1,47 @@
+diff -urEb openssl-3.1.4.orig/crypto/riscvcap.c openssl-3.1.4/crypto/riscvcap.c
+--- openssl-3.1.4.orig/crypto/riscvcap.c	2024-01-24 16:58:48.308108757 +0100
++++ openssl-3.1.4/crypto/riscvcap.c	2024-01-24 17:01:04.114914015 +0100
+@@ -37,7 +37,8 @@
+ 
+ static void strtoupper(char *str)
+ {
+-    for (char *x = str; *x; ++x)
++    char* x;
++    for (x = str; *x; ++x)
+         *x = toupper(*x);
+ }
+ 
+@@ -51,12 +52,13 @@
+ {
+     char envstrupper[BUFLEN];
+     char buf[BUFLEN];
++    size_t i;
+ 
+     /* Convert env str to all uppercase */
+     OPENSSL_strlcpy(envstrupper, envstr, sizeof(envstrupper));
+     strtoupper(envstrupper);
+ 
+-    for (size_t i = 0; i < kRISCVNumCaps; ++i) {
++    for (i = 0; i < kRISCVNumCaps; ++i) {
+         /* Prefix capability with underscore in preparation for search */
+         BIO_snprintf(buf, BUFLEN, "_%s", RISCV_capabilities[i].name);
+         if (strstr(envstrupper, buf) != NULL) {
+diff -urEb openssl-3.1.4.orig/providers/implementations/rands/seeding/rand_unix.c openssl-3.1.4/providers/implementations/rands/seeding/rand_unix.c
+--- openssl-3.1.4.orig/providers/implementations/rands/seeding/rand_unix.c	2024-01-24 16:58:48.332108547 +0100
++++ openssl-3.1.4/providers/implementations/rands/seeding/rand_unix.c	2024-01-24 17:01:30.182683539 +0100
+@@ -452,6 +452,7 @@
+              * system call and this should always succeed which renders
+              * this alternative but essentially identical source moot.
+              */
++#if !defined(__LSB_VERSION__)  // "syscall()" is not available in LSB
+             if (uname(&un) == 0) {
+                 kernel[0] = atoi(un.release);
+                 p = strchr(un.release, '.');
+@@ -462,6 +463,7 @@
+                     return 0;
+                 }
+             }
++#endif
+             /* Open /dev/random and wait for it to be readable */
+             if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) {
+                 if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) {
diff --git a/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch b/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch
new file mode 100644
index 0000000..b21b045
--- /dev/null
+++ b/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch
@@ -0,0 +1,30 @@
+diff -urEb protobuf-3.5.1.orig/src/google/protobuf/stubs/io_win32.cc protobuf-3.5.1/src/google/protobuf/stubs/io_win32.cc
+--- protobuf-3.5.1.orig/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:13:45.095021011 +0200
++++ protobuf-3.5.1/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:19:19.932920102 +0200
+@@ -91,7 +91,12 @@
+
+ template 
+ bool null_or_empty(const char_type* s) {
+-  return s == nullptr || *s == 0;
++  /**
++   * "nullptr" is not known to Visual Studio 2008, because this is a
++   * C++11 construction, which shouldn't be present in protobuf 3.5.1
++   * that is supposed to comply with C++98.
++   **/
++  return s == NULL || *s == 0;
+ }
+
+ // Returns true if the path starts with a drive letter, e.g. "c:".
+diff -urEb protobuf-3.5.1.orig/src/google/protobuf/stubs/hash.h protobuf-3.5.1/src/google/protobuf/stubs/hash.h
+--- protobuf-3.5.1.orig/src/google/protobuf/stubs/hash.h	2023-03-26 20:13:45.095021011 +0200
++++ protobuf-3.5.1/src/google/protobuf/stubs/hash.h	2023-03-26 20:19:19.932920102 +0200
+@@ -1,3 +1,9 @@
++#if _MSC_VER >= 1930       // Since Visual Studio 2022
++#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
++#include 
++#include 
++#endif
++
+ // Protocol Buffers - Google's data interchange format
+ // Copyright 2008 Google Inc.  All rights reserved.
+ // https://developers.google.com/protocol-buffers/
\ No newline at end of file
diff --git a/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt b/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt
new file mode 100644
index 0000000..76a15f0
--- /dev/null
+++ b/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt
@@ -0,0 +1,150 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+cmake_minimum_required(VERSION 2.8.3...4.0)
+
+project(ProtocolBuffers)
+
+set(ALLOW_DOWNLOADS ON)
+
+include(${CMAKE_SOURCE_DIR}/../CMake/DownloadPackage.cmake)
+include(${CMAKE_SOURCE_DIR}/../CMake/Compiler.cmake)
+
+include(${CMAKE_SOURCE_DIR}/ProtobufLibrary.cmake)
+
+set(PROTOBUF_COMPILER_SOURCES
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/code_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/command_line_interface.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_map_field.cc  
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_padding_optimizer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_string_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_doc_comment.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_field_base.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_reflection_class.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_source_generator_base.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_wrapper_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/importer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_context.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_doc_comment.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_extension_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_generator_factory.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_lazy_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_lazy_message_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_map_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_builder.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_builder_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_name_resolver.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_primitive_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_shared_code_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_string_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_string_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_primitive_field.cc
+  #${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/embed.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/js_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/well_known_types_embed.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/main.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_oneof.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/parser.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/php/php_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/plugin.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/plugin.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/python/python_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/ruby/ruby_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/subprocess.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/zip_writer.cc
+  )
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  set_property(
+    SOURCE ${PROTOBUF_COMPILER_SOURCES} APPEND
+    PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
+    )
+endif()
+
+add_executable(protoc
+  ${PROTOBUF_LIBRARY_SOURCES}
+  ${PROTOBUF_COMPILER_SOURCES}
+  )
+
+install(
+  TARGETS protoc
+  RUNTIME DESTINATION .
+  )
diff --git a/OrthancFramework/Resources/ProtocolBuffers/NOTES.txt b/OrthancFramework/Resources/ProtocolBuffers/NOTES.txt
new file mode 100644
index 0000000..9320d3b
--- /dev/null
+++ b/OrthancFramework/Resources/ProtocolBuffers/NOTES.txt
@@ -0,0 +1,29 @@
+
+Version
+=======
+
+We use Google's Protocol Buffers version 3.5.1, as this is the last
+release to be compatible with C++98, which is mandatory for Visual
+Studio 2008 and Linux Standard Base.
+
+References:
+https://github.com/protocolbuffers/protobuf/releases/tag/v3.5.1
+https://github.com/protocolbuffers/protobuf/issues/2780
+
+
+Linux Standard Base
+===================
+
+$ mkdir lsb
+$ cd lsb
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON -DCMAKE_TOOLCHAIN_FILE=../../Toolchains/LinuxStandardBaseToolchain.cmake -G Ninja
+$ ninja
+
+
+MinGW for 32bits
+================
+
+$ mkdir w32
+$ cd w32
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON -DCMAKE_TOOLCHAIN_FILE=../../Toolchains/MinGW-W64-Toolchain32.cmake -G Ninja
+$ ninja
diff --git a/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake b/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake
new file mode 100644
index 0000000..5ab7814
--- /dev/null
+++ b/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake
@@ -0,0 +1,145 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+set(PROTOBUF_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/protobuf-3.5.1)
+
+if (IS_DIRECTORY "${PROTOBUF_SOURCE_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(
+  "ca0d9b243e649d398a6b419acd35103a"
+  "https://orthanc.uclouvain.be/downloads/third-party-downloads/protobuf-cpp-3.5.1.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/protobuf-3.5.1")
+
+if (FirstRun)
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${CMAKE_CURRENT_LIST_DIR}/../Patches/protobuf-3.5.1.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+endif()
+
+include_directories(
+  ${PROTOBUF_SOURCE_DIR}/src
+  )
+  
+set(PROTOBUF_LIBRARY_SOURCES
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/any.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/any.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/api.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/arena.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/arenastring.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor_database.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/duration.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/dynamic_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/empty.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/extension_set.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/extension_set_heavy.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/field_mask.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_reflection.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_table_driven.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_table_driven_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/coded_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/gzip_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/printer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/strtod.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/tokenizer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream_impl.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream_impl_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/message_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/reflection_ops.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/repeated_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/source_context.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/struct.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_arm_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_generic_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_mips_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_ppc_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/common.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/int128.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/io_win32.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/mathlimits.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/once.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/status.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/statusor.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/stringpiece.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/stringprintf.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/structurally_valid.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/strutil.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/substitute.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/time.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/bytestream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/text_format.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/timestamp.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/type.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/unknown_field_set.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/delimited_message_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/field_comparator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/field_mask_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/datapiece.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/default_value_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/error_listener.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/field_mask_utility.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_escaping.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_stream_parser.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/object_writer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/proto_writer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/protostream_objectsource.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/protostream_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/type_info.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/utility.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/json_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/message_differencer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/time_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/type_resolver_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wire_format.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wire_format_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wrappers.pb.cc
+  )
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  set_property(
+    SOURCE ${PROTOBUF_LIBRARY_SOURCES} APPEND
+    PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
+    )
+endif()
diff --git a/OrthancFramework/Resources/ProtocolBuffers/ThirdPartyDownloads/protobuf-cpp-3.5.1.tar.gz b/OrthancFramework/Resources/ProtocolBuffers/ThirdPartyDownloads/protobuf-cpp-3.5.1.tar.gz
new file mode 100644
index 0000000..567b400
Binary files /dev/null and b/OrthancFramework/Resources/ProtocolBuffers/ThirdPartyDownloads/protobuf-cpp-3.5.1.tar.gz differ
diff --git a/OrthancFramework/Resources/RetrieveCACertificates.py b/OrthancFramework/Resources/RetrieveCACertificates.py
new file mode 100755
index 0000000..a141f53
--- /dev/null
+++ b/OrthancFramework/Resources/RetrieveCACertificates.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+import re
+import sys
+import subprocess
+import urllib2
+
+
+if len(sys.argv) <= 2:
+    print('Download a set of CA certificates, convert them to PEM, then format them as a C macro')
+    print('Usage: %s [Macro] [Certificate1] ...' % sys.argv[0])
+    print('')
+    print('Example: %s GITHUB_CERTIFICATES https://cacerts.digicert.com/DigiCertSHA2HighAssuranceServerCA.crt' % sys.argv[0])
+    print('')
+    sys.exit(-1)
+
+MACRO = sys.argv[1]
+
+sys.stdout.write('#define %s ' % MACRO)
+
+for url in sys.argv[2:]:
+    # Download the certificate from the CA authority, in the DES format
+    des = urllib2.urlopen(url).read()
+
+    # Convert DES to PEM
+    p = subprocess.Popen([ 'openssl', 'x509', '-inform', 'DES', '-outform', 'PEM' ],
+                         stdin = subprocess.PIPE,
+                         stdout = subprocess.PIPE)
+    pem = p.communicate(input = des)[0]
+    pem = re.sub(r'\r', '', pem)       # Remove any carriage return
+    pem = re.sub(r'\\', r'\\\\', pem)  # Escape any backslash
+    pem = re.sub(r'"', r'\\"', pem)    # Escape any quote
+
+    # Write the PEM data into the macro
+    for line in pem.split('\n'):
+        sys.stdout.write(' \\\n')
+        sys.stdout.write('"%s\\n" ' % line)
+
+sys.stdout.write('\n')
+sys.stderr.write('Done!\n')
diff --git a/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt b/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt
new file mode 100644
index 0000000..0bf438e
--- /dev/null
+++ b/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt
@@ -0,0 +1,37 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+cmake_minimum_required(VERSION 2.8...4.0)
+
+project(Sample)
+
+include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_ZLIB ON)
+set(ENABLE_WEB_SERVER ON)
+
+include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkConfiguration.cmake)
+
+add_executable(Sample
+  ${ORTHANC_CORE_SOURCES}
+  Sample.cpp
+  )
diff --git a/OrthancFramework/Resources/Samples/MicroService/README.txt b/OrthancFramework/Resources/Samples/MicroService/README.txt
new file mode 100644
index 0000000..a92d992
--- /dev/null
+++ b/OrthancFramework/Resources/Samples/MicroService/README.txt
@@ -0,0 +1,2 @@
+This file shows how to create a simple Web service in C++ (similar to
+Python's Flask) using the Orthanc standalone framework.
diff --git a/OrthancFramework/Resources/Samples/MicroService/Sample.cpp b/OrthancFramework/Resources/Samples/MicroService/Sample.cpp
new file mode 100644
index 0000000..80b9e2a
--- /dev/null
+++ b/OrthancFramework/Resources/Samples/MicroService/Sample.cpp
@@ -0,0 +1,85 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include 
+
+#include "../../../Sources/HttpServer/HttpServer.h"
+#include "../../../Sources/Logging.h"
+#include "../../../Sources/RestApi/RestApi.h"
+#include "../../../Sources/SystemToolbox.h"
+
+class MicroService : public Orthanc::RestApi
+{
+private:
+  static MicroService& GetSelf(Orthanc::RestApiCall& call)
+  {
+    return dynamic_cast(call.GetContext());
+  }
+
+  void SayHello()
+  {
+    printf("Hello\n");
+  }
+
+  static void Hello(Orthanc::RestApiGetCall& call)
+  {
+    GetSelf(call).SayHello();
+    
+    Json::Value value = Json::arrayValue;
+    value.append("World");
+    
+    call.GetOutput().AnswerJson(value);
+  }
+
+public:
+  MicroService()
+  {
+    Register("/hello", Hello);
+  }  
+};
+
+int main()
+{
+  Orthanc::Logging::Initialize();
+  Orthanc::Logging::EnableTraceLevel(true);
+
+  MicroService rest;
+  
+  {
+    Orthanc::HttpServer httpServer;
+    httpServer.SetPortNumber(8000);
+    httpServer.Register(rest);
+    httpServer.SetRemoteAccessAllowed(true);
+    httpServer.Start();
+    
+    LOG(WARNING) << "Micro-service started on port " << httpServer.GetPortNumber();
+    Orthanc::SystemToolbox::ServerBarrier();
+  }
+
+  LOG(WARNING) << "Micro-service stopped";
+
+  Orthanc::Logging::Finalize();
+  
+  return 0;
+}
diff --git a/OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h b/OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h
new file mode 100644
index 0000000..4fe0ef9
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h
@@ -0,0 +1,259 @@
+// ISO C9x  compliant stdint.h for Microsoft Visual Studio
+// Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124 
+// 
+//  Copyright (c) 2006-2013 Alexander Chemeris
+// 
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+// 
+//   1. Redistributions of source code must retain the above copyright notice,
+//      this list of conditions and the following disclaimer.
+// 
+//   2. Redistributions in binary form must reproduce the above copyright
+//      notice, this list of conditions and the following disclaimer in the
+//      documentation and/or other materials provided with the distribution.
+// 
+//   3. Neither the name of the product nor the names of its contributors may
+//      be used to endorse or promote products derived from this software
+//      without specific prior written permission.
+// 
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// 
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef _MSC_VER // [
+#error "Use this header only with Microsoft Visual C++ compilers!"
+#endif // _MSC_VER ]
+
+#ifndef _MSC_STDINT_H_ // [
+#define _MSC_STDINT_H_
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#if _MSC_VER >= 1600 // [
+#include 
+#else // ] _MSC_VER >= 1600 [
+
+#include 
+
+// For Visual Studio 6 in C++ mode and for many Visual Studio versions when
+// compiling for ARM we should wrap  include with 'extern "C++" {}'
+// or compiler give many errors like this:
+//   error C2733: second C linkage of overloaded function 'wmemchr' not allowed
+#ifdef __cplusplus
+extern "C" {
+#endif
+#  include 
+#ifdef __cplusplus
+}
+#endif
+
+// Define _W64 macros to mark types changing their size, like intptr_t.
+#ifndef _W64
+#  if !defined(__midl) && (defined(_X86_) || defined(_M_IX86)) && _MSC_VER >= 1300
+#     define _W64 __w64
+#  else
+#     define _W64
+#  endif
+#endif
+
+
+// 7.18.1 Integer types
+
+// 7.18.1.1 Exact-width integer types
+
+// Visual Studio 6 and Embedded Visual C++ 4 doesn't
+// realize that, e.g. char has the same size as __int8
+// so we give up on __intX for them.
+#if (_MSC_VER < 1300)
+   typedef signed char       int8_t;
+   typedef signed short      int16_t;
+   typedef signed int        int32_t;
+   typedef unsigned char     uint8_t;
+   typedef unsigned short    uint16_t;
+   typedef unsigned int      uint32_t;
+#else
+   typedef signed __int8     int8_t;
+   typedef signed __int16    int16_t;
+   typedef signed __int32    int32_t;
+   typedef unsigned __int8   uint8_t;
+   typedef unsigned __int16  uint16_t;
+   typedef unsigned __int32  uint32_t;
+#endif
+typedef signed __int64       int64_t;
+typedef unsigned __int64     uint64_t;
+
+
+// 7.18.1.2 Minimum-width integer types
+typedef int8_t    int_least8_t;
+typedef int16_t   int_least16_t;
+typedef int32_t   int_least32_t;
+typedef int64_t   int_least64_t;
+typedef uint8_t   uint_least8_t;
+typedef uint16_t  uint_least16_t;
+typedef uint32_t  uint_least32_t;
+typedef uint64_t  uint_least64_t;
+
+// 7.18.1.3 Fastest minimum-width integer types
+typedef int8_t    int_fast8_t;
+typedef int16_t   int_fast16_t;
+typedef int32_t   int_fast32_t;
+typedef int64_t   int_fast64_t;
+typedef uint8_t   uint_fast8_t;
+typedef uint16_t  uint_fast16_t;
+typedef uint32_t  uint_fast32_t;
+typedef uint64_t  uint_fast64_t;
+
+// 7.18.1.4 Integer types capable of holding object pointers
+#ifdef _WIN64 // [
+   typedef signed __int64    intptr_t;
+   typedef unsigned __int64  uintptr_t;
+#else // _WIN64 ][
+   typedef _W64 signed int   intptr_t;
+   typedef _W64 unsigned int uintptr_t;
+#endif // _WIN64 ]
+
+// 7.18.1.5 Greatest-width integer types
+typedef int64_t   intmax_t;
+typedef uint64_t  uintmax_t;
+
+
+// 7.18.2 Limits of specified-width integer types
+
+#if !defined(__cplusplus) || defined(__STDC_LIMIT_MACROS) // [   See footnote 220 at page 257 and footnote 221 at page 259
+
+// 7.18.2.1 Limits of exact-width integer types
+#define INT8_MIN     ((int8_t)_I8_MIN)
+#define INT8_MAX     _I8_MAX
+#define INT16_MIN    ((int16_t)_I16_MIN)
+#define INT16_MAX    _I16_MAX
+#define INT32_MIN    ((int32_t)_I32_MIN)
+#define INT32_MAX    _I32_MAX
+#define INT64_MIN    ((int64_t)_I64_MIN)
+#define INT64_MAX    _I64_MAX
+#define UINT8_MAX    _UI8_MAX
+#define UINT16_MAX   _UI16_MAX
+#define UINT32_MAX   _UI32_MAX
+#define UINT64_MAX   _UI64_MAX
+
+// 7.18.2.2 Limits of minimum-width integer types
+#define INT_LEAST8_MIN    INT8_MIN
+#define INT_LEAST8_MAX    INT8_MAX
+#define INT_LEAST16_MIN   INT16_MIN
+#define INT_LEAST16_MAX   INT16_MAX
+#define INT_LEAST32_MIN   INT32_MIN
+#define INT_LEAST32_MAX   INT32_MAX
+#define INT_LEAST64_MIN   INT64_MIN
+#define INT_LEAST64_MAX   INT64_MAX
+#define UINT_LEAST8_MAX   UINT8_MAX
+#define UINT_LEAST16_MAX  UINT16_MAX
+#define UINT_LEAST32_MAX  UINT32_MAX
+#define UINT_LEAST64_MAX  UINT64_MAX
+
+// 7.18.2.3 Limits of fastest minimum-width integer types
+#define INT_FAST8_MIN    INT8_MIN
+#define INT_FAST8_MAX    INT8_MAX
+#define INT_FAST16_MIN   INT16_MIN
+#define INT_FAST16_MAX   INT16_MAX
+#define INT_FAST32_MIN   INT32_MIN
+#define INT_FAST32_MAX   INT32_MAX
+#define INT_FAST64_MIN   INT64_MIN
+#define INT_FAST64_MAX   INT64_MAX
+#define UINT_FAST8_MAX   UINT8_MAX
+#define UINT_FAST16_MAX  UINT16_MAX
+#define UINT_FAST32_MAX  UINT32_MAX
+#define UINT_FAST64_MAX  UINT64_MAX
+
+// 7.18.2.4 Limits of integer types capable of holding object pointers
+#ifdef _WIN64 // [
+#  define INTPTR_MIN   INT64_MIN
+#  define INTPTR_MAX   INT64_MAX
+#  define UINTPTR_MAX  UINT64_MAX
+#else // _WIN64 ][
+#  define INTPTR_MIN   INT32_MIN
+#  define INTPTR_MAX   INT32_MAX
+#  define UINTPTR_MAX  UINT32_MAX
+#endif // _WIN64 ]
+
+// 7.18.2.5 Limits of greatest-width integer types
+#define INTMAX_MIN   INT64_MIN
+#define INTMAX_MAX   INT64_MAX
+#define UINTMAX_MAX  UINT64_MAX
+
+// 7.18.3 Limits of other integer types
+
+#ifdef _WIN64 // [
+#  define PTRDIFF_MIN  _I64_MIN
+#  define PTRDIFF_MAX  _I64_MAX
+#else  // _WIN64 ][
+#  define PTRDIFF_MIN  _I32_MIN
+#  define PTRDIFF_MAX  _I32_MAX
+#endif  // _WIN64 ]
+
+#define SIG_ATOMIC_MIN  INT_MIN
+#define SIG_ATOMIC_MAX  INT_MAX
+
+#ifndef SIZE_MAX // [
+#  ifdef _WIN64 // [
+#     define SIZE_MAX  _UI64_MAX
+#  else // _WIN64 ][
+#     define SIZE_MAX  _UI32_MAX
+#  endif // _WIN64 ]
+#endif // SIZE_MAX ]
+
+// WCHAR_MIN and WCHAR_MAX are also defined in 
+#ifndef WCHAR_MIN // [
+#  define WCHAR_MIN  0
+#endif  // WCHAR_MIN ]
+#ifndef WCHAR_MAX // [
+#  define WCHAR_MAX  _UI16_MAX
+#endif  // WCHAR_MAX ]
+
+#define WINT_MIN  0
+#define WINT_MAX  _UI16_MAX
+
+#endif // __STDC_LIMIT_MACROS ]
+
+
+// 7.18.4 Limits of other integer types
+
+#if !defined(__cplusplus) || defined(__STDC_CONSTANT_MACROS) // [   See footnote 224 at page 260
+
+// 7.18.4.1 Macros for minimum-width integer constants
+
+#define INT8_C(val)  val##i8
+#define INT16_C(val) val##i16
+#define INT32_C(val) val##i32
+#define INT64_C(val) val##i64
+
+#define UINT8_C(val)  val##ui8
+#define UINT16_C(val) val##ui16
+#define UINT32_C(val) val##ui32
+#define UINT64_C(val) val##ui64
+
+// 7.18.4.2 Macros for greatest-width integer constants
+// These #ifndef's are needed to prevent collisions with .
+// Check out Issue 9 for the details.
+#ifndef INTMAX_C //   [
+#  define INTMAX_C   INT64_C
+#endif // INTMAX_C    ]
+#ifndef UINTMAX_C //  [
+#  define UINTMAX_C  UINT64_C
+#endif // UINTMAX_C   ]
+
+#endif // __STDC_CONSTANT_MACROS ]
+
+#endif // _MSC_VER >= 1600 ]
+
+#endif // _MSC_STDINT_H_ ]
diff --git a/OrthancFramework/Resources/ThirdParty/base64/base64.cpp b/OrthancFramework/Resources/ThirdParty/base64/base64.cpp
new file mode 100644
index 0000000..aff7944
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/base64/base64.cpp
@@ -0,0 +1,179 @@
+/* 
+   base64.cpp and base64.h
+
+   Copyright (C) 2004-2008 Ren Nyffenegger
+
+   This source code is provided 'as-is', without any express or implied
+   warranty. In no event will the author be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this source code must not be misrepresented; you must not
+      claim that you wrote the original source code. If you use this source code
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original source code.
+
+   3. This notice may not be removed or altered from any source distribution.
+
+   Ren Nyffenegger rene.nyffenegger@adp-gmbh.ch
+
+   ------------------------------
+   This version has been modified (changed the interface + use another decoding algorithm
+   inspired from https://stackoverflow.com/a/34571089 which was faster)
+*/
+
+#include "base64.h"
+#include 
+#include 
+
+static const std::string base64_chars = 
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    "abcdefghijklmnopqrstuvwxyz"
+    "0123456789+/";
+
+static inline bool is_base64(unsigned char c) {
+  return (isalnum(c) || (c == '+') || (c == '/'));
+}
+
+void base64_encode(std::string& result, const std::string& stringToEncode)
+{
+  const unsigned char* bytes_to_encode = reinterpret_cast
+      (stringToEncode.size() > 0 ? &stringToEncode[0] : NULL);
+  size_t in_len = stringToEncode.size();
+  
+  result.reserve(result.size() + in_len * 4 / 3 + 10);
+
+  int i = 0;
+  int j = 0;
+  unsigned char char_array_3[3];
+  unsigned char char_array_4[4];
+
+  while (in_len--) {
+    char_array_3[i++] = *(bytes_to_encode++);
+    if (i == 3) {
+      char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+      char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+      char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+      char_array_4[3] = char_array_3[2] & 0x3f;
+
+      for(i = 0; (i <4) ; i++)
+        result += base64_chars[char_array_4[i]];
+      i = 0;
+    }
+  }
+
+  if (i)
+  {
+    for(j = i; j < 3; j++)
+      char_array_3[j] = '\0';
+
+    char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+    char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+    char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+    char_array_4[3] = char_array_3[2] & 0x3f;
+
+    for (j = 0; (j < i + 1); j++)
+      result += base64_chars[char_array_4[j]];
+
+    while((i++ < 3))
+      result += '=';
+
+  }
+}
+
+// old code from Ren Nyffenegger.  This code is slower
+void base64_decode_old(std::string& result, const std::string& encoded_string) {
+  size_t in_len = encoded_string.size();
+  int i = 0;
+  int j = 0;
+  int in_ = 0;
+  unsigned char char_array_4[4], char_array_3[3];
+
+  result.reserve(result.size() + in_len * 3 / 4 + 10);
+
+  while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
+    char_array_4[i++] = encoded_string[in_]; in_++;
+    if (i ==4) {
+      for (i = 0; i <4; i++)
+        char_array_4[i] = base64_chars.find(char_array_4[i]);
+
+      char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+      char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+      char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+      for (i = 0; (i < 3); i++)
+        result += char_array_3[i];
+      i = 0;
+    }
+  }
+
+  if (i) {
+    for (j = i; j <4; j++)
+      char_array_4[j] = 0;
+
+    for (j = 0; j <4; j++)
+      char_array_4[j] = base64_chars.find(char_array_4[j]);
+
+    char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+    char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+    char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+    for (j = 0; (j < i - 1); j++)
+      result += char_array_3[j];
+  }
+}
+
+
+// new code from https://stackoverflow.com/a/34571089
+// note that the encoding algorithm from this page was slower (and bugged !)
+// this code is not using std::vector::find
+
+// static init equivalent to:
+// decode_indexes.assign(256, -1);
+// for (int i=0; i<64; ++i)
+//   decode_indexes[base64_chars[i]] = i;
+
+static const int decode_indexes[] = {
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+  52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
+  -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
+  15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+  -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+  41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
+};
+
+
+void base64_decode(std::string& result, const std::string &stringToDecode) {
+
+  result.reserve(result.size() + stringToDecode.size() * 3 / 4 + 10);
+
+  int val=0, valb=-8;
+  for (std::string::const_iterator c = stringToDecode.begin(); c != stringToDecode.end(); ++c)
+  {
+    size_t index = static_cast(*c);
+    if (decode_indexes[index] == -1)
+      break;
+    val = (val<<6) + decode_indexes[index];
+    valb += 6;
+    if (valb>=0) {
+      result.push_back(char((val>>valb)&0xFF));
+      valb-=8;
+    }
+  }
+}
diff --git a/OrthancFramework/Resources/ThirdParty/base64/base64.h b/OrthancFramework/Resources/ThirdParty/base64/base64.h
new file mode 100644
index 0000000..624d434
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/base64/base64.h
@@ -0,0 +1,4 @@
+#include 
+
+void base64_encode(std::string& result, const std::string& stringToEncode);
+void base64_decode(std::string& result, const std::string& s);
diff --git a/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt b/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt
new file mode 100644
index 0000000..b31e58e
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt
@@ -0,0 +1,148 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+cmake_minimum_required(VERSION 2.8...4.0)
+project(IcuCodeGeneration)
+
+set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
+
+if (NOT USE_LEGACY_LIBICU)
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
+endif()
+
+include(${CMAKE_SOURCE_DIR}/../../CMake/Compiler.cmake)
+include(${CMAKE_SOURCE_DIR}/../../CMake/DownloadPackage.cmake)
+include(Version.cmake)
+
+set(SOURCE_DATA
+  "${LIBICU_SOURCES_DIR}/source/data/in/${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}.dat")
+
+set(ALLOW_DOWNLOADS ON)
+DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}")
+
+include_directories(
+  ${LIBICU_SOURCES_DIR}/source/common
+  ${LIBICU_SOURCES_DIR}/source/i18n
+  ${LIBICU_SOURCES_DIR}/source/tools/toolutil/
+  )
+
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/common         LIBICU_SOURCES)
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n           LIBICU_SOURCES)
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/tools/toolutil LIBICU_SOURCES)
+
+if (USE_LEGACY_LIBICU)
+  list(APPEND LIBICU_SOURCES
+    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.c
+    )
+else()
+  list(APPEND LIBICU_SOURCES
+    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.cpp
+    )
+  set_source_files_properties(
+    ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
+    PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t"
+    )
+endif()
+
+
+
+add_executable(IcuCodeGeneration
+  ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
+  ${LIBICU_SOURCES}
+  )
+
+configure_file(${SOURCE_DATA}
+  ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
+  COPYONLY)
+
+add_custom_command(
+  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
+  COMMAND  IcuCodeGeneration ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
+  DEPENDS  IcuCodeGeneration
+  )
+
+# "--no-name" is necessary for 7-zip on Windows to behave similarly to gunzip
+add_custom_command(
+  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  COMMAND  gzip ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat.c --no-name -c > ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  DEPENDS  ${LIBICU_DATA}
+  )
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  # Generate a precompiled version for Visual Studio 64bit
+  set(TMP_ASSEMBLER      ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat.S)
+  set(TMP_OBJECT         ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.o)
+  set(TMP_LIBRARY        ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.lib)
+  set(PRECOMPILED_WIN64  ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.lib.gz)
+  
+  add_custom_command(
+    OUTPUT   ${TMP_ASSEMBLER}
+    COMMAND  IcuCodeGeneration ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat --assembly gcc-mingw64
+    DEPENDS  IcuCodeGeneration
+    )
+
+  add_custom_command(
+    OUTPUT   ${TMP_OBJECT}
+    COMMAND  x86_64-w64-mingw32-gcc -c ${TMP_ASSEMBLER} -o ${TMP_OBJECT}
+    DEPENDS  ${TMP_ASSEMBLER}
+    )
+
+  add_custom_command(
+    OUTPUT   ${TMP_LIBRARY}
+    COMMAND  x86_64-w64-mingw32-ar qc ${TMP_LIBRARY} ${TMP_OBJECT}
+    COMMAND  x86_64-w64-mingw32-ranlib ${TMP_LIBRARY}
+    DEPENDS  ${TMP_OBJECT}
+    )
+
+  # "--no-name" is necessary for 7-zip on Windows to behave similarly to gunzip
+  add_custom_command(
+    OUTPUT   ${PRECOMPILED_WIN64}
+    COMMAND  gzip ${TMP_LIBRARY} --no-name -c > ${PRECOMPILED_WIN64}
+    DEPENDS  ${TMP_LIBRARY}
+    )
+endif()
+
+
+add_custom_target(Final ALL DEPENDS
+  ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  ${PRECOMPILED_WIN64}
+  )
+
+install(
+  FILES
+  ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  ${PRECOMPILED_WIN64}
+  DESTINATION ${CMAKE_SOURCE_DIR}/../../../ThirdPartyDownloads
+  )
+
+add_definitions(
+  #-DU_COMBINED_IMPLEMENTATION
+  -DUCONFIG_NO_SERVICE=1
+  -DU_COMMON_IMPLEMENTATION
+  -DU_ENABLE_DYLOAD=0
+  -DU_HAVE_STD_STRING=1
+  -DU_I18N_IMPLEMENTATION
+  -DU_IO_IMPLEMENTATION
+  -DU_STATIC_IMPLEMENTATION=1
+  -DU_TOOLUTIL_IMPLEMENTATION
+  )
diff --git a/OrthancFramework/Resources/ThirdParty/icu/README.txt b/OrthancFramework/Resources/ThirdParty/icu/README.txt
new file mode 100644
index 0000000..1fd87b5
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/icu/README.txt
@@ -0,0 +1,36 @@
+Generating ICU data file
+========================
+
+This folder generates the "icudtXXX_dat.c" file that contains the
+resources internal to ICU.
+
+IMPORTANT: Since ICU 59, C++11 is mandatory, making it incompatible
+with Linux Standard Base (LSB) SDK. The option
+"-DUSE_LEGACY_LIBICU=ON" will use the latest version of ICU that does
+not use C++11 (58-2).
+
+
+Usage
+-----
+
+Newest release of icu:
+
+$ cmake .. -G Ninja && ninja install
+
+Legacy version suitable for LSB:
+
+$ cmake .. -G Ninja -DUSE_LEGACY_LIBICU=ON && ninja install
+
+Legacy version, compiled using LSB:
+
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -G Ninja \
+  -DCMAKE_TOOLCHAIN_FILE=../../../LinuxStandardBaseToolchain.cmake \
+  -DUSE_LEGACY_LIBICU=ON
+$ ninja install
+
+
+Result
+------
+
+The resulting files are placed in the "ThirdPartyDownloads" folder at
+the root of the Orthanc repository (next to the main "CMakeLists.txt").
diff --git a/OrthancFramework/Resources/ThirdParty/icu/Version.cmake b/OrthancFramework/Resources/ThirdParty/icu/Version.cmake
new file mode 100644
index 0000000..7726cc4
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/icu/Version.cmake
@@ -0,0 +1,56 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+# NB: Orthanc assume that the platform is of ASCII-family, not of
+# EBCDIC-family. A "e" suffix would be needed on EBCDIC. Look for
+# macro "U_ICUDATA_TYPE_LETTER" in the source code of icu for more
+# information.
+
+include(TestBigEndian)
+TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
+if(IS_BIG_ENDIAN)
+  set(LIBICU_SUFFIX "b")
+else()
+  set(LIBICU_SUFFIX "l")
+endif()
+
+set(LIBICU_BASE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads")
+
+if (USE_LEGACY_LIBICU)
+  # This is the latest version of icu that compiles without C++11
+  # support. It is used for Linux Standard Base and Visual Studio 2008.
+  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-58_2-src.tgz")
+  set(LIBICU_MD5 "fac212b32b7ec7ab007a12dff1f3aea1")
+  set(LIBICU_DATA_VERSION "icudt58")
+  set(LIBICU_DATA_COMPRESSED_MD5 "a39b07b38195158c6c3070332cef2173")
+  set(LIBICU_DATA_UNCOMPRESSED_MD5 "54d2593cec5c6a4469373231658153ce")
+else()
+  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-63_1-src.tgz")
+  set(LIBICU_MD5 "9e40f6055294284df958200e308bce50")
+  set(LIBICU_DATA_VERSION "icudt63")
+  set(LIBICU_DATA_COMPRESSED_MD5 "be495c0830de5f377fdfa8301a5faf3d")
+  set(LIBICU_DATA_UNCOMPRESSED_MD5 "99613c3f2ca9426c45dc554ad28cfb79")
+endif()
+
+set(LIBICU_SOURCES_DIR ${CMAKE_BINARY_DIR}/icu)
+set(LIBICU_DATA "${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}.dat.gz")
+set(LIBICU_DATA_URL "${LIBICU_BASE_URL}/${LIBICU_DATA}")
diff --git a/OrthancFramework/Resources/ThirdParty/md5/md5.c b/OrthancFramework/Resources/ThirdParty/md5/md5.c
new file mode 100644
index 0000000..c35d96c
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/md5/md5.c
@@ -0,0 +1,381 @@
+/*
+  Copyright (C) 1999, 2000, 2002 Aladdin Enterprises.  All rights reserved.
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  L. Peter Deutsch
+  ghost@aladdin.com
+
+ */
+/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */
+/*
+  Independent implementation of MD5 (RFC 1321).
+
+  This code implements the MD5 Algorithm defined in RFC 1321, whose
+  text is available at
+	http://www.ietf.org/rfc/rfc1321.txt
+  The code is derived from the text of the RFC, including the test suite
+  (section A.5) but excluding the rest of Appendix A.  It does not include
+  any code or documentation that is identified in the RFC as being
+  copyrighted.
+
+  The original and principal author of md5.c is L. Peter Deutsch
+  .  Other authors are noted in the change history
+  that follows (in reverse chronological order):
+
+  2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order
+	either statically or dynamically; added missing #include 
+	in library.
+  2002-03-11 lpd Corrected argument list for main(), and added int return
+	type, in test program and T value program.
+  2002-02-21 lpd Added missing #include  in test program.
+  2000-07-03 lpd Patched to eliminate warnings about "constant is
+	unsigned in ANSI C, signed in traditional"; made test program
+	self-checking.
+  1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
+  1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5).
+  1999-05-03 lpd Original version.
+ */
+
+#include "md5.h"
+#include 
+
+#undef BYTE_ORDER	/* 1 = big-endian, -1 = little-endian, 0 = unknown */
+#ifdef ARCH_IS_BIG_ENDIAN
+#  define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1)
+#else
+#  define BYTE_ORDER 0
+#endif
+
+#define T_MASK ((md5_word_t)~0)
+#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87)
+#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9)
+#define T3    0x242070db
+#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111)
+#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050)
+#define T6    0x4787c62a
+#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec)
+#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe)
+#define T9    0x698098d8
+#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850)
+#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e)
+#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841)
+#define T13    0x6b901122
+#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c)
+#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71)
+#define T16    0x49b40821
+#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d)
+#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf)
+#define T19    0x265e5a51
+#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855)
+#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2)
+#define T22    0x02441453
+#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e)
+#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437)
+#define T25    0x21e1cde6
+#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829)
+#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278)
+#define T28    0x455a14ed
+#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa)
+#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07)
+#define T31    0x676f02d9
+#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375)
+#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd)
+#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e)
+#define T35    0x6d9d6122
+#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3)
+#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb)
+#define T38    0x4bdecfa9
+#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f)
+#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f)
+#define T41    0x289b7ec6
+#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805)
+#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a)
+#define T44    0x04881d05
+#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6)
+#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a)
+#define T47    0x1fa27cf8
+#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a)
+#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb)
+#define T50    0x432aff97
+#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58)
+#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6)
+#define T53    0x655b59c3
+#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d)
+#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82)
+#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e)
+#define T57    0x6fa87e4f
+#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f)
+#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb)
+#define T60    0x4e0811a1
+#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d)
+#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca)
+#define T63    0x2ad7d2bb
+#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e)
+
+
+static void
+md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/)
+{
+    md5_word_t
+	a = pms->abcd[0], b = pms->abcd[1],
+	c = pms->abcd[2], d = pms->abcd[3];
+    md5_word_t t;
+#if BYTE_ORDER > 0
+    /* Define storage only for big-endian CPUs. */
+    md5_word_t X[16];
+#else
+    /* Define storage for little-endian or both types of CPUs. */
+    md5_word_t xbuf[16];
+    const md5_word_t *X;
+#endif
+
+    {
+#if BYTE_ORDER == 0
+	/*
+	 * Determine dynamically whether this is a big-endian or
+	 * little-endian machine, since we can use a more efficient
+	 * algorithm on the latter.
+	 */
+	static const int w = 1;
+
+	if (*((const md5_byte_t *)&w)) /* dynamic little-endian */
+#endif
+#if BYTE_ORDER <= 0		/* little-endian */
+	{
+	    /*
+	     * On little-endian machines, we can process properly aligned
+	     * data without copying it.
+	     */
+	    if (!((data - (const md5_byte_t *)0) & 3)) {
+		/* data are properly aligned */
+		X = (const md5_word_t *)data;
+	    } else {
+		/* not aligned */
+		memcpy(xbuf, data, 64);
+		X = xbuf;
+	    }
+	}
+#endif
+#if BYTE_ORDER == 0
+	else			/* dynamic big-endian */
+#endif
+#if BYTE_ORDER >= 0		/* big-endian */
+	{
+	    /*
+	     * On big-endian machines, we must arrange the bytes in the
+	     * right order.
+	     */
+	    const md5_byte_t *xp = data;
+	    int i;
+
+#  if BYTE_ORDER == 0
+	    X = xbuf;		/* (dynamic only) */
+#  else
+#    define xbuf X		/* (static only) */
+#  endif
+	    for (i = 0; i < 16; ++i, xp += 4)
+		xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24);
+	}
+#endif
+    }
+
+#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
+
+    /* Round 1. */
+    /* Let [abcd k s i] denote the operation
+       a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */
+#define F(x, y, z) (((x) & (y)) | (~(x) & (z)))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + F(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+    /* Do the following 16 operations. */
+    SET(a, b, c, d,  0,  7,  T1);
+    SET(d, a, b, c,  1, 12,  T2);
+    SET(c, d, a, b,  2, 17,  T3);
+    SET(b, c, d, a,  3, 22,  T4);
+    SET(a, b, c, d,  4,  7,  T5);
+    SET(d, a, b, c,  5, 12,  T6);
+    SET(c, d, a, b,  6, 17,  T7);
+    SET(b, c, d, a,  7, 22,  T8);
+    SET(a, b, c, d,  8,  7,  T9);
+    SET(d, a, b, c,  9, 12, T10);
+    SET(c, d, a, b, 10, 17, T11);
+    SET(b, c, d, a, 11, 22, T12);
+    SET(a, b, c, d, 12,  7, T13);
+    SET(d, a, b, c, 13, 12, T14);
+    SET(c, d, a, b, 14, 17, T15);
+    SET(b, c, d, a, 15, 22, T16);
+#undef SET
+
+     /* Round 2. */
+     /* Let [abcd k s i] denote the operation
+          a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */
+#define G(x, y, z) (((x) & (z)) | ((y) & ~(z)))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + G(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+     /* Do the following 16 operations. */
+    SET(a, b, c, d,  1,  5, T17);
+    SET(d, a, b, c,  6,  9, T18);
+    SET(c, d, a, b, 11, 14, T19);
+    SET(b, c, d, a,  0, 20, T20);
+    SET(a, b, c, d,  5,  5, T21);
+    SET(d, a, b, c, 10,  9, T22);
+    SET(c, d, a, b, 15, 14, T23);
+    SET(b, c, d, a,  4, 20, T24);
+    SET(a, b, c, d,  9,  5, T25);
+    SET(d, a, b, c, 14,  9, T26);
+    SET(c, d, a, b,  3, 14, T27);
+    SET(b, c, d, a,  8, 20, T28);
+    SET(a, b, c, d, 13,  5, T29);
+    SET(d, a, b, c,  2,  9, T30);
+    SET(c, d, a, b,  7, 14, T31);
+    SET(b, c, d, a, 12, 20, T32);
+#undef SET
+
+     /* Round 3. */
+     /* Let [abcd k s t] denote the operation
+          a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + H(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+     /* Do the following 16 operations. */
+    SET(a, b, c, d,  5,  4, T33);
+    SET(d, a, b, c,  8, 11, T34);
+    SET(c, d, a, b, 11, 16, T35);
+    SET(b, c, d, a, 14, 23, T36);
+    SET(a, b, c, d,  1,  4, T37);
+    SET(d, a, b, c,  4, 11, T38);
+    SET(c, d, a, b,  7, 16, T39);
+    SET(b, c, d, a, 10, 23, T40);
+    SET(a, b, c, d, 13,  4, T41);
+    SET(d, a, b, c,  0, 11, T42);
+    SET(c, d, a, b,  3, 16, T43);
+    SET(b, c, d, a,  6, 23, T44);
+    SET(a, b, c, d,  9,  4, T45);
+    SET(d, a, b, c, 12, 11, T46);
+    SET(c, d, a, b, 15, 16, T47);
+    SET(b, c, d, a,  2, 23, T48);
+#undef SET
+
+     /* Round 4. */
+     /* Let [abcd k s t] denote the operation
+          a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */
+#define I(x, y, z) ((y) ^ ((x) | ~(z)))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + I(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+     /* Do the following 16 operations. */
+    SET(a, b, c, d,  0,  6, T49);
+    SET(d, a, b, c,  7, 10, T50);
+    SET(c, d, a, b, 14, 15, T51);
+    SET(b, c, d, a,  5, 21, T52);
+    SET(a, b, c, d, 12,  6, T53);
+    SET(d, a, b, c,  3, 10, T54);
+    SET(c, d, a, b, 10, 15, T55);
+    SET(b, c, d, a,  1, 21, T56);
+    SET(a, b, c, d,  8,  6, T57);
+    SET(d, a, b, c, 15, 10, T58);
+    SET(c, d, a, b,  6, 15, T59);
+    SET(b, c, d, a, 13, 21, T60);
+    SET(a, b, c, d,  4,  6, T61);
+    SET(d, a, b, c, 11, 10, T62);
+    SET(c, d, a, b,  2, 15, T63);
+    SET(b, c, d, a,  9, 21, T64);
+#undef SET
+
+     /* Then perform the following additions. (That is increment each
+        of the four registers by the value it had before this block
+        was started.) */
+    pms->abcd[0] += a;
+    pms->abcd[1] += b;
+    pms->abcd[2] += c;
+    pms->abcd[3] += d;
+}
+
+void
+md5_init(md5_state_t *pms)
+{
+    pms->count[0] = pms->count[1] = 0;
+    pms->abcd[0] = 0x67452301;
+    pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476;
+    pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301;
+    pms->abcd[3] = 0x10325476;
+}
+
+void
+md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes)
+{
+    const md5_byte_t *p = data;
+    int left = nbytes;
+    int offset = (pms->count[0] >> 3) & 63;
+    md5_word_t nbits = (md5_word_t)(nbytes << 3);
+
+    if (nbytes <= 0)
+	return;
+
+    /* Update the message length. */
+    pms->count[1] += nbytes >> 29;
+    pms->count[0] += nbits;
+    if (pms->count[0] < nbits)
+	pms->count[1]++;
+
+    /* Process an initial partial block. */
+    if (offset) {
+	int copy = (offset + nbytes > 64 ? 64 - offset : nbytes);
+
+	memcpy(pms->buf + offset, p, copy);
+	if (offset + copy < 64)
+	    return;
+	p += copy;
+	left -= copy;
+	md5_process(pms, pms->buf);
+    }
+
+    /* Process full blocks. */
+    for (; left >= 64; p += 64, left -= 64)
+	md5_process(pms, p);
+
+    /* Process a final partial block. */
+    if (left)
+	memcpy(pms->buf, p, left);
+}
+
+void
+md5_finish(md5_state_t *pms, md5_byte_t digest[16])
+{
+    static const md5_byte_t pad[64] = {
+	0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+    };
+    md5_byte_t data[8];
+    int i;
+
+    /* Save the length before padding. */
+    for (i = 0; i < 8; ++i)
+	data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3));
+    /* Pad to 56 bytes mod 64. */
+    md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1);
+    /* Append the length. */
+    md5_append(pms, data, 8);
+    for (i = 0; i < 16; ++i)
+	digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3));
+}
diff --git a/OrthancFramework/Resources/ThirdParty/md5/md5.h b/OrthancFramework/Resources/ThirdParty/md5/md5.h
new file mode 100644
index 0000000..698c995
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/md5/md5.h
@@ -0,0 +1,91 @@
+/*
+  Copyright (C) 1999, 2002 Aladdin Enterprises.  All rights reserved.
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  L. Peter Deutsch
+  ghost@aladdin.com
+
+ */
+/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */
+/*
+  Independent implementation of MD5 (RFC 1321).
+
+  This code implements the MD5 Algorithm defined in RFC 1321, whose
+  text is available at
+	http://www.ietf.org/rfc/rfc1321.txt
+  The code is derived from the text of the RFC, including the test suite
+  (section A.5) but excluding the rest of Appendix A.  It does not include
+  any code or documentation that is identified in the RFC as being
+  copyrighted.
+
+  The original and principal author of md5.h is L. Peter Deutsch
+  .  Other authors are noted in the change history
+  that follows (in reverse chronological order):
+
+  2002-04-13 lpd Removed support for non-ANSI compilers; removed
+	references to Ghostscript; clarified derivation from RFC 1321;
+	now handles byte order either statically or dynamically.
+  1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
+  1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5);
+	added conditionalization for C++ compilation from Martin
+	Purschke .
+  1999-05-03 lpd Original version.
+ */
+
+#ifndef md5_INCLUDED
+#  define md5_INCLUDED
+
+/*
+ * This package supports both compile-time and run-time determination of CPU
+ * byte order.  If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be
+ * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is
+ * defined as non-zero, the code will be compiled to run only on big-endian
+ * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to
+ * run on either big- or little-endian CPUs, but will run slightly less
+ * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined.
+ */
+
+typedef unsigned char md5_byte_t; /* 8-bit byte */
+typedef unsigned int md5_word_t; /* 32-bit word */
+
+/* Define the state of the MD5 Algorithm. */
+typedef struct md5_state_s {
+    md5_word_t count[2];	/* message length in bits, lsw first */
+    md5_word_t abcd[4];		/* digest buffer */
+    md5_byte_t buf[64];		/* accumulate block */
+} md5_state_t;
+
+#ifdef __cplusplus
+extern "C" 
+{
+#endif
+
+/* Initialize the algorithm. */
+void md5_init(md5_state_t *pms);
+
+/* Append a string to the message. */
+void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes);
+
+/* Finish the message and return the digest. */
+void md5_finish(md5_state_t *pms, md5_byte_t digest[16]);
+
+#ifdef __cplusplus
+}  /* end extern "C" */
+#endif
+
+#endif /* md5_INCLUDED */
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/NOTES b/OrthancFramework/Resources/ThirdParty/minizip/NOTES
new file mode 100644
index 0000000..db6766a
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/NOTES
@@ -0,0 +1,2 @@
+These files come from the "contrib/minizip" directory in zlib 1.3+.
+It was last synced on this commit: https://github.com/madler/zlib/commit/73331a6a0481067628f065ffe87bb1d8f787d10c.
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/crypt.h b/OrthancFramework/Resources/ThirdParty/minizip/crypt.h
new file mode 100644
index 0000000..f4b93b7
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/crypt.h
@@ -0,0 +1,128 @@
+/* crypt.h -- base code for crypt/uncrypt ZIPfile
+
+
+   Version 1.01e, February 12th, 2005
+
+   Copyright (C) 1998-2005 Gilles Vollant
+
+   This code is a modified version of crypting code in Infozip distribution
+
+   The encryption/decryption parts of this source code (as opposed to the
+   non-echoing password parts) were originally written in Europe.  The
+   whole source package can be freely distributed, including from the USA.
+   (Prior to January 2000, re-export from the US was a violation of US law.)
+
+   This encryption code is a direct transcription of the algorithm from
+   Roger Schlafly, described by Phil Katz in the file appnote.txt.  This
+   file (appnote.txt) is distributed with the PKZIP program (even in the
+   version without encryption capabilities).
+
+   If you don't need crypting in your application, just define symbols
+   NOCRYPT and NOUNCRYPT.
+
+   This code support the "Traditional PKWARE Encryption".
+
+   The new AES encryption added on Zip format by Winzip (see the page
+   http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong
+   Encryption is not supported.
+*/
+
+#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8))
+
+/***********************************************************************
+ * Return the next byte in the pseudo-random sequence
+ */
+static int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab) {
+    unsigned temp;  /* POTENTIAL BUG:  temp*(temp^1) may overflow in an
+                     * unpredictable manner on 16-bit systems; not a problem
+                     * with any known compiler so far, though */
+
+    (void)pcrc_32_tab;
+    temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2;
+    return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);
+}
+
+/***********************************************************************
+ * Update the encryption keys with the next byte of plain text
+ */
+static int update_keys(unsigned long* pkeys, const z_crc_t* pcrc_32_tab, int c) {
+    (*(pkeys+0)) = CRC32((*(pkeys+0)), c);
+    (*(pkeys+1)) += (*(pkeys+0)) & 0xff;
+    (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1;
+    {
+      register int keyshift = (int)((*(pkeys+1)) >> 24);
+      (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift);
+    }
+    return c;
+}
+
+
+/***********************************************************************
+ * Initialize the encryption keys and the random header according to
+ * the given password.
+ */
+static void init_keys(const char* passwd, unsigned long* pkeys, const z_crc_t* pcrc_32_tab) {
+    *(pkeys+0) = 305419896L;
+    *(pkeys+1) = 591751049L;
+    *(pkeys+2) = 878082192L;
+    while (*passwd != '\0') {
+        update_keys(pkeys,pcrc_32_tab,(int)*passwd);
+        passwd++;
+    }
+}
+
+#define zdecode(pkeys,pcrc_32_tab,c) \
+    (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab)))
+
+#define zencode(pkeys,pcrc_32_tab,c,t) \
+    (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), (Byte)t^(c))
+
+#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED
+
+#define RAND_HEAD_LEN  12
+   /* "last resort" source for second part of crypt seed pattern */
+#  ifndef ZCR_SEED2
+#    define ZCR_SEED2 3141592654UL      /* use PI as default pattern */
+#  endif
+
+static unsigned crypthead(const char* passwd,       /* password string */
+                          unsigned char* buf,       /* where to write header */
+                          int bufSize,
+                          unsigned long* pkeys,
+                          const z_crc_t* pcrc_32_tab,
+                          unsigned long crcForCrypting) {
+    unsigned n;                  /* index in random header */
+    int t;                       /* temporary */
+    int c;                       /* random byte */
+    unsigned char header[RAND_HEAD_LEN-2]; /* random header */
+    static unsigned calls = 0;   /* ensure different random header each time */
+
+    if (bufSize> 7) & 0xff;
+        header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t);
+    }
+    /* Encrypt random header (last two bytes is high word of crc) */
+    init_keys(passwd, pkeys, pcrc_32_tab);
+    for (n = 0; n < RAND_HEAD_LEN-2; n++)
+    {
+        buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t);
+    }
+    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t);
+    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t);
+    return n;
+}
+
+#endif
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/ioapi.c b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.c
new file mode 100644
index 0000000..782d324
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.c
@@ -0,0 +1,231 @@
+/* ioapi.h -- IO base function header for compress/uncompress .zip
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+*/
+
+#if defined(_WIN32) && (!(defined(_CRT_SECURE_NO_WARNINGS)))
+        #define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#if defined(__APPLE__) || defined(IOAPI_NO_64) || defined(__HAIKU__) || defined(MINIZIP_FOPEN_NO_64)
+// In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions
+#define FOPEN_FUNC(filename, mode) fopen(filename, mode)
+#define FTELLO_FUNC(stream) ftello(stream)
+#define FSEEKO_FUNC(stream, offset, origin) fseeko(stream, offset, origin)
+#else
+#define FOPEN_FUNC(filename, mode) fopen64(filename, mode)
+#define FTELLO_FUNC(stream) ftello64(stream)
+#define FSEEKO_FUNC(stream, offset, origin) fseeko64(stream, offset, origin)
+#endif
+
+
+#include "ioapi.h"
+
+voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc, const void*filename, int mode) {
+    if (pfilefunc->zfile_func64.zopen64_file != NULL)
+        return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode);
+    else
+    {
+        return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode);
+    }
+}
+
+long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin) {
+    if (pfilefunc->zfile_func64.zseek64_file != NULL)
+        return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin);
+    else
+    {
+        uLong offsetTruncated = (uLong)offset;
+        if (offsetTruncated != offset)
+            return -1;
+        else
+            return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin);
+    }
+}
+
+ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc, voidpf filestream) {
+    if (pfilefunc->zfile_func64.zseek64_file != NULL)
+        return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream);
+    else
+    {
+        uLong tell_uLong = (uLong)(*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);
+        if ((tell_uLong) == MAXU32)
+            return (ZPOS64_T)-1;
+        else
+            return tell_uLong;
+    }
+}
+
+void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32, const zlib_filefunc_def* p_filefunc32) {
+    p_filefunc64_32->zfile_func64.zopen64_file = NULL;
+    p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file;
+    p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file;
+    p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file;
+    p_filefunc64_32->zfile_func64.ztell64_file = NULL;
+    p_filefunc64_32->zfile_func64.zseek64_file = NULL;
+    p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file;
+    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
+    p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque;
+    p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file;
+    p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file;
+}
+
+
+
+static voidpf ZCALLBACK fopen_file_func(voidpf opaque, const char* filename, int mode) {
+    FILE* file = NULL;
+    const char* mode_fopen = NULL;
+    (void)opaque;
+    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
+        mode_fopen = "rb";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
+        mode_fopen = "r+b";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
+        mode_fopen = "wb";
+
+    if ((filename!=NULL) && (mode_fopen != NULL))
+        file = fopen(filename, mode_fopen);
+    return file;
+}
+
+static voidpf ZCALLBACK fopen64_file_func(voidpf opaque, const void* filename, int mode) {
+    FILE* file = NULL;
+    const char* mode_fopen = NULL;
+    (void)opaque;
+    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
+        mode_fopen = "rb";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
+        mode_fopen = "r+b";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
+        mode_fopen = "wb";
+
+    if ((filename!=NULL) && (mode_fopen != NULL))
+        file = FOPEN_FUNC((const char*)filename, mode_fopen);
+    return file;
+}
+
+
+static uLong ZCALLBACK fread_file_func(voidpf opaque, voidpf stream, void* buf, uLong size) {
+    uLong ret;
+    (void)opaque;
+    ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream);
+    return ret;
+}
+
+static uLong ZCALLBACK fwrite_file_func(voidpf opaque, voidpf stream, const void* buf, uLong size) {
+    uLong ret;
+    (void)opaque;
+    ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream);
+    return ret;
+}
+
+static long ZCALLBACK ftell_file_func(voidpf opaque, voidpf stream) {
+    long ret;
+    (void)opaque;
+    ret = ftell((FILE *)stream);
+    return ret;
+}
+
+
+static ZPOS64_T ZCALLBACK ftell64_file_func(voidpf opaque, voidpf stream) {
+    ZPOS64_T ret;
+    (void)opaque;
+    ret = (ZPOS64_T)FTELLO_FUNC((FILE *)stream);
+    return ret;
+}
+
+static long ZCALLBACK fseek_file_func(voidpf opaque, voidpf stream, uLong offset, int origin) {
+    int fseek_origin=0;
+    long ret;
+    (void)opaque;
+    switch (origin)
+    {
+    case ZLIB_FILEFUNC_SEEK_CUR :
+        fseek_origin = SEEK_CUR;
+        break;
+    case ZLIB_FILEFUNC_SEEK_END :
+        fseek_origin = SEEK_END;
+        break;
+    case ZLIB_FILEFUNC_SEEK_SET :
+        fseek_origin = SEEK_SET;
+        break;
+    default: return -1;
+    }
+    ret = 0;
+    if (fseek((FILE *)stream, (long)offset, fseek_origin) != 0)
+        ret = -1;
+    return ret;
+}
+
+static long ZCALLBACK fseek64_file_func(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) {
+    int fseek_origin=0;
+    long ret;
+    (void)opaque;
+    switch (origin)
+    {
+    case ZLIB_FILEFUNC_SEEK_CUR :
+        fseek_origin = SEEK_CUR;
+        break;
+    case ZLIB_FILEFUNC_SEEK_END :
+        fseek_origin = SEEK_END;
+        break;
+    case ZLIB_FILEFUNC_SEEK_SET :
+        fseek_origin = SEEK_SET;
+        break;
+    default: return -1;
+    }
+    ret = 0;
+
+    if(FSEEKO_FUNC((FILE *)stream, (z_off64_t)offset, fseek_origin) != 0)
+                        ret = -1;
+
+    return ret;
+}
+
+
+static int ZCALLBACK fclose_file_func(voidpf opaque, voidpf stream) {
+    int ret;
+    (void)opaque;
+    ret = fclose((FILE *)stream);
+    return ret;
+}
+
+static int ZCALLBACK ferror_file_func(voidpf opaque, voidpf stream) {
+    int ret;
+    (void)opaque;
+    ret = ferror((FILE *)stream);
+    return ret;
+}
+
+void fill_fopen_filefunc(zlib_filefunc_def* pzlib_filefunc_def) {
+    pzlib_filefunc_def->zopen_file = fopen_file_func;
+    pzlib_filefunc_def->zread_file = fread_file_func;
+    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
+    pzlib_filefunc_def->ztell_file = ftell_file_func;
+    pzlib_filefunc_def->zseek_file = fseek_file_func;
+    pzlib_filefunc_def->zclose_file = fclose_file_func;
+    pzlib_filefunc_def->zerror_file = ferror_file_func;
+    pzlib_filefunc_def->opaque = NULL;
+}
+
+void fill_fopen64_filefunc(zlib_filefunc64_def* pzlib_filefunc_def) {
+    pzlib_filefunc_def->zopen64_file = fopen64_file_func;
+    pzlib_filefunc_def->zread_file = fread_file_func;
+    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
+    pzlib_filefunc_def->ztell64_file = ftell64_file_func;
+    pzlib_filefunc_def->zseek64_file = fseek64_file_func;
+    pzlib_filefunc_def->zclose_file = fclose_file_func;
+    pzlib_filefunc_def->zerror_file = ferror_file_func;
+    pzlib_filefunc_def->opaque = NULL;
+}
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/ioapi.h b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.h
new file mode 100644
index 0000000..a2d2e6e
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.h
@@ -0,0 +1,210 @@
+/* ioapi.h -- IO base function header for compress/uncompress .zip
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+         Changes
+
+    Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this)
+    Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux.
+               More if/def section may be needed to support other platforms
+    Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows.
+                          (but you should use iowin32.c for windows instead)
+
+*/
+
+#ifndef _ZLIBIOAPI64_H
+#define _ZLIBIOAPI64_H
+
+#if (!defined(_WIN32)) && (!defined(WIN32)) && (!defined(__APPLE__))
+
+  // Linux needs this to support file operation on files larger then 4+GB
+  // But might need better if/def to select just the platforms that needs them.
+
+        #ifndef __USE_FILE_OFFSET64
+                #define __USE_FILE_OFFSET64
+        #endif
+        #ifndef __USE_LARGEFILE64
+                #define __USE_LARGEFILE64
+        #endif
+        #ifndef _LARGEFILE64_SOURCE
+                #define _LARGEFILE64_SOURCE
+        #endif
+        #ifndef _FILE_OFFSET_BIT
+                #define _FILE_OFFSET_BIT 64
+        #endif
+
+#endif
+
+#include 
+#include 
+#include "zlib.h"
+
+#if defined(USE_FILE32API)
+#define fopen64 fopen
+#define ftello64 ftell
+#define fseeko64 fseek
+#else
+#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__HAIKU__) || defined(MINIZIP_FOPEN_NO_64)
+#define fopen64 fopen
+#define ftello64 ftello
+#define fseeko64 fseeko
+#endif
+#ifdef _MSC_VER
+ #define fopen64 fopen
+ #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC)))
+  #define ftello64 _ftelli64
+  #define fseeko64 _fseeki64
+ #else // old MSC
+  #define ftello64 ftell
+  #define fseeko64 fseek
+ #endif
+#endif
+#endif
+
+/*
+#ifndef ZPOS64_T
+  #ifdef _WIN32
+                #define ZPOS64_T fpos_t
+  #else
+    #include 
+    #define ZPOS64_T uint64_t
+  #endif
+#endif
+*/
+
+#ifdef HAVE_MINIZIP64_CONF_H
+#include "mz64conf.h"
+#endif
+
+/* a type chosen by DEFINE */
+#ifdef HAVE_64BIT_INT_CUSTOM
+typedef  64BIT_INT_CUSTOM_TYPE ZPOS64_T;
+#else
+#ifdef HAS_STDINT_H
+#include "stdint.h"
+typedef uint64_t ZPOS64_T;
+#else
+
+
+
+#if defined(_MSC_VER) || defined(__BORLANDC__)
+typedef unsigned __int64 ZPOS64_T;
+#else
+typedef unsigned long long int ZPOS64_T;
+#endif
+#endif
+#endif
+
+/* Maximum unsigned 32-bit value used as placeholder for zip64 */
+#ifndef MAXU32
+#define MAXU32 (0xffffffff)
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+#define ZLIB_FILEFUNC_SEEK_CUR (1)
+#define ZLIB_FILEFUNC_SEEK_END (2)
+#define ZLIB_FILEFUNC_SEEK_SET (0)
+
+#define ZLIB_FILEFUNC_MODE_READ      (1)
+#define ZLIB_FILEFUNC_MODE_WRITE     (2)
+#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3)
+
+#define ZLIB_FILEFUNC_MODE_EXISTING (4)
+#define ZLIB_FILEFUNC_MODE_CREATE   (8)
+
+
+#ifndef ZCALLBACK
+ #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK)
+   #define ZCALLBACK CALLBACK
+ #else
+   #define ZCALLBACK
+ #endif
+#endif
+
+
+
+
+typedef voidpf   (ZCALLBACK *open_file_func)      (voidpf opaque, const char* filename, int mode);
+typedef uLong    (ZCALLBACK *read_file_func)      (voidpf opaque, voidpf stream, void* buf, uLong size);
+typedef uLong    (ZCALLBACK *write_file_func)     (voidpf opaque, voidpf stream, const void* buf, uLong size);
+typedef int      (ZCALLBACK *close_file_func)     (voidpf opaque, voidpf stream);
+typedef int      (ZCALLBACK *testerror_file_func) (voidpf opaque, voidpf stream);
+
+typedef long     (ZCALLBACK *tell_file_func)      (voidpf opaque, voidpf stream);
+typedef long     (ZCALLBACK *seek_file_func)      (voidpf opaque, voidpf stream, uLong offset, int origin);
+
+
+/* here is the "old" 32 bits structure */
+typedef struct zlib_filefunc_def_s
+{
+    open_file_func      zopen_file;
+    read_file_func      zread_file;
+    write_file_func     zwrite_file;
+    tell_file_func      ztell_file;
+    seek_file_func      zseek_file;
+    close_file_func     zclose_file;
+    testerror_file_func zerror_file;
+    voidpf              opaque;
+} zlib_filefunc_def;
+
+typedef ZPOS64_T (ZCALLBACK *tell64_file_func)    (voidpf opaque, voidpf stream);
+typedef long     (ZCALLBACK *seek64_file_func)    (voidpf opaque, voidpf stream, ZPOS64_T offset, int origin);
+typedef voidpf   (ZCALLBACK *open64_file_func)    (voidpf opaque, const void* filename, int mode);
+
+typedef struct zlib_filefunc64_def_s
+{
+    open64_file_func    zopen64_file;
+    read_file_func      zread_file;
+    write_file_func     zwrite_file;
+    tell64_file_func    ztell64_file;
+    seek64_file_func    zseek64_file;
+    close_file_func     zclose_file;
+    testerror_file_func zerror_file;
+    voidpf              opaque;
+} zlib_filefunc64_def;
+
+void fill_fopen64_filefunc(zlib_filefunc64_def* pzlib_filefunc_def);
+void fill_fopen_filefunc(zlib_filefunc_def* pzlib_filefunc_def);
+
+/* now internal definition, only for zip.c and unzip.h */
+typedef struct zlib_filefunc64_32_def_s
+{
+    zlib_filefunc64_def zfile_func64;
+    open_file_func      zopen32_file;
+    tell_file_func      ztell32_file;
+    seek_file_func      zseek32_file;
+} zlib_filefunc64_32_def;
+
+
+#define ZREAD64(filefunc,filestream,buf,size)     ((*((filefunc).zfile_func64.zread_file))   ((filefunc).zfile_func64.opaque,filestream,buf,size))
+#define ZWRITE64(filefunc,filestream,buf,size)    ((*((filefunc).zfile_func64.zwrite_file))  ((filefunc).zfile_func64.opaque,filestream,buf,size))
+//#define ZTELL64(filefunc,filestream)            ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream))
+//#define ZSEEK64(filefunc,filestream,pos,mode)   ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode))
+#define ZCLOSE64(filefunc,filestream)             ((*((filefunc).zfile_func64.zclose_file))  ((filefunc).zfile_func64.opaque,filestream))
+#define ZERROR64(filefunc,filestream)             ((*((filefunc).zfile_func64.zerror_file))  ((filefunc).zfile_func64.opaque,filestream))
+
+voidpf call_zopen64(const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode);
+long call_zseek64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin);
+ZPOS64_T call_ztell64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream);
+
+void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32);
+
+#define ZOPEN64(filefunc,filename,mode)         (call_zopen64((&(filefunc)),(filename),(mode)))
+#define ZTELL64(filefunc,filestream)            (call_ztell64((&(filefunc)),(filestream)))
+#define ZSEEK64(filefunc,filestream,pos,mode)   (call_zseek64((&(filefunc)),(filestream),(pos),(mode)))
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/unzip.c b/OrthancFramework/Resources/ThirdParty/minizip/unzip.c
new file mode 100644
index 0000000..ed763f8
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/unzip.c
@@ -0,0 +1,1985 @@
+/* unzip.c -- IO for uncompress .zip files using zlib
+   Version 1.1, February 14h, 2010
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications of Unzip for Zip64
+         Copyright (C) 2007-2008 Even Rouault
+
+         Modifications for Zip64 support on both zip and unzip
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+
+  ------------------------------------------------------------------------------------
+  Decryption code comes from crypt.c by Info-ZIP but has been greatly reduced in terms of
+  compatibility with older software. The following is from the original crypt.c.
+  Code woven in by Terry Thorsen 1/2003.
+
+  Copyright (c) 1990-2000 Info-ZIP.  All rights reserved.
+
+  See the accompanying file LICENSE, version 2000-Apr-09 or later
+  (the contents of which are also included in zip.h) for terms of use.
+  If, for some reason, all these files are missing, the Info-ZIP license
+  also may be found at:  ftp://ftp.info-zip.org/pub/infozip/license.html
+
+        crypt.c (full version) by Info-ZIP.      Last revised:  [see crypt.h]
+
+  The encryption/decryption parts of this source code (as opposed to the
+  non-echoing password parts) were originally written in Europe.  The
+  whole source package can be freely distributed, including from the USA.
+  (Prior to January 2000, re-export from the US was a violation of US law.)
+
+        This encryption code is a direct transcription of the algorithm from
+  Roger Schlafly, described by Phil Katz in the file appnote.txt.  This
+  file (appnote.txt) is distributed with the PKZIP program (even in the
+  version without encryption capabilities).
+
+        ------------------------------------------------------------------------------------
+
+        Changes in unzip.c
+
+        2007-2008 - Even Rouault - Addition of cpl_unzGetCurrentFileZStreamPos
+  2007-2008 - Even Rouault - Decoration of symbol names unz* -> cpl_unz*
+  2007-2008 - Even Rouault - Remove old C style function prototypes
+  2007-2008 - Even Rouault - Add unzip support for ZIP64
+
+        Copyright (C) 2007-2008 Even Rouault
+
+
+  Oct-2009 - Mathias Svensson - Removed cpl_* from symbol names (Even Rouault added them but since this is now moved to a new project (minizip64) I renamed them again).
+  Oct-2009 - Mathias Svensson - Fixed problem if uncompressed size was > 4G and compressed size was <4G
+                                should only read the compressed/uncompressed size from the Zip64 format if
+                                the size from normal header was 0xFFFFFFFF
+  Oct-2009 - Mathias Svensson - Applied some bug fixes from patches received from Gilles Vollant
+  Oct-2009 - Mathias Svensson - Applied support to unzip files with compression method BZIP2 (bzip2 lib is required)
+                                Patch created by Daniel Borca
+
+  Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
+
+  Copyright (C) 1998 - 2010 Gilles Vollant, Even Rouault, Mathias Svensson
+
+*/
+
+
+#include 
+#include 
+#include 
+
+#ifndef NOUNCRYPT
+        #define NOUNCRYPT
+#endif
+
+#include "zlib.h"
+#include "unzip.h"
+
+#ifdef STDC
+#  include 
+#endif
+#ifdef NO_ERRNO_H
+    extern int errno;
+#else
+#   include 
+#endif
+
+
+#ifndef local
+#  define local static
+#endif
+/* compile with -Dlocal if your debugger can't find static symbols */
+
+
+#ifndef CASESENSITIVITYDEFAULT_NO
+#  if !defined(unix) && !defined(CASESENSITIVITYDEFAULT_YES)
+#    define CASESENSITIVITYDEFAULT_NO
+#  endif
+#endif
+
+
+#ifndef UNZ_BUFSIZE
+#define UNZ_BUFSIZE (16384)
+#endif
+
+#ifndef UNZ_MAXFILENAMEINZIP
+#define UNZ_MAXFILENAMEINZIP (256)
+#endif
+
+#ifndef ALLOC
+# define ALLOC(size) (malloc(size))
+#endif
+
+#define SIZECENTRALDIRITEM (0x2e)
+#define SIZEZIPLOCALHEADER (0x1e)
+
+
+const char unz_copyright[] =
+   " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll";
+
+/* unz_file_info_interntal contain internal info about a file in zipfile*/
+typedef struct unz_file_info64_internal_s
+{
+    ZPOS64_T offset_curfile;/* relative offset of local header 8 bytes */
+} unz_file_info64_internal;
+
+
+/* file_in_zip_read_info_s contain internal information about a file in zipfile,
+    when reading and decompress it */
+typedef struct
+{
+    char  *read_buffer;         /* internal buffer for compressed data */
+    z_stream stream;            /* zLib stream structure for inflate */
+
+#ifdef HAVE_BZIP2
+    bz_stream bstream;          /* bzLib stream structure for bziped */
+#endif
+
+    ZPOS64_T pos_in_zipfile;       /* position in byte on the zipfile, for fseek*/
+    uLong stream_initialised;   /* flag set if stream structure is initialised*/
+
+    ZPOS64_T offset_local_extrafield;/* offset of the local extra field */
+    uInt  size_local_extrafield;/* size of the local extra field */
+    ZPOS64_T pos_local_extrafield;   /* position in the local extra field in read*/
+    ZPOS64_T total_out_64;
+
+    uLong crc32;                /* crc32 of all data uncompressed */
+    uLong crc32_wait;           /* crc32 we must obtain after decompress all */
+    ZPOS64_T rest_read_compressed; /* number of byte to be decompressed */
+    ZPOS64_T rest_read_uncompressed;/*number of byte to be obtained after decomp*/
+    zlib_filefunc64_32_def z_filefunc;
+    voidpf filestream;        /* io structure of the zipfile */
+    uLong compression_method;   /* compression method (0==store) */
+    ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
+    int   raw;
+} file_in_zip64_read_info_s;
+
+
+/* unz64_s contain internal information about the zipfile
+*/
+typedef struct
+{
+    zlib_filefunc64_32_def z_filefunc;
+    int is64bitOpenFunction;
+    voidpf filestream;        /* io structure of the zipfile */
+    unz_global_info64 gi;       /* public global information */
+    ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
+    ZPOS64_T num_file;             /* number of the current file in the zipfile*/
+    ZPOS64_T pos_in_central_dir;   /* pos of the current file in the central dir*/
+    ZPOS64_T current_file_ok;      /* flag about the usability of the current file*/
+    ZPOS64_T central_pos;          /* position of the beginning of the central dir*/
+
+    ZPOS64_T size_central_dir;     /* size of the central directory  */
+    ZPOS64_T offset_central_dir;   /* offset of start of central directory with
+                                   respect to the starting disk number */
+
+    unz_file_info64 cur_file_info; /* public info about the current file in zip*/
+    unz_file_info64_internal cur_file_info_internal; /* private info about it*/
+    file_in_zip64_read_info_s* pfile_in_zip_read; /* structure about the current
+                                        file if we are decompressing it */
+    int encrypted;
+
+    int isZip64;
+
+#    ifndef NOUNCRYPT
+    unsigned long keys[3];     /* keys defining the pseudo-random sequence */
+    const z_crc_t* pcrc_32_tab;
+#    endif
+} unz64_s;
+
+
+#ifndef NOUNCRYPT
+#include "crypt.h"
+#endif
+
+
+/* ===========================================================================
+   Reads a long in LSB order from the given gz_stream. Sets
+*/
+
+local int unz64local_getShort(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+                              voidpf filestream,
+                              uLong *pX) {
+    unsigned char c[2];
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,2);
+    if (err==2)
+    {
+        *pX = c[0] | ((uLong)c[1] << 8);
+        return UNZ_OK;
+    }
+    else
+    {
+        *pX = 0;
+        if (ZERROR64(*pzlib_filefunc_def,filestream))
+            return UNZ_ERRNO;
+        else
+            return UNZ_EOF;
+    }
+}
+
+local int unz64local_getLong(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+                             voidpf filestream,
+                             uLong *pX) {
+    unsigned char c[4];
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,4);
+    if (err==4)
+    {
+        *pX = c[0] | ((uLong)c[1] << 8) | ((uLong)c[2] << 16) | ((uLong)c[3] << 24);
+        return UNZ_OK;
+    }
+    else
+    {
+        *pX = 0;
+        if (ZERROR64(*pzlib_filefunc_def,filestream))
+            return UNZ_ERRNO;
+        else
+            return UNZ_EOF;
+    }
+}
+
+
+local int unz64local_getLong64(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+                               voidpf filestream,
+                               ZPOS64_T *pX) {
+    unsigned char c[8];
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,c,8);
+    if (err==8)
+    {
+        *pX = c[0] | ((ZPOS64_T)c[1] << 8) | ((ZPOS64_T)c[2] << 16) | ((ZPOS64_T)c[3] << 24)
+            | ((ZPOS64_T)c[4] << 32) | ((ZPOS64_T)c[5] << 40) | ((ZPOS64_T)c[6] << 48) | ((ZPOS64_T)c[7] << 56);
+        return UNZ_OK;
+    }
+    else
+    {
+        *pX = 0;
+        if (ZERROR64(*pzlib_filefunc_def,filestream))
+            return UNZ_ERRNO;
+        else
+            return UNZ_EOF;
+    }
+}
+
+/* My own strcmpi / strcasecmp */
+local int strcmpcasenosensitive_internal(const char* fileName1, const char* fileName2) {
+    for (;;)
+    {
+        char c1=*(fileName1++);
+        char c2=*(fileName2++);
+        if ((c1>='a') && (c1<='z'))
+            c1 -= 0x20;
+        if ((c2>='a') && (c2<='z'))
+            c2 -= 0x20;
+        if (c1=='\0')
+            return ((c2=='\0') ? 0 : -1);
+        if (c2=='\0')
+            return 1;
+        if (c1c2)
+            return 1;
+    }
+}
+
+
+#ifdef  CASESENSITIVITYDEFAULT_NO
+#define CASESENSITIVITYDEFAULTVALUE 2
+#else
+#define CASESENSITIVITYDEFAULTVALUE 1
+#endif
+
+#ifndef STRCMPCASENOSENTIVEFUNCTION
+#define STRCMPCASENOSENTIVEFUNCTION strcmpcasenosensitive_internal
+#endif
+
+/*
+   Compare two filenames (fileName1,fileName2).
+   If iCaseSensitivity = 1, comparison is case sensitive (like strcmp)
+   If iCaseSensitivity = 2, comparison is not case sensitive (like strcmpi
+                                                                or strcasecmp)
+   If iCaseSensitivity = 0, case sensitivity is default of your operating system
+        (like 1 on Unix, 2 on Windows)
+
+*/
+extern int ZEXPORT unzStringFileNameCompare (const char*  fileName1,
+                                             const char*  fileName2,
+                                             int iCaseSensitivity) {
+    if (iCaseSensitivity==0)
+        iCaseSensitivity=CASESENSITIVITYDEFAULTVALUE;
+
+    if (iCaseSensitivity==1)
+        return strcmp(fileName1,fileName2);
+
+    return STRCMPCASENOSENTIVEFUNCTION(fileName1,fileName2);
+}
+
+#ifndef BUFREADCOMMENT
+#define BUFREADCOMMENT (0x400)
+#endif
+
+#ifndef CENTRALDIRINVALID
+#define CENTRALDIRINVALID ((ZPOS64_T)(-1))
+#endif
+
+/*
+  Locate the Central directory of a zipfile (at the end, just before
+    the global comment)
+*/
+local ZPOS64_T unz64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {
+    unsigned char* buf;
+    ZPOS64_T uSizeFile;
+    ZPOS64_T uBackRead;
+    ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+    ZPOS64_T uPosFound=CENTRALDIRINVALID;
+
+    if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+        return CENTRALDIRINVALID;
+
+
+    uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+    if (uMaxBack>uSizeFile)
+        uMaxBack = uSizeFile;
+
+    buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+    if (buf==NULL)
+        return CENTRALDIRINVALID;
+
+    uBackRead = 4;
+    while (uBackReaduMaxBack)
+            uBackRead = uMaxBack;
+        else
+            uBackRead+=BUFREADCOMMENT;
+        uReadPos = uSizeFile-uBackRead ;
+
+        uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+                     (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+        if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+            break;
+
+        if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+            break;
+
+        for (i=(int)uReadSize-3; (i--)>0;)
+            if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
+                ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
+            {
+                uPosFound = uReadPos+(unsigned)i;
+                break;
+            }
+
+        if (uPosFound!=CENTRALDIRINVALID)
+            break;
+    }
+    free(buf);
+    return uPosFound;
+}
+
+
+/*
+  Locate the Central directory 64 of a zipfile (at the end, just before
+    the global comment)
+*/
+local ZPOS64_T unz64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+                                             voidpf filestream) {
+    unsigned char* buf;
+    ZPOS64_T uSizeFile;
+    ZPOS64_T uBackRead;
+    ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+    ZPOS64_T uPosFound=CENTRALDIRINVALID;
+    uLong uL;
+                ZPOS64_T relativeOffset;
+
+    if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+        return CENTRALDIRINVALID;
+
+
+    uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+    if (uMaxBack>uSizeFile)
+        uMaxBack = uSizeFile;
+
+    buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+    if (buf==NULL)
+        return CENTRALDIRINVALID;
+
+    uBackRead = 4;
+    while (uBackReaduMaxBack)
+            uBackRead = uMaxBack;
+        else
+            uBackRead+=BUFREADCOMMENT;
+        uReadPos = uSizeFile-uBackRead ;
+
+        uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+                     (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+        if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+            break;
+
+        if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+            break;
+
+        for (i=(int)uReadSize-3; (i--)>0;)
+            if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
+                ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
+            {
+                uPosFound = uReadPos+(unsigned)i;
+                break;
+            }
+
+        if (uPosFound!=CENTRALDIRINVALID)
+            break;
+    }
+    free(buf);
+    if (uPosFound == CENTRALDIRINVALID)
+        return CENTRALDIRINVALID;
+
+    /* Zip64 end of central directory locator */
+    if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)
+        return CENTRALDIRINVALID;
+
+    /* the signature, already checked */
+    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+        return CENTRALDIRINVALID;
+
+    /* number of the disk with the start of the zip64 end of  central directory */
+    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+        return CENTRALDIRINVALID;
+    if (uL != 0)
+        return CENTRALDIRINVALID;
+
+    /* relative offset of the zip64 end of central directory record */
+    if (unz64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=UNZ_OK)
+        return CENTRALDIRINVALID;
+
+    /* total number of disks */
+    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+        return CENTRALDIRINVALID;
+    if (uL != 1)
+        return CENTRALDIRINVALID;
+
+    /* Goto end of central directory record */
+    if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)
+        return CENTRALDIRINVALID;
+
+     /* the signature */
+    if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+        return CENTRALDIRINVALID;
+
+    if (uL != 0x06064b50)
+        return CENTRALDIRINVALID;
+
+    return relativeOffset;
+}
+
+/*
+  Open a Zip file. path contain the full pathname (by example,
+     on a Windows NT computer "c:\\test\\zlib114.zip" or on an Unix computer
+     "zlib/zlib114.zip".
+     If the zipfile cannot be opened (file doesn't exist or in not valid), the
+       return value is NULL.
+     Else, the return value is a unzFile Handle, usable with other function
+       of this unzip package.
+*/
+local unzFile unzOpenInternal(const void *path,
+                              zlib_filefunc64_32_def* pzlib_filefunc64_32_def,
+                              int is64bitOpenFunction) {
+    unz64_s us;
+    unz64_s *s;
+    ZPOS64_T central_pos;
+    uLong   uL;
+
+    uLong number_disk;          /* number of the current dist, used for
+                                   spanning ZIP, unsupported, always 0*/
+    uLong number_disk_with_CD;  /* number the the disk with central dir, used
+                                   for spanning ZIP, unsupported, always 0*/
+    ZPOS64_T number_entry_CD;      /* total number of entries in
+                                   the central dir
+                                   (same than number_entry on nospan) */
+
+    int err=UNZ_OK;
+
+    if (unz_copyright[0]!=' ')
+        return NULL;
+
+    us.z_filefunc.zseek32_file = NULL;
+    us.z_filefunc.ztell32_file = NULL;
+    if (pzlib_filefunc64_32_def==NULL)
+        fill_fopen64_filefunc(&us.z_filefunc.zfile_func64);
+    else
+        us.z_filefunc = *pzlib_filefunc64_32_def;
+    us.is64bitOpenFunction = is64bitOpenFunction;
+
+
+
+    us.filestream = ZOPEN64(us.z_filefunc,
+                                                 path,
+                                                 ZLIB_FILEFUNC_MODE_READ |
+                                                 ZLIB_FILEFUNC_MODE_EXISTING);
+    if (us.filestream==NULL)
+        return NULL;
+
+    central_pos = unz64local_SearchCentralDir64(&us.z_filefunc,us.filestream);
+    if (central_pos!=CENTRALDIRINVALID)
+    {
+        uLong uS;
+        ZPOS64_T uL64;
+
+        us.isZip64 = 1;
+
+        if (ZSEEK64(us.z_filefunc, us.filestream,
+                                      central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+        err=UNZ_ERRNO;
+
+        /* the signature, already checked */
+        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* size of zip64 end of central directory record */
+        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&uL64)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* version made by */
+        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* version needed to extract */
+        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* number of this disk */
+        if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* number of the disk with the start of the central directory */
+        if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* total number of entries in the central directory on this disk */
+        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.gi.number_entry)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* total number of entries in the central directory */
+        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&number_entry_CD)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        if ((number_entry_CD!=us.gi.number_entry) ||
+            (number_disk_with_CD!=0) ||
+            (number_disk!=0))
+            err=UNZ_BADZIPFILE;
+
+        /* size of the central directory */
+        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.size_central_dir)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* offset of start of central directory with respect to the
+          starting disk number */
+        if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.offset_central_dir)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        us.gi.size_comment = 0;
+    }
+    else
+    {
+        central_pos = unz64local_SearchCentralDir(&us.z_filefunc,us.filestream);
+        if (central_pos==CENTRALDIRINVALID)
+            err=UNZ_ERRNO;
+
+        us.isZip64 = 0;
+
+        if (ZSEEK64(us.z_filefunc, us.filestream,
+                                        central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+            err=UNZ_ERRNO;
+
+        /* the signature, already checked */
+        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* number of this disk */
+        if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* number of the disk with the start of the central directory */
+        if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK)
+            err=UNZ_ERRNO;
+
+        /* total number of entries in the central dir on this disk */
+        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+            err=UNZ_ERRNO;
+        us.gi.number_entry = uL;
+
+        /* total number of entries in the central dir */
+        if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+            err=UNZ_ERRNO;
+        number_entry_CD = uL;
+
+        if ((number_entry_CD!=us.gi.number_entry) ||
+            (number_disk_with_CD!=0) ||
+            (number_disk!=0))
+            err=UNZ_BADZIPFILE;
+
+        /* size of the central directory */
+        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+            err=UNZ_ERRNO;
+        us.size_central_dir = uL;
+
+        /* offset of start of central directory with respect to the
+            starting disk number */
+        if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+            err=UNZ_ERRNO;
+        us.offset_central_dir = uL;
+
+        /* zipfile comment length */
+        if (unz64local_getShort(&us.z_filefunc, us.filestream,&us.gi.size_comment)!=UNZ_OK)
+            err=UNZ_ERRNO;
+    }
+
+    if ((central_pospfile_in_zip_read!=NULL)
+        unzCloseCurrentFile(file);
+
+    ZCLOSE64(s->z_filefunc, s->filestream);
+    free(s);
+    return UNZ_OK;
+}
+
+
+/*
+  Write info about the ZipFile in the *pglobal_info structure.
+  No preparation of the structure is needed
+  return UNZ_OK if there is no problem. */
+extern int ZEXPORT unzGetGlobalInfo64(unzFile file, unz_global_info64* pglobal_info) {
+    unz64_s* s;
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    *pglobal_info=s->gi;
+    return UNZ_OK;
+}
+
+extern int ZEXPORT unzGetGlobalInfo(unzFile file, unz_global_info* pglobal_info32) {
+    unz64_s* s;
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    /* to do : check if number_entry is not truncated */
+    pglobal_info32->number_entry = (uLong)s->gi.number_entry;
+    pglobal_info32->size_comment = s->gi.size_comment;
+    return UNZ_OK;
+}
+/*
+   Translate date/time from Dos format to tm_unz (readable more easily)
+*/
+local void unz64local_DosDateToTmuDate(ZPOS64_T ulDosDate, tm_unz* ptm) {
+    ZPOS64_T uDate;
+    uDate = (ZPOS64_T)(ulDosDate>>16);
+    ptm->tm_mday = (int)(uDate&0x1f) ;
+    ptm->tm_mon =  (int)((((uDate)&0x1E0)/0x20)-1) ;
+    ptm->tm_year = (int)(((uDate&0x0FE00)/0x0200)+1980) ;
+
+    ptm->tm_hour = (int) ((ulDosDate &0xF800)/0x800);
+    ptm->tm_min =  (int) ((ulDosDate&0x7E0)/0x20) ;
+    ptm->tm_sec =  (int) (2*(ulDosDate&0x1f)) ;
+}
+
+/*
+  Get Info about the current file in the zipfile, with internal only info
+*/
+local int unz64local_GetCurrentFileInfoInternal(unzFile file,
+                                                unz_file_info64 *pfile_info,
+                                                unz_file_info64_internal
+                                                *pfile_info_internal,
+                                                char *szFileName,
+                                                uLong fileNameBufferSize,
+                                                void *extraField,
+                                                uLong extraFieldBufferSize,
+                                                char *szComment,
+                                                uLong commentBufferSize) {
+    unz64_s* s;
+    unz_file_info64 file_info;
+    unz_file_info64_internal file_info_internal;
+    int err=UNZ_OK;
+    uLong uMagic;
+    long lSeek=0;
+    uLong uL;
+
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    if (ZSEEK64(s->z_filefunc, s->filestream,
+              s->pos_in_central_dir+s->byte_before_the_zipfile,
+              ZLIB_FILEFUNC_SEEK_SET)!=0)
+        err=UNZ_ERRNO;
+
+
+    /* we check the magic */
+    if (err==UNZ_OK)
+    {
+        if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK)
+            err=UNZ_ERRNO;
+        else if (uMagic!=0x02014b50)
+            err=UNZ_BADZIPFILE;
+    }
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version_needed) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.flag) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.compression_method) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.dosDate) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    unz64local_DosDateToTmuDate(file_info.dosDate,&file_info.tmu_date);
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.crc) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
+        err=UNZ_ERRNO;
+    file_info.compressed_size = uL;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
+        err=UNZ_ERRNO;
+    file_info.uncompressed_size = uL;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_filename) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_extra) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_comment) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.internal_fa) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.external_fa) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+                // relative offset of local header
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
+        err=UNZ_ERRNO;
+    file_info_internal.offset_curfile = uL;
+
+    lSeek+=file_info.size_filename;
+    if ((err==UNZ_OK) && (szFileName!=NULL))
+    {
+        uLong uSizeRead ;
+        if (file_info.size_filename0) && (fileNameBufferSize>0))
+            if (ZREAD64(s->z_filefunc, s->filestream,szFileName,uSizeRead)!=uSizeRead)
+                err=UNZ_ERRNO;
+        lSeek -= uSizeRead;
+    }
+
+    // Read extrafield
+    if ((err==UNZ_OK) && (extraField!=NULL))
+    {
+        ZPOS64_T uSizeRead ;
+        if (file_info.size_file_extraz_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+                lSeek=0;
+            else
+                err=UNZ_ERRNO;
+        }
+
+        if ((file_info.size_file_extra>0) && (extraFieldBufferSize>0))
+            if (ZREAD64(s->z_filefunc, s->filestream,extraField,(uLong)uSizeRead)!=uSizeRead)
+                err=UNZ_ERRNO;
+
+        lSeek += file_info.size_file_extra - (uLong)uSizeRead;
+    }
+    else
+        lSeek += file_info.size_file_extra;
+
+
+    if ((err==UNZ_OK) && (file_info.size_file_extra != 0))
+    {
+                                uLong acc = 0;
+
+        // since lSeek now points to after the extra field we need to move back
+        lSeek -= file_info.size_file_extra;
+
+        if (lSeek!=0)
+        {
+            if (ZSEEK64(s->z_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+                lSeek=0;
+            else
+                err=UNZ_ERRNO;
+        }
+
+        while(acc < file_info.size_file_extra)
+        {
+            uLong headerId;
+                                                uLong dataSize;
+
+            if (unz64local_getShort(&s->z_filefunc, s->filestream,&headerId) != UNZ_OK)
+                err=UNZ_ERRNO;
+
+            if (unz64local_getShort(&s->z_filefunc, s->filestream,&dataSize) != UNZ_OK)
+                err=UNZ_ERRNO;
+
+            /* ZIP64 extra fields */
+            if (headerId == 0x0001)
+            {
+                if(file_info.uncompressed_size == MAXU32)
+                {
+                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
+
+                if(file_info.compressed_size == MAXU32)
+                {
+                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
+
+                if(file_info_internal.offset_curfile == MAXU32)
+                {
+                    /* Relative Header offset */
+                    if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
+
+                if(file_info.disk_num_start == 0xffff)
+                {
+                    /* Disk Start Number */
+                    if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK)
+                        err=UNZ_ERRNO;
+                }
+
+            }
+            else
+            {
+                if (ZSEEK64(s->z_filefunc, s->filestream,dataSize,ZLIB_FILEFUNC_SEEK_CUR)!=0)
+                    err=UNZ_ERRNO;
+            }
+
+            acc += 2 + 2 + dataSize;
+        }
+    }
+
+    if ((err==UNZ_OK) && (szComment!=NULL))
+    {
+        uLong uSizeRead ;
+        if (file_info.size_file_commentz_filefunc, s->filestream,(ZPOS64_T)lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+                lSeek=0;
+            else
+                err=UNZ_ERRNO;
+        }
+
+        if ((file_info.size_file_comment>0) && (commentBufferSize>0))
+            if (ZREAD64(s->z_filefunc, s->filestream,szComment,uSizeRead)!=uSizeRead)
+                err=UNZ_ERRNO;
+        lSeek+=file_info.size_file_comment - uSizeRead;
+    }
+    else
+        lSeek+=file_info.size_file_comment;
+
+
+    if ((err==UNZ_OK) && (pfile_info!=NULL))
+        *pfile_info=file_info;
+
+    if ((err==UNZ_OK) && (pfile_info_internal!=NULL))
+        *pfile_info_internal=file_info_internal;
+
+    return err;
+}
+
+
+
+/*
+  Write info about the ZipFile in the *pglobal_info structure.
+  No preparation of the structure is needed
+  return UNZ_OK if there is no problem.
+*/
+extern int ZEXPORT unzGetCurrentFileInfo64(unzFile file,
+                                           unz_file_info64 * pfile_info,
+                                           char * szFileName, uLong fileNameBufferSize,
+                                           void *extraField, uLong extraFieldBufferSize,
+                                           char* szComment,  uLong commentBufferSize) {
+    return unz64local_GetCurrentFileInfoInternal(file,pfile_info,NULL,
+                                                 szFileName,fileNameBufferSize,
+                                                 extraField,extraFieldBufferSize,
+                                                 szComment,commentBufferSize);
+}
+
+extern int ZEXPORT unzGetCurrentFileInfo(unzFile file,
+                                         unz_file_info * pfile_info,
+                                         char * szFileName, uLong fileNameBufferSize,
+                                         void *extraField, uLong extraFieldBufferSize,
+                                         char* szComment,  uLong commentBufferSize) {
+    int err;
+    unz_file_info64 file_info64;
+    err = unz64local_GetCurrentFileInfoInternal(file,&file_info64,NULL,
+                                                szFileName,fileNameBufferSize,
+                                                extraField,extraFieldBufferSize,
+                                                szComment,commentBufferSize);
+    if ((err==UNZ_OK) && (pfile_info != NULL))
+    {
+        pfile_info->version = file_info64.version;
+        pfile_info->version_needed = file_info64.version_needed;
+        pfile_info->flag = file_info64.flag;
+        pfile_info->compression_method = file_info64.compression_method;
+        pfile_info->dosDate = file_info64.dosDate;
+        pfile_info->crc = file_info64.crc;
+
+        pfile_info->size_filename = file_info64.size_filename;
+        pfile_info->size_file_extra = file_info64.size_file_extra;
+        pfile_info->size_file_comment = file_info64.size_file_comment;
+
+        pfile_info->disk_num_start = file_info64.disk_num_start;
+        pfile_info->internal_fa = file_info64.internal_fa;
+        pfile_info->external_fa = file_info64.external_fa;
+
+        pfile_info->tmu_date = file_info64.tmu_date;
+
+
+        pfile_info->compressed_size = (uLong)file_info64.compressed_size;
+        pfile_info->uncompressed_size = (uLong)file_info64.uncompressed_size;
+
+    }
+    return err;
+}
+/*
+  Set the current file of the zipfile to the first file.
+  return UNZ_OK if there is no problem
+*/
+extern int ZEXPORT unzGoToFirstFile(unzFile file) {
+    int err=UNZ_OK;
+    unz64_s* s;
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    s->pos_in_central_dir=s->offset_central_dir;
+    s->num_file=0;
+    err=unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+                                             &s->cur_file_info_internal,
+                                             NULL,0,NULL,0,NULL,0);
+    s->current_file_ok = (err == UNZ_OK);
+    return err;
+}
+
+/*
+  Set the current file of the zipfile to the next file.
+  return UNZ_OK if there is no problem
+  return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.
+*/
+extern int ZEXPORT unzGoToNextFile(unzFile file) {
+    unz64_s* s;
+    int err;
+
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    if (!s->current_file_ok)
+        return UNZ_END_OF_LIST_OF_FILE;
+    if (s->gi.number_entry != 0xffff)    /* 2^16 files overflow hack */
+      if (s->num_file+1==s->gi.number_entry)
+        return UNZ_END_OF_LIST_OF_FILE;
+
+    s->pos_in_central_dir += SIZECENTRALDIRITEM + s->cur_file_info.size_filename +
+            s->cur_file_info.size_file_extra + s->cur_file_info.size_file_comment ;
+    s->num_file++;
+    err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+                                               &s->cur_file_info_internal,
+                                               NULL,0,NULL,0,NULL,0);
+    s->current_file_ok = (err == UNZ_OK);
+    return err;
+}
+
+
+/*
+  Try locate the file szFileName in the zipfile.
+  For the iCaseSensitivity signification, see unzStringFileNameCompare
+
+  return value :
+  UNZ_OK if the file is found. It becomes the current file.
+  UNZ_END_OF_LIST_OF_FILE if the file is not found
+*/
+extern int ZEXPORT unzLocateFile(unzFile file, const char *szFileName, int iCaseSensitivity) {
+    unz64_s* s;
+    int err;
+
+    /* We remember the 'current' position in the file so that we can jump
+     * back there if we fail.
+     */
+    unz_file_info64 cur_file_infoSaved;
+    unz_file_info64_internal cur_file_info_internalSaved;
+    ZPOS64_T num_fileSaved;
+    ZPOS64_T pos_in_central_dirSaved;
+
+
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+
+    if (strlen(szFileName)>=UNZ_MAXFILENAMEINZIP)
+        return UNZ_PARAMERROR;
+
+    s=(unz64_s*)file;
+    if (!s->current_file_ok)
+        return UNZ_END_OF_LIST_OF_FILE;
+
+    /* Save the current state */
+    num_fileSaved = s->num_file;
+    pos_in_central_dirSaved = s->pos_in_central_dir;
+    cur_file_infoSaved = s->cur_file_info;
+    cur_file_info_internalSaved = s->cur_file_info_internal;
+
+    err = unzGoToFirstFile(file);
+
+    while (err == UNZ_OK)
+    {
+        char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1];
+        err = unzGetCurrentFileInfo64(file,NULL,
+                                    szCurrentFileName,sizeof(szCurrentFileName)-1,
+                                    NULL,0,NULL,0);
+        if (err == UNZ_OK)
+        {
+            if (unzStringFileNameCompare(szCurrentFileName,
+                                            szFileName,iCaseSensitivity)==0)
+                return UNZ_OK;
+            err = unzGoToNextFile(file);
+        }
+    }
+
+    /* We failed, so restore the state of the 'current file' to where we
+     * were.
+     */
+    s->num_file = num_fileSaved ;
+    s->pos_in_central_dir = pos_in_central_dirSaved ;
+    s->cur_file_info = cur_file_infoSaved;
+    s->cur_file_info_internal = cur_file_info_internalSaved;
+    return err;
+}
+
+
+/*
+///////////////////////////////////////////
+// Contributed by Ryan Haksi (mailto://cryogen@infoserve.net)
+// I need random access
+//
+// Further optimization could be realized by adding an ability
+// to cache the directory in memory. The goal being a single
+// comprehensive file read to put the file I need in a memory.
+*/
+
+/*
+typedef struct unz_file_pos_s
+{
+    ZPOS64_T pos_in_zip_directory;   // offset in file
+    ZPOS64_T num_of_file;            // # of file
+} unz_file_pos;
+*/
+
+extern int ZEXPORT unzGetFilePos64(unzFile file, unz64_file_pos* file_pos) {
+    unz64_s* s;
+
+    if (file==NULL || file_pos==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    if (!s->current_file_ok)
+        return UNZ_END_OF_LIST_OF_FILE;
+
+    file_pos->pos_in_zip_directory  = s->pos_in_central_dir;
+    file_pos->num_of_file           = s->num_file;
+
+    return UNZ_OK;
+}
+
+extern int ZEXPORT unzGetFilePos(unzFile file, unz_file_pos* file_pos) {
+    unz64_file_pos file_pos64;
+    int err = unzGetFilePos64(file,&file_pos64);
+    if (err==UNZ_OK)
+    {
+        file_pos->pos_in_zip_directory = (uLong)file_pos64.pos_in_zip_directory;
+        file_pos->num_of_file = (uLong)file_pos64.num_of_file;
+    }
+    return err;
+}
+
+extern int ZEXPORT unzGoToFilePos64(unzFile file, const unz64_file_pos* file_pos) {
+    unz64_s* s;
+    int err;
+
+    if (file==NULL || file_pos==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+
+    /* jump to the right spot */
+    s->pos_in_central_dir = file_pos->pos_in_zip_directory;
+    s->num_file           = file_pos->num_of_file;
+
+    /* set the current file */
+    err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+                                               &s->cur_file_info_internal,
+                                               NULL,0,NULL,0,NULL,0);
+    /* return results */
+    s->current_file_ok = (err == UNZ_OK);
+    return err;
+}
+
+extern int ZEXPORT unzGoToFilePos(unzFile file, unz_file_pos* file_pos) {
+    unz64_file_pos file_pos64;
+    if (file_pos == NULL)
+        return UNZ_PARAMERROR;
+
+    file_pos64.pos_in_zip_directory = file_pos->pos_in_zip_directory;
+    file_pos64.num_of_file = file_pos->num_of_file;
+    return unzGoToFilePos64(file,&file_pos64);
+}
+
+/*
+// Unzip Helper Functions - should be here?
+///////////////////////////////////////////
+*/
+
+/*
+  Read the local header of the current zipfile
+  Check the coherency of the local header and info in the end of central
+        directory about this file
+  store in *piSizeVar the size of extra info in local header
+        (filename and size of extra field data)
+*/
+local int unz64local_CheckCurrentFileCoherencyHeader(unz64_s* s, uInt* piSizeVar,
+                                                     ZPOS64_T * poffset_local_extrafield,
+                                                     uInt  * psize_local_extrafield) {
+    uLong uMagic,uData,uFlags;
+    uLong size_filename;
+    uLong size_extra_field;
+    int err=UNZ_OK;
+
+    *piSizeVar = 0;
+    *poffset_local_extrafield = 0;
+    *psize_local_extrafield = 0;
+
+    if (ZSEEK64(s->z_filefunc, s->filestream,s->cur_file_info_internal.offset_curfile +
+                                s->byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET)!=0)
+        return UNZ_ERRNO;
+
+
+    if (err==UNZ_OK)
+    {
+        if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK)
+            err=UNZ_ERRNO;
+        else if (uMagic!=0x04034b50)
+            err=UNZ_BADZIPFILE;
+    }
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK)
+        err=UNZ_ERRNO;
+/*
+    else if ((err==UNZ_OK) && (uData!=s->cur_file_info.wVersion))
+        err=UNZ_BADZIPFILE;
+*/
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&uFlags) != UNZ_OK)
+        err=UNZ_ERRNO;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK)
+        err=UNZ_ERRNO;
+    else if ((err==UNZ_OK) && (uData!=s->cur_file_info.compression_method))
+        err=UNZ_BADZIPFILE;
+
+    if ((err==UNZ_OK) && (s->cur_file_info.compression_method!=0) &&
+/* #ifdef HAVE_BZIP2 */
+                         (s->cur_file_info.compression_method!=Z_BZIP2ED) &&
+/* #endif */
+                         (s->cur_file_info.compression_method!=Z_DEFLATED))
+        err=UNZ_BADZIPFILE;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* date/time */
+        err=UNZ_ERRNO;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* crc */
+        err=UNZ_ERRNO;
+    else if ((err==UNZ_OK) && (uData!=s->cur_file_info.crc) && ((uFlags & 8)==0))
+        err=UNZ_BADZIPFILE;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size compr */
+        err=UNZ_ERRNO;
+    else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.compressed_size) && ((uFlags & 8)==0))
+        err=UNZ_BADZIPFILE;
+
+    if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size uncompr */
+        err=UNZ_ERRNO;
+    else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.uncompressed_size) && ((uFlags & 8)==0))
+        err=UNZ_BADZIPFILE;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_filename) != UNZ_OK)
+        err=UNZ_ERRNO;
+    else if ((err==UNZ_OK) && (size_filename!=s->cur_file_info.size_filename))
+        err=UNZ_BADZIPFILE;
+
+    *piSizeVar += (uInt)size_filename;
+
+    if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_extra_field) != UNZ_OK)
+        err=UNZ_ERRNO;
+    *poffset_local_extrafield= s->cur_file_info_internal.offset_curfile +
+                                    SIZEZIPLOCALHEADER + size_filename;
+    *psize_local_extrafield = (uInt)size_extra_field;
+
+    *piSizeVar += (uInt)size_extra_field;
+
+    return err;
+}
+
+/*
+  Open for reading data the current file in the zipfile.
+  If there is no error and the file is opened, the return value is UNZ_OK.
+*/
+extern int ZEXPORT unzOpenCurrentFile3(unzFile file, int* method,
+                                       int* level, int raw, const char* password) {
+    int err=UNZ_OK;
+    uInt iSizeVar;
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    ZPOS64_T offset_local_extrafield;  /* offset of the local extra field */
+    uInt  size_local_extrafield;    /* size of the local extra field */
+#    ifndef NOUNCRYPT
+    char source[12];
+#    else
+    if (password != NULL)
+        return UNZ_PARAMERROR;
+#    endif
+
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    if (!s->current_file_ok)
+        return UNZ_PARAMERROR;
+
+    if (s->pfile_in_zip_read != NULL)
+        unzCloseCurrentFile(file);
+
+    if (unz64local_CheckCurrentFileCoherencyHeader(s,&iSizeVar, &offset_local_extrafield,&size_local_extrafield)!=UNZ_OK)
+        return UNZ_BADZIPFILE;
+
+    pfile_in_zip_read_info = (file_in_zip64_read_info_s*)ALLOC(sizeof(file_in_zip64_read_info_s));
+    if (pfile_in_zip_read_info==NULL)
+        return UNZ_INTERNALERROR;
+
+    pfile_in_zip_read_info->read_buffer=(char*)ALLOC(UNZ_BUFSIZE);
+    pfile_in_zip_read_info->offset_local_extrafield = offset_local_extrafield;
+    pfile_in_zip_read_info->size_local_extrafield = size_local_extrafield;
+    pfile_in_zip_read_info->pos_local_extrafield=0;
+    pfile_in_zip_read_info->raw=raw;
+
+    if (pfile_in_zip_read_info->read_buffer==NULL)
+    {
+        free(pfile_in_zip_read_info);
+        return UNZ_INTERNALERROR;
+    }
+
+    pfile_in_zip_read_info->stream_initialised=0;
+
+    if (method!=NULL)
+        *method = (int)s->cur_file_info.compression_method;
+
+    if (level!=NULL)
+    {
+        *level = 6;
+        switch (s->cur_file_info.flag & 0x06)
+        {
+          case 6 : *level = 1; break;
+          case 4 : *level = 2; break;
+          case 2 : *level = 9; break;
+        }
+    }
+
+    if ((s->cur_file_info.compression_method!=0) &&
+/* #ifdef HAVE_BZIP2 */
+        (s->cur_file_info.compression_method!=Z_BZIP2ED) &&
+/* #endif */
+        (s->cur_file_info.compression_method!=Z_DEFLATED))
+
+        err=UNZ_BADZIPFILE;
+
+    pfile_in_zip_read_info->crc32_wait=s->cur_file_info.crc;
+    pfile_in_zip_read_info->crc32=0;
+    pfile_in_zip_read_info->total_out_64=0;
+    pfile_in_zip_read_info->compression_method = s->cur_file_info.compression_method;
+    pfile_in_zip_read_info->filestream=s->filestream;
+    pfile_in_zip_read_info->z_filefunc=s->z_filefunc;
+    pfile_in_zip_read_info->byte_before_the_zipfile=s->byte_before_the_zipfile;
+
+    pfile_in_zip_read_info->stream.total_out = 0;
+
+    if ((s->cur_file_info.compression_method==Z_BZIP2ED) && (!raw))
+    {
+#ifdef HAVE_BZIP2
+      pfile_in_zip_read_info->bstream.bzalloc = (void *(*) (void *, int, int))0;
+      pfile_in_zip_read_info->bstream.bzfree = (free_func)0;
+      pfile_in_zip_read_info->bstream.opaque = (voidpf)0;
+      pfile_in_zip_read_info->bstream.state = (voidpf)0;
+
+      pfile_in_zip_read_info->stream.zalloc = (alloc_func)0;
+      pfile_in_zip_read_info->stream.zfree = (free_func)0;
+      pfile_in_zip_read_info->stream.opaque = (voidpf)0;
+      pfile_in_zip_read_info->stream.next_in = (voidpf)0;
+      pfile_in_zip_read_info->stream.avail_in = 0;
+
+      err=BZ2_bzDecompressInit(&pfile_in_zip_read_info->bstream, 0, 0);
+      if (err == Z_OK)
+        pfile_in_zip_read_info->stream_initialised=Z_BZIP2ED;
+      else
+      {
+        free(pfile_in_zip_read_info->read_buffer);
+        free(pfile_in_zip_read_info);
+        return err;
+      }
+#else
+      pfile_in_zip_read_info->raw=1;
+#endif
+    }
+    else if ((s->cur_file_info.compression_method==Z_DEFLATED) && (!raw))
+    {
+      pfile_in_zip_read_info->stream.zalloc = (alloc_func)0;
+      pfile_in_zip_read_info->stream.zfree = (free_func)0;
+      pfile_in_zip_read_info->stream.opaque = (voidpf)0;
+      pfile_in_zip_read_info->stream.next_in = 0;
+      pfile_in_zip_read_info->stream.avail_in = 0;
+
+      err=inflateInit2(&pfile_in_zip_read_info->stream, -MAX_WBITS);
+      if (err == Z_OK)
+        pfile_in_zip_read_info->stream_initialised=Z_DEFLATED;
+      else
+      {
+        free(pfile_in_zip_read_info->read_buffer);
+        free(pfile_in_zip_read_info);
+        return err;
+      }
+        /* windowBits is passed < 0 to tell that there is no zlib header.
+         * Note that in this case inflate *requires* an extra "dummy" byte
+         * after the compressed stream in order to complete decompression and
+         * return Z_STREAM_END.
+         * In unzip, i don't wait absolutely Z_STREAM_END because I known the
+         * size of both compressed and uncompressed data
+         */
+    }
+    pfile_in_zip_read_info->rest_read_compressed =
+            s->cur_file_info.compressed_size ;
+    pfile_in_zip_read_info->rest_read_uncompressed =
+            s->cur_file_info.uncompressed_size ;
+
+
+    pfile_in_zip_read_info->pos_in_zipfile =
+            s->cur_file_info_internal.offset_curfile + SIZEZIPLOCALHEADER +
+              iSizeVar;
+
+    pfile_in_zip_read_info->stream.avail_in = (uInt)0;
+
+    s->pfile_in_zip_read = pfile_in_zip_read_info;
+                s->encrypted = 0;
+
+#    ifndef NOUNCRYPT
+    if (password != NULL)
+    {
+        int i;
+        s->pcrc_32_tab = get_crc_table();
+        init_keys(password,s->keys,s->pcrc_32_tab);
+        if (ZSEEK64(s->z_filefunc, s->filestream,
+                  s->pfile_in_zip_read->pos_in_zipfile +
+                     s->pfile_in_zip_read->byte_before_the_zipfile,
+                  SEEK_SET)!=0)
+            return UNZ_INTERNALERROR;
+        if(ZREAD64(s->z_filefunc, s->filestream,source, 12)<12)
+            return UNZ_INTERNALERROR;
+
+        for (i = 0; i<12; i++)
+            zdecode(s->keys,s->pcrc_32_tab,source[i]);
+
+        s->pfile_in_zip_read->pos_in_zipfile+=12;
+        s->encrypted=1;
+    }
+#    endif
+
+
+    return UNZ_OK;
+}
+
+extern int ZEXPORT unzOpenCurrentFile(unzFile file) {
+    return unzOpenCurrentFile3(file, NULL, NULL, 0, NULL);
+}
+
+extern int ZEXPORT unzOpenCurrentFilePassword(unzFile file, const char* password) {
+    return unzOpenCurrentFile3(file, NULL, NULL, 0, password);
+}
+
+extern int ZEXPORT unzOpenCurrentFile2(unzFile file, int* method, int* level, int raw) {
+    return unzOpenCurrentFile3(file, method, level, raw, NULL);
+}
+
+/** Addition for GDAL : START */
+
+extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64(unzFile file) {
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    s=(unz64_s*)file;
+    if (file==NULL)
+        return 0; //UNZ_PARAMERROR;
+    pfile_in_zip_read_info=s->pfile_in_zip_read;
+    if (pfile_in_zip_read_info==NULL)
+        return 0; //UNZ_PARAMERROR;
+    return pfile_in_zip_read_info->pos_in_zipfile +
+                         pfile_in_zip_read_info->byte_before_the_zipfile;
+}
+
+/** Addition for GDAL : END */
+
+/*
+  Read bytes from the current file.
+  buf contain buffer where data must be copied
+  len the size of buf.
+
+  return the number of byte copied if some bytes are copied
+  return 0 if the end of file was reached
+  return <0 with error code if there is an error
+    (UNZ_ERRNO for IO error, or zLib error for uncompress error)
+*/
+extern int ZEXPORT unzReadCurrentFile(unzFile file, voidp buf, unsigned len) {
+    int err=UNZ_OK;
+    uInt iRead = 0;
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+    if (pfile_in_zip_read_info==NULL)
+        return UNZ_PARAMERROR;
+
+
+    if (pfile_in_zip_read_info->read_buffer == NULL)
+        return UNZ_END_OF_LIST_OF_FILE;
+    if (len==0)
+        return 0;
+
+    pfile_in_zip_read_info->stream.next_out = (Bytef*)buf;
+
+    pfile_in_zip_read_info->stream.avail_out = (uInt)len;
+
+    if ((len>pfile_in_zip_read_info->rest_read_uncompressed) &&
+        (!(pfile_in_zip_read_info->raw)))
+        pfile_in_zip_read_info->stream.avail_out =
+            (uInt)pfile_in_zip_read_info->rest_read_uncompressed;
+
+    if ((len>pfile_in_zip_read_info->rest_read_compressed+
+           pfile_in_zip_read_info->stream.avail_in) &&
+         (pfile_in_zip_read_info->raw))
+        pfile_in_zip_read_info->stream.avail_out =
+            (uInt)pfile_in_zip_read_info->rest_read_compressed+
+            pfile_in_zip_read_info->stream.avail_in;
+
+    while (pfile_in_zip_read_info->stream.avail_out>0)
+    {
+        if ((pfile_in_zip_read_info->stream.avail_in==0) &&
+            (pfile_in_zip_read_info->rest_read_compressed>0))
+        {
+            uInt uReadThis = UNZ_BUFSIZE;
+            if (pfile_in_zip_read_info->rest_read_compressedrest_read_compressed;
+            if (uReadThis == 0)
+                return UNZ_EOF;
+            if (ZSEEK64(pfile_in_zip_read_info->z_filefunc,
+                      pfile_in_zip_read_info->filestream,
+                      pfile_in_zip_read_info->pos_in_zipfile +
+                         pfile_in_zip_read_info->byte_before_the_zipfile,
+                         ZLIB_FILEFUNC_SEEK_SET)!=0)
+                return UNZ_ERRNO;
+            if (ZREAD64(pfile_in_zip_read_info->z_filefunc,
+                      pfile_in_zip_read_info->filestream,
+                      pfile_in_zip_read_info->read_buffer,
+                      uReadThis)!=uReadThis)
+                return UNZ_ERRNO;
+
+
+#            ifndef NOUNCRYPT
+            if(s->encrypted)
+            {
+                uInt i;
+                for(i=0;iread_buffer[i] =
+                      zdecode(s->keys,s->pcrc_32_tab,
+                              pfile_in_zip_read_info->read_buffer[i]);
+            }
+#            endif
+
+
+            pfile_in_zip_read_info->pos_in_zipfile += uReadThis;
+
+            pfile_in_zip_read_info->rest_read_compressed-=uReadThis;
+
+            pfile_in_zip_read_info->stream.next_in =
+                (Bytef*)pfile_in_zip_read_info->read_buffer;
+            pfile_in_zip_read_info->stream.avail_in = (uInt)uReadThis;
+        }
+
+        if ((pfile_in_zip_read_info->compression_method==0) || (pfile_in_zip_read_info->raw))
+        {
+            uInt uDoCopy,i ;
+
+            if ((pfile_in_zip_read_info->stream.avail_in == 0) &&
+                (pfile_in_zip_read_info->rest_read_compressed == 0))
+                return (iRead==0) ? UNZ_EOF : (int)iRead;
+
+            if (pfile_in_zip_read_info->stream.avail_out <
+                            pfile_in_zip_read_info->stream.avail_in)
+                uDoCopy = pfile_in_zip_read_info->stream.avail_out ;
+            else
+                uDoCopy = pfile_in_zip_read_info->stream.avail_in ;
+
+            for (i=0;istream.next_out+i) =
+                        *(pfile_in_zip_read_info->stream.next_in+i);
+
+            pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uDoCopy;
+
+            pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,
+                                pfile_in_zip_read_info->stream.next_out,
+                                uDoCopy);
+            pfile_in_zip_read_info->rest_read_uncompressed-=uDoCopy;
+            pfile_in_zip_read_info->stream.avail_in -= uDoCopy;
+            pfile_in_zip_read_info->stream.avail_out -= uDoCopy;
+            pfile_in_zip_read_info->stream.next_out += uDoCopy;
+            pfile_in_zip_read_info->stream.next_in += uDoCopy;
+            pfile_in_zip_read_info->stream.total_out += uDoCopy;
+            iRead += uDoCopy;
+        }
+        else if (pfile_in_zip_read_info->compression_method==Z_BZIP2ED)
+        {
+#ifdef HAVE_BZIP2
+            uLong uTotalOutBefore,uTotalOutAfter;
+            const Bytef *bufBefore;
+            uLong uOutThis;
+
+            pfile_in_zip_read_info->bstream.next_in        = (char*)pfile_in_zip_read_info->stream.next_in;
+            pfile_in_zip_read_info->bstream.avail_in       = pfile_in_zip_read_info->stream.avail_in;
+            pfile_in_zip_read_info->bstream.total_in_lo32  = pfile_in_zip_read_info->stream.total_in;
+            pfile_in_zip_read_info->bstream.total_in_hi32  = 0;
+            pfile_in_zip_read_info->bstream.next_out       = (char*)pfile_in_zip_read_info->stream.next_out;
+            pfile_in_zip_read_info->bstream.avail_out      = pfile_in_zip_read_info->stream.avail_out;
+            pfile_in_zip_read_info->bstream.total_out_lo32 = pfile_in_zip_read_info->stream.total_out;
+            pfile_in_zip_read_info->bstream.total_out_hi32 = 0;
+
+            uTotalOutBefore = pfile_in_zip_read_info->bstream.total_out_lo32;
+            bufBefore = (const Bytef *)pfile_in_zip_read_info->bstream.next_out;
+
+            err=BZ2_bzDecompress(&pfile_in_zip_read_info->bstream);
+
+            uTotalOutAfter = pfile_in_zip_read_info->bstream.total_out_lo32;
+            uOutThis = uTotalOutAfter-uTotalOutBefore;
+
+            pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis;
+
+            pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,bufBefore, (uInt)(uOutThis));
+            pfile_in_zip_read_info->rest_read_uncompressed -= uOutThis;
+            iRead += (uInt)(uTotalOutAfter - uTotalOutBefore);
+
+            pfile_in_zip_read_info->stream.next_in   = (Bytef*)pfile_in_zip_read_info->bstream.next_in;
+            pfile_in_zip_read_info->stream.avail_in  = pfile_in_zip_read_info->bstream.avail_in;
+            pfile_in_zip_read_info->stream.total_in  = pfile_in_zip_read_info->bstream.total_in_lo32;
+            pfile_in_zip_read_info->stream.next_out  = (Bytef*)pfile_in_zip_read_info->bstream.next_out;
+            pfile_in_zip_read_info->stream.avail_out = pfile_in_zip_read_info->bstream.avail_out;
+            pfile_in_zip_read_info->stream.total_out = pfile_in_zip_read_info->bstream.total_out_lo32;
+
+            if (err==BZ_STREAM_END)
+              return (iRead==0) ? UNZ_EOF : iRead;
+            if (err!=BZ_OK)
+              break;
+#endif
+        } // end Z_BZIP2ED
+        else
+        {
+            ZPOS64_T uTotalOutBefore,uTotalOutAfter;
+            const Bytef *bufBefore;
+            ZPOS64_T uOutThis;
+            int flush=Z_SYNC_FLUSH;
+
+            uTotalOutBefore = pfile_in_zip_read_info->stream.total_out;
+            bufBefore = pfile_in_zip_read_info->stream.next_out;
+
+            /*
+            if ((pfile_in_zip_read_info->rest_read_uncompressed ==
+                     pfile_in_zip_read_info->stream.avail_out) &&
+                (pfile_in_zip_read_info->rest_read_compressed == 0))
+                flush = Z_FINISH;
+            */
+            err=inflate(&pfile_in_zip_read_info->stream,flush);
+
+            if ((err>=0) && (pfile_in_zip_read_info->stream.msg!=NULL))
+              err = Z_DATA_ERROR;
+
+            uTotalOutAfter = pfile_in_zip_read_info->stream.total_out;
+            /* Detect overflow, because z_stream.total_out is uLong (32 bits) */
+            if (uTotalOutAftertotal_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis;
+
+            pfile_in_zip_read_info->crc32 =
+                crc32(pfile_in_zip_read_info->crc32,bufBefore,
+                        (uInt)(uOutThis));
+
+            pfile_in_zip_read_info->rest_read_uncompressed -=
+                uOutThis;
+
+            iRead += (uInt)(uTotalOutAfter - uTotalOutBefore);
+
+            if (err==Z_STREAM_END)
+                return (iRead==0) ? UNZ_EOF : (int)iRead;
+            if (err!=Z_OK)
+                break;
+        }
+    }
+
+    if (err==Z_OK)
+        return (int)iRead;
+    return err;
+}
+
+
+/*
+  Give the current position in uncompressed data
+*/
+extern z_off_t ZEXPORT unztell(unzFile file) {
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+    if (pfile_in_zip_read_info==NULL)
+        return UNZ_PARAMERROR;
+
+    return (z_off_t)pfile_in_zip_read_info->stream.total_out;
+}
+
+extern ZPOS64_T ZEXPORT unztell64(unzFile file) {
+
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    if (file==NULL)
+        return (ZPOS64_T)-1;
+    s=(unz64_s*)file;
+    pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+    if (pfile_in_zip_read_info==NULL)
+        return (ZPOS64_T)-1;
+
+    return pfile_in_zip_read_info->total_out_64;
+}
+
+
+/*
+  return 1 if the end of file was reached, 0 elsewhere
+*/
+extern int ZEXPORT unzeof(unzFile file) {
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+    if (pfile_in_zip_read_info==NULL)
+        return UNZ_PARAMERROR;
+
+    if (pfile_in_zip_read_info->rest_read_uncompressed == 0)
+        return 1;
+    else
+        return 0;
+}
+
+
+
+/*
+Read extra field from the current file (opened by unzOpenCurrentFile)
+This is the local-header version of the extra field (sometimes, there is
+more info in the local-header version than in the central-header)
+
+  if buf==NULL, it return the size of the local extra field that can be read
+
+  if buf!=NULL, len is the size of the buffer, the extra header is copied in
+    buf.
+  the return value is the number of bytes copied in buf, or (if <0)
+    the error code
+*/
+extern int ZEXPORT unzGetLocalExtrafield(unzFile file, voidp buf, unsigned len) {
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    uInt read_now;
+    ZPOS64_T size_to_read;
+
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+    if (pfile_in_zip_read_info==NULL)
+        return UNZ_PARAMERROR;
+
+    size_to_read = (pfile_in_zip_read_info->size_local_extrafield -
+                pfile_in_zip_read_info->pos_local_extrafield);
+
+    if (buf==NULL)
+        return (int)size_to_read;
+
+    if (len>size_to_read)
+        read_now = (uInt)size_to_read;
+    else
+        read_now = (uInt)len ;
+
+    if (read_now==0)
+        return 0;
+
+    if (ZSEEK64(pfile_in_zip_read_info->z_filefunc,
+              pfile_in_zip_read_info->filestream,
+              pfile_in_zip_read_info->offset_local_extrafield +
+              pfile_in_zip_read_info->pos_local_extrafield,
+              ZLIB_FILEFUNC_SEEK_SET)!=0)
+        return UNZ_ERRNO;
+
+    if (ZREAD64(pfile_in_zip_read_info->z_filefunc,
+              pfile_in_zip_read_info->filestream,
+              buf,read_now)!=read_now)
+        return UNZ_ERRNO;
+
+    return (int)read_now;
+}
+
+/*
+  Close the file in zip opened with unzOpenCurrentFile
+  Return UNZ_CRCERROR if all the file was read but the CRC is not good
+*/
+extern int ZEXPORT unzCloseCurrentFile(unzFile file) {
+    int err=UNZ_OK;
+
+    unz64_s* s;
+    file_in_zip64_read_info_s* pfile_in_zip_read_info;
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+    if (pfile_in_zip_read_info==NULL)
+        return UNZ_PARAMERROR;
+
+
+    if ((pfile_in_zip_read_info->rest_read_uncompressed == 0) &&
+        (!pfile_in_zip_read_info->raw))
+    {
+        if (pfile_in_zip_read_info->crc32 != pfile_in_zip_read_info->crc32_wait)
+            err=UNZ_CRCERROR;
+    }
+
+
+    free(pfile_in_zip_read_info->read_buffer);
+    pfile_in_zip_read_info->read_buffer = NULL;
+    if (pfile_in_zip_read_info->stream_initialised == Z_DEFLATED)
+        inflateEnd(&pfile_in_zip_read_info->stream);
+#ifdef HAVE_BZIP2
+    else if (pfile_in_zip_read_info->stream_initialised == Z_BZIP2ED)
+        BZ2_bzDecompressEnd(&pfile_in_zip_read_info->bstream);
+#endif
+
+
+    pfile_in_zip_read_info->stream_initialised = 0;
+    free(pfile_in_zip_read_info);
+
+    s->pfile_in_zip_read=NULL;
+
+    return err;
+}
+
+
+/*
+  Get the global comment string of the ZipFile, in the szComment buffer.
+  uSizeBuf is the size of the szComment buffer.
+  return the number of byte copied or an error code <0
+*/
+extern int ZEXPORT unzGetGlobalComment(unzFile file, char * szComment, uLong uSizeBuf) {
+    unz64_s* s;
+    uLong uReadThis ;
+    if (file==NULL)
+        return (int)UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+
+    uReadThis = uSizeBuf;
+    if (uReadThis>s->gi.size_comment)
+        uReadThis = s->gi.size_comment;
+
+    if (ZSEEK64(s->z_filefunc,s->filestream,s->central_pos+22,ZLIB_FILEFUNC_SEEK_SET)!=0)
+        return UNZ_ERRNO;
+
+    if (uReadThis>0)
+    {
+      *szComment='\0';
+      if (ZREAD64(s->z_filefunc,s->filestream,szComment,uReadThis)!=uReadThis)
+        return UNZ_ERRNO;
+    }
+
+    if ((szComment != NULL) && (uSizeBuf > s->gi.size_comment))
+        *(szComment+s->gi.size_comment)='\0';
+    return (int)uReadThis;
+}
+
+/* Additions by RX '2004 */
+extern ZPOS64_T ZEXPORT unzGetOffset64(unzFile file) {
+    unz64_s* s;
+
+    if (file==NULL)
+          return 0; //UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+    if (!s->current_file_ok)
+      return 0;
+    if (s->gi.number_entry != 0 && s->gi.number_entry != 0xffff)
+      if (s->num_file==s->gi.number_entry)
+         return 0;
+    return s->pos_in_central_dir;
+}
+
+extern uLong ZEXPORT unzGetOffset(unzFile file) {
+    ZPOS64_T offset64;
+
+    if (file==NULL)
+          return 0; //UNZ_PARAMERROR;
+    offset64 = unzGetOffset64(file);
+    return (uLong)offset64;
+}
+
+extern int ZEXPORT unzSetOffset64(unzFile file, ZPOS64_T pos) {
+    unz64_s* s;
+    int err;
+
+    if (file==NULL)
+        return UNZ_PARAMERROR;
+    s=(unz64_s*)file;
+
+    s->pos_in_central_dir = pos;
+    s->num_file = s->gi.number_entry;      /* hack */
+    err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+                                              &s->cur_file_info_internal,
+                                              NULL,0,NULL,0,NULL,0);
+    s->current_file_ok = (err == UNZ_OK);
+    return err;
+}
+
+extern int ZEXPORT unzSetOffset (unzFile file, uLong pos) {
+    return unzSetOffset64(file,pos);
+}
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/unzip.h b/OrthancFramework/Resources/ThirdParty/minizip/unzip.h
new file mode 100644
index 0000000..1410584
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/unzip.h
@@ -0,0 +1,437 @@
+/* unzip.h -- IO for uncompress .zip files using zlib
+   Version 1.1, February 14h, 2010
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications of Unzip for Zip64
+         Copyright (C) 2007-2008 Even Rouault
+
+         Modifications for Zip64 support on both zip and unzip
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+         ---------------------------------------------------------------------------------
+
+        Condition of use and distribution are the same than zlib :
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  ---------------------------------------------------------------------------------
+
+        Changes
+
+        See header of unzip64.c
+
+*/
+
+#ifndef _unz64_H
+#define _unz64_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef _ZLIB_H
+#include "zlib.h"
+#endif
+
+#ifndef  _ZLIBIOAPI_H
+#include "ioapi.h"
+#endif
+
+#ifdef HAVE_BZIP2
+#include "bzlib.h"
+#endif
+
+#define Z_BZIP2ED 12
+
+#if defined(STRICTUNZIP) || defined(STRICTZIPUNZIP)
+/* like the STRICT of WIN32, we define a pointer that cannot be converted
+    from (void*) without cast */
+typedef struct TagunzFile__ { int unused; } unzFile__;
+typedef unzFile__ *unzFile;
+#else
+typedef voidp unzFile;
+#endif
+
+
+#define UNZ_OK                          (0)
+#define UNZ_END_OF_LIST_OF_FILE         (-100)
+#define UNZ_ERRNO                       (Z_ERRNO)
+#define UNZ_EOF                         (0)
+#define UNZ_PARAMERROR                  (-102)
+#define UNZ_BADZIPFILE                  (-103)
+#define UNZ_INTERNALERROR               (-104)
+#define UNZ_CRCERROR                    (-105)
+
+/* tm_unz contain date/time info */
+typedef struct tm_unz_s
+{
+    int tm_sec;             /* seconds after the minute - [0,59] */
+    int tm_min;             /* minutes after the hour - [0,59] */
+    int tm_hour;            /* hours since midnight - [0,23] */
+    int tm_mday;            /* day of the month - [1,31] */
+    int tm_mon;             /* months since January - [0,11] */
+    int tm_year;            /* years - [1980..2044] */
+} tm_unz;
+
+/* unz_global_info structure contain global data about the ZIPfile
+   These data comes from the end of central dir */
+typedef struct unz_global_info64_s
+{
+    ZPOS64_T number_entry;         /* total number of entries in
+                                     the central dir on this disk */
+    uLong size_comment;         /* size of the global comment of the zipfile */
+} unz_global_info64;
+
+typedef struct unz_global_info_s
+{
+    uLong number_entry;         /* total number of entries in
+                                     the central dir on this disk */
+    uLong size_comment;         /* size of the global comment of the zipfile */
+} unz_global_info;
+
+/* unz_file_info contain information about a file in the zipfile */
+typedef struct unz_file_info64_s
+{
+    uLong version;              /* version made by                 2 bytes */
+    uLong version_needed;       /* version needed to extract       2 bytes */
+    uLong flag;                 /* general purpose bit flag        2 bytes */
+    uLong compression_method;   /* compression method              2 bytes */
+    uLong dosDate;              /* last mod file date in Dos fmt   4 bytes */
+    uLong crc;                  /* crc-32                          4 bytes */
+    ZPOS64_T compressed_size;   /* compressed size                 8 bytes */
+    ZPOS64_T uncompressed_size; /* uncompressed size               8 bytes */
+    uLong size_filename;        /* filename length                 2 bytes */
+    uLong size_file_extra;      /* extra field length              2 bytes */
+    uLong size_file_comment;    /* file comment length             2 bytes */
+
+    uLong disk_num_start;       /* disk number start               2 bytes */
+    uLong internal_fa;          /* internal file attributes        2 bytes */
+    uLong external_fa;          /* external file attributes        4 bytes */
+
+    tm_unz tmu_date;
+} unz_file_info64;
+
+typedef struct unz_file_info_s
+{
+    uLong version;              /* version made by                 2 bytes */
+    uLong version_needed;       /* version needed to extract       2 bytes */
+    uLong flag;                 /* general purpose bit flag        2 bytes */
+    uLong compression_method;   /* compression method              2 bytes */
+    uLong dosDate;              /* last mod file date in Dos fmt   4 bytes */
+    uLong crc;                  /* crc-32                          4 bytes */
+    uLong compressed_size;      /* compressed size                 4 bytes */
+    uLong uncompressed_size;    /* uncompressed size               4 bytes */
+    uLong size_filename;        /* filename length                 2 bytes */
+    uLong size_file_extra;      /* extra field length              2 bytes */
+    uLong size_file_comment;    /* file comment length             2 bytes */
+
+    uLong disk_num_start;       /* disk number start               2 bytes */
+    uLong internal_fa;          /* internal file attributes        2 bytes */
+    uLong external_fa;          /* external file attributes        4 bytes */
+
+    tm_unz tmu_date;
+} unz_file_info;
+
+extern int ZEXPORT unzStringFileNameCompare(const char* fileName1,
+                                            const char* fileName2,
+                                            int iCaseSensitivity);
+/*
+   Compare two filenames (fileName1,fileName2).
+   If iCaseSensitivity = 1, comparison is case sensitive (like strcmp)
+   If iCaseSensitivity = 2, comparison is not case sensitive (like strcmpi
+                                or strcasecmp)
+   If iCaseSensitivity = 0, case sensitivity is default of your operating system
+    (like 1 on Unix, 2 on Windows)
+*/
+
+
+extern unzFile ZEXPORT unzOpen(const char *path);
+extern unzFile ZEXPORT unzOpen64(const void *path);
+/*
+  Open a Zip file. path contain the full pathname (by example,
+     on a Windows XP computer "c:\\zlib\\zlib113.zip" or on an Unix computer
+     "zlib/zlib113.zip".
+     If the zipfile cannot be opened (file don't exist or in not valid), the
+       return value is NULL.
+     Else, the return value is a unzFile Handle, usable with other function
+       of this unzip package.
+     the "64" function take a const void* pointer, because the path is just the
+       value passed to the open64_file_func callback.
+     Under Windows, if UNICODE is defined, using fill_fopen64_filefunc, the path
+       is a pointer to a wide unicode string (LPCTSTR is LPCWSTR), so const char*
+       does not describe the reality
+*/
+
+
+extern unzFile ZEXPORT unzOpen2(const char *path,
+                                zlib_filefunc_def* pzlib_filefunc_def);
+/*
+   Open a Zip file, like unzOpen, but provide a set of file low level API
+      for read/write the zip file (see ioapi.h)
+*/
+
+extern unzFile ZEXPORT unzOpen2_64(const void *path,
+                                   zlib_filefunc64_def* pzlib_filefunc_def);
+/*
+   Open a Zip file, like unz64Open, but provide a set of file low level API
+      for read/write the zip file (see ioapi.h)
+*/
+
+extern int ZEXPORT unzClose(unzFile file);
+/*
+  Close a ZipFile opened with unzOpen.
+  If there is files inside the .Zip opened with unzOpenCurrentFile (see later),
+    these files MUST be closed with unzCloseCurrentFile before call unzClose.
+  return UNZ_OK if there is no problem. */
+
+extern int ZEXPORT unzGetGlobalInfo(unzFile file,
+                                    unz_global_info *pglobal_info);
+
+extern int ZEXPORT unzGetGlobalInfo64(unzFile file,
+                                      unz_global_info64 *pglobal_info);
+/*
+  Write info about the ZipFile in the *pglobal_info structure.
+  No preparation of the structure is needed
+  return UNZ_OK if there is no problem. */
+
+
+extern int ZEXPORT unzGetGlobalComment(unzFile file,
+                                       char *szComment,
+                                       uLong uSizeBuf);
+/*
+  Get the global comment string of the ZipFile, in the szComment buffer.
+  uSizeBuf is the size of the szComment buffer.
+  return the number of byte copied or an error code <0
+*/
+
+
+/***************************************************************************/
+/* Unzip package allow you browse the directory of the zipfile */
+
+extern int ZEXPORT unzGoToFirstFile(unzFile file);
+/*
+  Set the current file of the zipfile to the first file.
+  return UNZ_OK if there is no problem
+*/
+
+extern int ZEXPORT unzGoToNextFile(unzFile file);
+/*
+  Set the current file of the zipfile to the next file.
+  return UNZ_OK if there is no problem
+  return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.
+*/
+
+extern int ZEXPORT unzLocateFile(unzFile file,
+                                 const char *szFileName,
+                                 int iCaseSensitivity);
+/*
+  Try locate the file szFileName in the zipfile.
+  For the iCaseSensitivity signification, see unzStringFileNameCompare
+
+  return value :
+  UNZ_OK if the file is found. It becomes the current file.
+  UNZ_END_OF_LIST_OF_FILE if the file is not found
+*/
+
+
+/* ****************************************** */
+/* Ryan supplied functions */
+/* unz_file_info contain information about a file in the zipfile */
+typedef struct unz_file_pos_s
+{
+    uLong pos_in_zip_directory;   /* offset in zip file directory */
+    uLong num_of_file;            /* # of file */
+} unz_file_pos;
+
+extern int ZEXPORT unzGetFilePos(
+    unzFile file,
+    unz_file_pos* file_pos);
+
+extern int ZEXPORT unzGoToFilePos(
+    unzFile file,
+    unz_file_pos* file_pos);
+
+typedef struct unz64_file_pos_s
+{
+    ZPOS64_T pos_in_zip_directory;   /* offset in zip file directory */
+    ZPOS64_T num_of_file;            /* # of file */
+} unz64_file_pos;
+
+extern int ZEXPORT unzGetFilePos64(
+    unzFile file,
+    unz64_file_pos* file_pos);
+
+extern int ZEXPORT unzGoToFilePos64(
+    unzFile file,
+    const unz64_file_pos* file_pos);
+
+/* ****************************************** */
+
+extern int ZEXPORT unzGetCurrentFileInfo64(unzFile file,
+                                           unz_file_info64 *pfile_info,
+                                           char *szFileName,
+                                           uLong fileNameBufferSize,
+                                           void *extraField,
+                                           uLong extraFieldBufferSize,
+                                           char *szComment,
+                                           uLong commentBufferSize);
+
+extern int ZEXPORT unzGetCurrentFileInfo(unzFile file,
+                                         unz_file_info *pfile_info,
+                                         char *szFileName,
+                                         uLong fileNameBufferSize,
+                                         void *extraField,
+                                         uLong extraFieldBufferSize,
+                                         char *szComment,
+                                         uLong commentBufferSize);
+/*
+  Get Info about the current file
+  if pfile_info!=NULL, the *pfile_info structure will contain some info about
+        the current file
+  if szFileName!=NULL, the filemane string will be copied in szFileName
+            (fileNameBufferSize is the size of the buffer)
+  if extraField!=NULL, the extra field information will be copied in extraField
+            (extraFieldBufferSize is the size of the buffer).
+            This is the Central-header version of the extra field
+  if szComment!=NULL, the comment string of the file will be copied in szComment
+            (commentBufferSize is the size of the buffer)
+*/
+
+
+/** Addition for GDAL : START */
+
+extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64(unzFile file);
+
+/** Addition for GDAL : END */
+
+
+/***************************************************************************/
+/* for reading the content of the current zipfile, you can open it, read data
+   from it, and close it (you can close it before reading all the file)
+   */
+
+extern int ZEXPORT unzOpenCurrentFile(unzFile file);
+/*
+  Open for reading data the current file in the zipfile.
+  If there is no error, the return value is UNZ_OK.
+*/
+
+extern int ZEXPORT unzOpenCurrentFilePassword(unzFile file,
+                                              const char* password);
+/*
+  Open for reading data the current file in the zipfile.
+  password is a crypting password
+  If there is no error, the return value is UNZ_OK.
+*/
+
+extern int ZEXPORT unzOpenCurrentFile2(unzFile file,
+                                       int* method,
+                                       int* level,
+                                       int raw);
+/*
+  Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)
+    if raw==1
+  *method will receive method of compression, *level will receive level of
+     compression
+  note : you can set level parameter as NULL (if you did not want known level,
+         but you CANNOT set method parameter as NULL
+*/
+
+extern int ZEXPORT unzOpenCurrentFile3(unzFile file,
+                                       int* method,
+                                       int* level,
+                                       int raw,
+                                       const char* password);
+/*
+  Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)
+    if raw==1
+  *method will receive method of compression, *level will receive level of
+     compression
+  note : you can set level parameter as NULL (if you did not want known level,
+         but you CANNOT set method parameter as NULL
+*/
+
+
+extern int ZEXPORT unzCloseCurrentFile(unzFile file);
+/*
+  Close the file in zip opened with unzOpenCurrentFile
+  Return UNZ_CRCERROR if all the file was read but the CRC is not good
+*/
+
+extern int ZEXPORT unzReadCurrentFile(unzFile file,
+                                      voidp buf,
+                                      unsigned len);
+/*
+  Read bytes from the current file (opened by unzOpenCurrentFile)
+  buf contain buffer where data must be copied
+  len the size of buf.
+
+  return the number of byte copied if some bytes are copied
+  return 0 if the end of file was reached
+  return <0 with error code if there is an error
+    (UNZ_ERRNO for IO error, or zLib error for uncompress error)
+*/
+
+extern z_off_t ZEXPORT unztell(unzFile file);
+
+extern ZPOS64_T ZEXPORT unztell64(unzFile file);
+/*
+  Give the current position in uncompressed data
+*/
+
+extern int ZEXPORT unzeof(unzFile file);
+/*
+  return 1 if the end of file was reached, 0 elsewhere
+*/
+
+extern int ZEXPORT unzGetLocalExtrafield(unzFile file,
+                                         voidp buf,
+                                         unsigned len);
+/*
+  Read extra field from the current file (opened by unzOpenCurrentFile)
+  This is the local-header version of the extra field (sometimes, there is
+    more info in the local-header version than in the central-header)
+
+  if buf==NULL, it return the size of the local extra field
+
+  if buf!=NULL, len is the size of the buffer, the extra header is copied in
+    buf.
+  the return value is the number of bytes copied in buf, or (if <0)
+    the error code
+*/
+
+/***************************************************************************/
+
+/* Get the current file offset */
+extern ZPOS64_T ZEXPORT unzGetOffset64 (unzFile file);
+extern uLong ZEXPORT unzGetOffset (unzFile file);
+
+/* Set the current file offset */
+extern int ZEXPORT unzSetOffset64 (unzFile file, ZPOS64_T pos);
+extern int ZEXPORT unzSetOffset (unzFile file, uLong pos);
+
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _unz64_H */
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/zip.c b/OrthancFramework/Resources/ThirdParty/minizip/zip.c
new file mode 100644
index 0000000..e2e9da0
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/zip.c
@@ -0,0 +1,1956 @@
+/* zip.c -- IO on .zip files using zlib
+   Version 1.1, February 14h, 2010
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+         Changes
+   Oct-2009 - Mathias Svensson - Remove old C style function prototypes
+   Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives
+   Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions.
+   Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data
+                                 It is used when recreating zip archive with RAW when deleting items from a zip.
+                                 ZIP64 data is automatically added to items that needs it, and existing ZIP64 data need to be removed.
+   Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required)
+   Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
+
+*/
+
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "zlib.h"
+#include "zip.h"
+
+#ifdef STDC
+#  include 
+#endif
+#ifdef NO_ERRNO_H
+    extern int errno;
+#else
+#   include 
+#endif
+
+
+#ifndef local
+#  define local static
+#endif
+/* compile with -Dlocal if your debugger can't find static symbols */
+
+#ifndef VERSIONMADEBY
+# define VERSIONMADEBY   (0x0) /* platform dependent */
+#endif
+
+#ifndef Z_BUFSIZE
+#define Z_BUFSIZE (64*1024) //(16384)
+#endif
+
+#ifndef Z_MAXFILENAMEINZIP
+#define Z_MAXFILENAMEINZIP (256)
+#endif
+
+#ifndef ALLOC
+# define ALLOC(size) (malloc(size))
+#endif
+
+/*
+#define SIZECENTRALDIRITEM (0x2e)
+#define SIZEZIPLOCALHEADER (0x1e)
+*/
+
+/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */
+
+
+// NOT sure that this work on ALL platform
+#define MAKEULONG64(a, b) ((ZPOS64_T)(((unsigned long)(a)) | ((ZPOS64_T)((unsigned long)(b))) << 32))
+
+#ifndef SEEK_CUR
+#define SEEK_CUR    1
+#endif
+
+#ifndef SEEK_END
+#define SEEK_END    2
+#endif
+
+#ifndef SEEK_SET
+#define SEEK_SET    0
+#endif
+
+#ifndef DEF_MEM_LEVEL
+#if MAX_MEM_LEVEL >= 8
+#  define DEF_MEM_LEVEL 8
+#else
+#  define DEF_MEM_LEVEL  MAX_MEM_LEVEL
+#endif
+#endif
+const char zip_copyright[] =" zip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll";
+
+
+#define SIZEDATA_INDATABLOCK (4096-(4*4))
+
+#define LOCALHEADERMAGIC    (0x04034b50)
+#define CENTRALHEADERMAGIC  (0x02014b50)
+#define ENDHEADERMAGIC      (0x06054b50)
+#define ZIP64ENDHEADERMAGIC      (0x6064b50)
+#define ZIP64ENDLOCHEADERMAGIC   (0x7064b50)
+
+#define FLAG_LOCALHEADER_OFFSET (0x06)
+#define CRC_LOCALHEADER_OFFSET  (0x0e)
+
+#define SIZECENTRALHEADER (0x2e) /* 46 */
+
+typedef struct linkedlist_datablock_internal_s
+{
+  struct linkedlist_datablock_internal_s* next_datablock;
+  uLong  avail_in_this_block;
+  uLong  filled_in_this_block;
+  uLong  unused; /* for future use and alignment */
+  unsigned char data[SIZEDATA_INDATABLOCK];
+} linkedlist_datablock_internal;
+
+typedef struct linkedlist_data_s
+{
+    linkedlist_datablock_internal* first_block;
+    linkedlist_datablock_internal* last_block;
+} linkedlist_data;
+
+
+typedef struct
+{
+    z_stream stream;            /* zLib stream structure for inflate */
+#ifdef HAVE_BZIP2
+    bz_stream bstream;          /* bzLib stream structure for bziped */
+#endif
+
+    int  stream_initialised;    /* 1 is stream is initialised */
+    uInt pos_in_buffered_data;  /* last written byte in buffered_data */
+
+    ZPOS64_T pos_local_header;     /* offset of the local header of the file
+                                     currently writing */
+    char* central_header;       /* central header data for the current file */
+    uLong size_centralExtra;
+    uLong size_centralheader;   /* size of the central header for cur file */
+    uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */
+    uLong flag;                 /* flag of the file currently writing */
+
+    int  method;                /* compression method of file currently wr.*/
+    int  raw;                   /* 1 for directly writing raw data */
+    Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/
+    uLong dosDate;
+    uLong crc32;
+    int  encrypt;
+    int  zip64;               /* Add ZIP64 extended information in the extra field */
+    ZPOS64_T pos_zip64extrainfo;
+    ZPOS64_T totalCompressedData;
+    ZPOS64_T totalUncompressedData;
+#ifndef NOCRYPT
+    unsigned long keys[3];     /* keys defining the pseudo-random sequence */
+    const z_crc_t* pcrc_32_tab;
+    unsigned crypt_header_size;
+#endif
+} curfile64_info;
+
+typedef struct
+{
+    zlib_filefunc64_32_def z_filefunc;
+    voidpf filestream;        /* io structure of the zipfile */
+    linkedlist_data central_dir;/* datablock with central dir in construction*/
+    int  in_opened_file_inzip;  /* 1 if a file in the zip is currently writ.*/
+    curfile64_info ci;            /* info on the file currently writing */
+
+    ZPOS64_T begin_pos;            /* position of the beginning of the zipfile */
+    ZPOS64_T add_position_when_writing_offset;
+    ZPOS64_T number_entry;
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+    char *globalcomment;
+#endif
+
+} zip64_internal;
+
+
+#ifndef NOCRYPT
+#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED
+#include "crypt.h"
+#endif
+
+local linkedlist_datablock_internal* allocate_new_datablock(void) {
+    linkedlist_datablock_internal* ldi;
+    ldi = (linkedlist_datablock_internal*)
+                 ALLOC(sizeof(linkedlist_datablock_internal));
+    if (ldi!=NULL)
+    {
+        ldi->next_datablock = NULL ;
+        ldi->filled_in_this_block = 0 ;
+        ldi->avail_in_this_block = SIZEDATA_INDATABLOCK ;
+    }
+    return ldi;
+}
+
+local void free_datablock(linkedlist_datablock_internal* ldi) {
+    while (ldi!=NULL)
+    {
+        linkedlist_datablock_internal* ldinext = ldi->next_datablock;
+        free(ldi);
+        ldi = ldinext;
+    }
+}
+
+local void init_linkedlist(linkedlist_data* ll) {
+    ll->first_block = ll->last_block = NULL;
+}
+
+local void free_linkedlist(linkedlist_data* ll) {
+    free_datablock(ll->first_block);
+    ll->first_block = ll->last_block = NULL;
+}
+
+
+local int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len) {
+    linkedlist_datablock_internal* ldi;
+    const unsigned char* from_copy;
+
+    if (ll==NULL)
+        return ZIP_INTERNALERROR;
+
+    if (ll->last_block == NULL)
+    {
+        ll->first_block = ll->last_block = allocate_new_datablock();
+        if (ll->first_block == NULL)
+            return ZIP_INTERNALERROR;
+    }
+
+    ldi = ll->last_block;
+    from_copy = (const unsigned char*)buf;
+
+    while (len>0)
+    {
+        uInt copy_this;
+        uInt i;
+        unsigned char* to_copy;
+
+        if (ldi->avail_in_this_block==0)
+        {
+            ldi->next_datablock = allocate_new_datablock();
+            if (ldi->next_datablock == NULL)
+                return ZIP_INTERNALERROR;
+            ldi = ldi->next_datablock ;
+            ll->last_block = ldi;
+        }
+
+        if (ldi->avail_in_this_block < len)
+            copy_this = (uInt)ldi->avail_in_this_block;
+        else
+            copy_this = (uInt)len;
+
+        to_copy = &(ldi->data[ldi->filled_in_this_block]);
+
+        for (i=0;ifilled_in_this_block += copy_this;
+        ldi->avail_in_this_block -= copy_this;
+        from_copy += copy_this ;
+        len -= copy_this;
+    }
+    return ZIP_OK;
+}
+
+
+
+/****************************************************************************/
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+/* ===========================================================================
+   Inputs a long in LSB order to the given file
+   nbByte == 1, 2 ,4 or 8 (byte, short or long, ZPOS64_T)
+*/
+
+local int zip64local_putValue(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte) {
+    unsigned char buf[8];
+    int n;
+    for (n = 0; n < nbByte; n++)
+    {
+        buf[n] = (unsigned char)(x & 0xff);
+        x >>= 8;
+    }
+    if (x != 0)
+      {     /* data overflow - hack for ZIP64 (X Roche) */
+      for (n = 0; n < nbByte; n++)
+        {
+          buf[n] = 0xff;
+        }
+      }
+
+    if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,(uLong)nbByte)!=(uLong)nbByte)
+        return ZIP_ERRNO;
+    else
+        return ZIP_OK;
+}
+
+local void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte) {
+    unsigned char* buf=(unsigned char*)dest;
+    int n;
+    for (n = 0; n < nbByte; n++) {
+        buf[n] = (unsigned char)(x & 0xff);
+        x >>= 8;
+    }
+
+    if (x != 0)
+    {     /* data overflow - hack for ZIP64 */
+       for (n = 0; n < nbByte; n++)
+       {
+          buf[n] = 0xff;
+       }
+    }
+}
+
+/****************************************************************************/
+
+
+local uLong zip64local_TmzDateToDosDate(const tm_zip* ptm) {
+    uLong year = (uLong)ptm->tm_year;
+    if (year>=1980)
+        year-=1980;
+    else if (year>=80)
+        year-=80;
+    return
+      (uLong) (((uLong)(ptm->tm_mday) + (32 * (uLong)(ptm->tm_mon+1)) + (512 * year)) << 16) |
+        (((uLong)ptm->tm_sec/2) + (32 * (uLong)ptm->tm_min) + (2048 * (uLong)ptm->tm_hour));
+}
+
+
+/****************************************************************************/
+
+local int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int* pi) {
+    unsigned char c;
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);
+    if (err==1)
+    {
+        *pi = (int)c;
+        return ZIP_OK;
+    }
+    else
+    {
+        if (ZERROR64(*pzlib_filefunc_def,filestream))
+            return ZIP_ERRNO;
+        else
+            return ZIP_EOF;
+    }
+}
+
+
+/* ===========================================================================
+   Reads a long in LSB order from the given gz_stream. Sets
+*/
+local int zip64local_getShort(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) {
+    uLong x ;
+    int i = 0;
+    int err;
+
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x = (uLong)i;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<8;
+
+    if (err==ZIP_OK)
+        *pX = x;
+    else
+        *pX = 0;
+    return err;
+}
+
+local int zip64local_getLong(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX) {
+    uLong x ;
+    int i = 0;
+    int err;
+
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x = (uLong)i;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<8;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<16;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<24;
+
+    if (err==ZIP_OK)
+        *pX = x;
+    else
+        *pX = 0;
+    return err;
+}
+
+
+local int zip64local_getLong64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX) {
+  ZPOS64_T x;
+  int i = 0;
+  int err;
+
+  err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x = (ZPOS64_T)i;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<8;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<16;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<24;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<32;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<40;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<48;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<56;
+
+  if (err==ZIP_OK)
+    *pX = x;
+  else
+    *pX = 0;
+
+  return err;
+}
+
+#ifndef BUFREADCOMMENT
+#define BUFREADCOMMENT (0x400)
+#endif
+/*
+  Locate the Central directory of a zipfile (at the end, just before
+    the global comment)
+*/
+local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {
+  unsigned char* buf;
+  ZPOS64_T uSizeFile;
+  ZPOS64_T uBackRead;
+  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+  ZPOS64_T uPosFound=0;
+
+  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+    return 0;
+
+
+  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+  if (uMaxBack>uSizeFile)
+    uMaxBack = uSizeFile;
+
+  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+  if (buf==NULL)
+    return 0;
+
+  uBackRead = 4;
+  while (uBackReaduMaxBack)
+      uBackRead = uMaxBack;
+    else
+      uBackRead+=BUFREADCOMMENT;
+    uReadPos = uSizeFile-uBackRead ;
+
+    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+      break;
+
+    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+      break;
+
+    for (i=(int)uReadSize-3; (i--)>0;)
+      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
+        ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
+      {
+        uPosFound = uReadPos+(unsigned)i;
+        break;
+      }
+
+    if (uPosFound!=0)
+      break;
+  }
+  free(buf);
+  return uPosFound;
+}
+
+/*
+Locate the End of Zip64 Central directory locator and from there find the CD of a zipfile (at the end, just before
+the global comment)
+*/
+local ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) {
+  unsigned char* buf;
+  ZPOS64_T uSizeFile;
+  ZPOS64_T uBackRead;
+  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+  ZPOS64_T uPosFound=0;
+  uLong uL;
+  ZPOS64_T relativeOffset;
+
+  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+    return 0;
+
+  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+  if (uMaxBack>uSizeFile)
+    uMaxBack = uSizeFile;
+
+  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+  if (buf==NULL)
+    return 0;
+
+  uBackRead = 4;
+  while (uBackReaduMaxBack)
+      uBackRead = uMaxBack;
+    else
+      uBackRead+=BUFREADCOMMENT;
+    uReadPos = uSizeFile-uBackRead ;
+
+    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+      break;
+
+    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+      break;
+
+    for (i=(int)uReadSize-3; (i--)>0;)
+    {
+      // Signature "0x07064b50" Zip64 end of central directory locater
+      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
+      {
+        uPosFound = uReadPos+(unsigned)i;
+        break;
+      }
+    }
+
+      if (uPosFound!=0)
+        break;
+  }
+
+  free(buf);
+  if (uPosFound == 0)
+    return 0;
+
+  /* Zip64 end of central directory locator */
+  if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)
+    return 0;
+
+  /* the signature, already checked */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+
+  /* number of the disk with the start of the zip64 end of  central directory */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+  if (uL != 0)
+    return 0;
+
+  /* relative offset of the zip64 end of central directory record */
+  if (zip64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=ZIP_OK)
+    return 0;
+
+  /* total number of disks */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+  if (uL != 1)
+    return 0;
+
+  /* Goto Zip64 end of central directory record */
+  if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)
+    return 0;
+
+  /* the signature */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+
+  if (uL != 0x06064b50) // signature of 'Zip64 end of central directory'
+    return 0;
+
+  return relativeOffset;
+}
+
+local int LoadCentralDirectoryRecord(zip64_internal* pziinit) {
+  int err=ZIP_OK;
+  ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
+
+  ZPOS64_T size_central_dir;     /* size of the central directory  */
+  ZPOS64_T offset_central_dir;   /* offset of start of central directory */
+  ZPOS64_T central_pos;
+  uLong uL;
+
+  uLong number_disk;          /* number of the current disk, used for
+                              spanning ZIP, unsupported, always 0*/
+  uLong number_disk_with_CD;  /* number of the disk with central dir, used
+                              for spanning ZIP, unsupported, always 0*/
+  ZPOS64_T number_entry;
+  ZPOS64_T number_entry_CD;      /* total number of entries in
+                                the central dir
+                                (same than number_entry on nospan) */
+  uLong VersionMadeBy;
+  uLong VersionNeeded;
+  uLong size_comment;
+
+  int hasZIP64Record = 0;
+
+  // check first if we find a ZIP64 record
+  central_pos = zip64local_SearchCentralDir64(&pziinit->z_filefunc,pziinit->filestream);
+  if(central_pos > 0)
+  {
+    hasZIP64Record = 1;
+  }
+  else if(central_pos == 0)
+  {
+    central_pos = zip64local_SearchCentralDir(&pziinit->z_filefunc,pziinit->filestream);
+  }
+
+/* disable to allow appending to empty ZIP archive
+        if (central_pos==0)
+            err=ZIP_ERRNO;
+*/
+
+  if(hasZIP64Record)
+  {
+    ZPOS64_T sizeEndOfCentralDirectory;
+    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos, ZLIB_FILEFUNC_SEEK_SET) != 0)
+      err=ZIP_ERRNO;
+
+    /* the signature, already checked */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* size of zip64 end of central directory record */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &sizeEndOfCentralDirectory)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* version made by */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionMadeBy)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* version needed to extract */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionNeeded)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of this disk */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of the disk with the start of the central directory */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* total number of entries in the central directory on this disk */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &number_entry)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* total number of entries in the central directory */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&number_entry_CD)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
+      err=ZIP_BADZIPFILE;
+
+    /* size of the central directory */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&size_central_dir)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* offset of start of central directory with respect to the
+    starting disk number */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&offset_central_dir)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    // TODO..
+    // read the comment from the standard central header.
+    size_comment = 0;
+  }
+  else
+  {
+    // Read End of central Directory info
+    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+      err=ZIP_ERRNO;
+
+    /* the signature, already checked */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of this disk */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of the disk with the start of the central directory */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* total number of entries in the central dir on this disk */
+    number_entry = 0;
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      number_entry = uL;
+
+    /* total number of entries in the central dir */
+    number_entry_CD = 0;
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      number_entry_CD = uL;
+
+    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
+      err=ZIP_BADZIPFILE;
+
+    /* size of the central directory */
+    size_central_dir = 0;
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      size_central_dir = uL;
+
+    /* offset of start of central directory with respect to the starting disk number */
+    offset_central_dir = 0;
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      offset_central_dir = uL;
+
+
+    /* zipfile global comment length */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &size_comment)!=ZIP_OK)
+      err=ZIP_ERRNO;
+  }
+
+  if ((central_posz_filefunc, pziinit->filestream);
+    return ZIP_ERRNO;
+  }
+
+  if (size_comment>0)
+  {
+    pziinit->globalcomment = (char*)ALLOC(size_comment+1);
+    if (pziinit->globalcomment)
+    {
+      size_comment = ZREAD64(pziinit->z_filefunc, pziinit->filestream, pziinit->globalcomment,size_comment);
+      pziinit->globalcomment[size_comment]=0;
+    }
+  }
+
+  byte_before_the_zipfile = central_pos - (offset_central_dir+size_central_dir);
+  pziinit->add_position_when_writing_offset = byte_before_the_zipfile;
+
+  {
+    ZPOS64_T size_central_dir_to_read = size_central_dir;
+    size_t buf_size = SIZEDATA_INDATABLOCK;
+    void* buf_read = (void*)ALLOC(buf_size);
+    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir + byte_before_the_zipfile, ZLIB_FILEFUNC_SEEK_SET) != 0)
+      err=ZIP_ERRNO;
+
+    while ((size_central_dir_to_read>0) && (err==ZIP_OK))
+    {
+      ZPOS64_T read_this = SIZEDATA_INDATABLOCK;
+      if (read_this > size_central_dir_to_read)
+        read_this = size_central_dir_to_read;
+
+      if (ZREAD64(pziinit->z_filefunc, pziinit->filestream,buf_read,(uLong)read_this) != read_this)
+        err=ZIP_ERRNO;
+
+      if (err==ZIP_OK)
+        err = add_data_in_datablock(&pziinit->central_dir,buf_read, (uLong)read_this);
+
+      size_central_dir_to_read-=read_this;
+    }
+    free(buf_read);
+  }
+  pziinit->begin_pos = byte_before_the_zipfile;
+  pziinit->number_entry = number_entry_CD;
+
+  if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir+byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET) != 0)
+    err=ZIP_ERRNO;
+
+  return err;
+}
+
+
+#endif /* !NO_ADDFILEINEXISTINGZIP*/
+
+
+/************************************************************/
+extern zipFile ZEXPORT zipOpen3(const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def) {
+    zip64_internal ziinit;
+    zip64_internal* zi;
+    int err=ZIP_OK;
+
+    ziinit.z_filefunc.zseek32_file = NULL;
+    ziinit.z_filefunc.ztell32_file = NULL;
+    if (pzlib_filefunc64_32_def==NULL)
+        fill_fopen64_filefunc(&ziinit.z_filefunc.zfile_func64);
+    else
+        ziinit.z_filefunc = *pzlib_filefunc64_32_def;
+
+    ziinit.filestream = ZOPEN64(ziinit.z_filefunc,
+                  pathname,
+                  (append == APPEND_STATUS_CREATE) ?
+                  (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_CREATE) :
+                    (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_EXISTING));
+
+    if (ziinit.filestream == NULL)
+        return NULL;
+
+    if (append == APPEND_STATUS_CREATEAFTER)
+        ZSEEK64(ziinit.z_filefunc,ziinit.filestream,0,SEEK_END);
+
+    ziinit.begin_pos = ZTELL64(ziinit.z_filefunc,ziinit.filestream);
+    ziinit.in_opened_file_inzip = 0;
+    ziinit.ci.stream_initialised = 0;
+    ziinit.number_entry = 0;
+    ziinit.add_position_when_writing_offset = 0;
+    init_linkedlist(&(ziinit.central_dir));
+
+
+
+    zi = (zip64_internal*)ALLOC(sizeof(zip64_internal));
+    if (zi==NULL)
+    {
+        ZCLOSE64(ziinit.z_filefunc,ziinit.filestream);
+        return NULL;
+    }
+
+    /* now we add file in a zipfile */
+#    ifndef NO_ADDFILEINEXISTINGZIP
+    ziinit.globalcomment = NULL;
+    if (append == APPEND_STATUS_ADDINZIP)
+    {
+      // Read and Cache Central Directory Records
+      err = LoadCentralDirectoryRecord(&ziinit);
+    }
+
+    if (globalcomment)
+    {
+      *globalcomment = ziinit.globalcomment;
+    }
+#    endif /* !NO_ADDFILEINEXISTINGZIP*/
+
+    if (err != ZIP_OK)
+    {
+#    ifndef NO_ADDFILEINEXISTINGZIP
+        free(ziinit.globalcomment);
+#    endif /* !NO_ADDFILEINEXISTINGZIP*/
+        free(zi);
+        return NULL;
+    }
+    else
+    {
+        *zi = ziinit;
+        return (zipFile)zi;
+    }
+}
+
+extern zipFile ZEXPORT zipOpen2(const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def) {
+    if (pzlib_filefunc32_def != NULL)
+    {
+        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+        fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def);
+        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
+    }
+    else
+        return zipOpen3(pathname, append, globalcomment, NULL);
+}
+
+extern zipFile ZEXPORT zipOpen2_64(const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def) {
+    if (pzlib_filefunc_def != NULL)
+    {
+        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+        zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;
+        zlib_filefunc64_32_def_fill.ztell32_file = NULL;
+        zlib_filefunc64_32_def_fill.zseek32_file = NULL;
+        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
+    }
+    else
+        return zipOpen3(pathname, append, globalcomment, NULL);
+}
+
+
+
+extern zipFile ZEXPORT zipOpen(const char* pathname, int append) {
+    return zipOpen3((const void*)pathname,append,NULL,NULL);
+}
+
+extern zipFile ZEXPORT zipOpen64(const void* pathname, int append) {
+    return zipOpen3(pathname,append,NULL,NULL);
+}
+
+local int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local) {
+  /* write the local header */
+  int err;
+  uInt size_filename = (uInt)strlen(filename);
+  uInt size_extrafield = size_extrafield_local;
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)LOCALHEADERMAGIC, 4);
+
+  if (err==ZIP_OK)
+  {
+    if(zi->ci.zip64)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);/* version needed to extract */
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)20,2);/* version needed to extract */
+  }
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.flag,2);
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.method,2);
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.dosDate,4);
+
+  // CRC / Compressed size / Uncompressed size will be filled in later and rewritten later
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* crc 32, unknown */
+  if (err==ZIP_OK)
+  {
+    if(zi->ci.zip64)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* compressed size, unknown */
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* compressed size, unknown */
+  }
+  if (err==ZIP_OK)
+  {
+    if(zi->ci.zip64)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* uncompressed size, unknown */
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* uncompressed size, unknown */
+  }
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_filename,2);
+
+  if(zi->ci.zip64)
+  {
+    size_extrafield += 20;
+  }
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_extrafield,2);
+
+  if ((err==ZIP_OK) && (size_filename > 0))
+  {
+    if (ZWRITE64(zi->z_filefunc,zi->filestream,filename,size_filename)!=size_filename)
+      err = ZIP_ERRNO;
+  }
+
+  if ((err==ZIP_OK) && (size_extrafield_local > 0))
+  {
+    if (ZWRITE64(zi->z_filefunc, zi->filestream, extrafield_local, size_extrafield_local) != size_extrafield_local)
+      err = ZIP_ERRNO;
+  }
+
+
+  if ((err==ZIP_OK) && (zi->ci.zip64))
+  {
+      // write the Zip64 extended info
+      short HeaderID = 1;
+      short DataSize = 16;
+      ZPOS64_T CompressedSize = 0;
+      ZPOS64_T UncompressedSize = 0;
+
+      // Remember position of Zip64 extended info for the local file header. (needed when we update size after done with file)
+      zi->ci.pos_zip64extrainfo = ZTELL64(zi->z_filefunc,zi->filestream);
+
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)HeaderID,2);
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)DataSize,2);
+
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)UncompressedSize,8);
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)CompressedSize,8);
+  }
+
+  return err;
+}
+
+/*
+ NOTE.
+ When writing RAW the ZIP64 extended information in extrafield_local and extrafield_global needs to be stripped
+ before calling this function it can be done with zipRemoveExtraInfoBlock
+
+ It is not done here because then we need to realloc a new buffer since parameters are 'const' and I want to minimize
+ unnecessary allocations.
+ */
+extern int ZEXPORT zipOpenNewFileInZip4_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                           const void* extrafield_local, uInt size_extrafield_local,
+                                           const void* extrafield_global, uInt size_extrafield_global,
+                                           const char* comment, int method, int level, int raw,
+                                           int windowBits,int memLevel, int strategy,
+                                           const char* password, uLong crcForCrypting,
+                                           uLong versionMadeBy, uLong flagBase, int zip64) {
+    zip64_internal* zi;
+    uInt size_filename;
+    uInt size_comment;
+    uInt i;
+    int err = ZIP_OK;
+
+#    ifdef NOCRYPT
+    (crcForCrypting);
+    if (password != NULL)
+        return ZIP_PARAMERROR;
+#    endif
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+
+#ifdef HAVE_BZIP2
+    if ((method!=0) && (method!=Z_DEFLATED) && (method!=Z_BZIP2ED))
+      return ZIP_PARAMERROR;
+#else
+    if ((method!=0) && (method!=Z_DEFLATED))
+      return ZIP_PARAMERROR;
+#endif
+
+    // The filename and comment length must fit in 16 bits.
+    if ((filename!=NULL) && (strlen(filename)>0xffff))
+        return ZIP_PARAMERROR;
+    if ((comment!=NULL) && (strlen(comment)>0xffff))
+        return ZIP_PARAMERROR;
+    // The extra field length must fit in 16 bits. If the member also requires
+    // a Zip64 extra block, that will also need to fit within that 16-bit
+    // length, but that will be checked for later.
+    if ((size_extrafield_local>0xffff) || (size_extrafield_global>0xffff))
+        return ZIP_PARAMERROR;
+
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 1)
+    {
+        err = zipCloseFileInZip (file);
+        if (err != ZIP_OK)
+            return err;
+    }
+
+    if (filename==NULL)
+        filename="-";
+
+    if (comment==NULL)
+        size_comment = 0;
+    else
+        size_comment = (uInt)strlen(comment);
+
+    size_filename = (uInt)strlen(filename);
+
+    if (zipfi == NULL)
+        zi->ci.dosDate = 0;
+    else
+    {
+        if (zipfi->dosDate != 0)
+            zi->ci.dosDate = zipfi->dosDate;
+        else
+          zi->ci.dosDate = zip64local_TmzDateToDosDate(&zipfi->tmz_date);
+    }
+
+    zi->ci.flag = flagBase;
+    if ((level==8) || (level==9))
+      zi->ci.flag |= 2;
+    if (level==2)
+      zi->ci.flag |= 4;
+    if (level==1)
+      zi->ci.flag |= 6;
+    if (password != NULL)
+      zi->ci.flag |= 1;
+
+    zi->ci.crc32 = 0;
+    zi->ci.method = method;
+    zi->ci.encrypt = 0;
+    zi->ci.stream_initialised = 0;
+    zi->ci.pos_in_buffered_data = 0;
+    zi->ci.raw = raw;
+    zi->ci.pos_local_header = ZTELL64(zi->z_filefunc,zi->filestream);
+
+    zi->ci.size_centralheader = SIZECENTRALHEADER + size_filename + size_extrafield_global + size_comment;
+    zi->ci.size_centralExtraFree = 32; // Extra space we have reserved in case we need to add ZIP64 extra info data
+
+    zi->ci.central_header = (char*)ALLOC((uInt)zi->ci.size_centralheader + zi->ci.size_centralExtraFree);
+
+    zi->ci.size_centralExtra = size_extrafield_global;
+    zip64local_putValue_inmemory(zi->ci.central_header,(uLong)CENTRALHEADERMAGIC,4);
+    /* version info */
+    zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)versionMadeBy,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)20,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+8,(uLong)zi->ci.flag,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+10,(uLong)zi->ci.method,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+12,(uLong)zi->ci.dosDate,4);
+    zip64local_putValue_inmemory(zi->ci.central_header+16,(uLong)0,4); /*crc*/
+    zip64local_putValue_inmemory(zi->ci.central_header+20,(uLong)0,4); /*compr size*/
+    zip64local_putValue_inmemory(zi->ci.central_header+24,(uLong)0,4); /*uncompr size*/
+    zip64local_putValue_inmemory(zi->ci.central_header+28,(uLong)size_filename,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)size_extrafield_global,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+32,(uLong)size_comment,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+34,(uLong)0,2); /*disk nm start*/
+
+    if (zipfi==NULL)
+        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)0,2);
+    else
+        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)zipfi->internal_fa,2);
+
+    if (zipfi==NULL)
+        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)0,4);
+    else
+        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)zipfi->external_fa,4);
+
+    if(zi->ci.pos_local_header >= 0xffffffff)
+      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)0xffffffff,4);
+    else
+      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writing_offset,4);
+
+    for (i=0;ici.central_header+SIZECENTRALHEADER+i) = *(filename+i);
+
+    for (i=0;ici.central_header+SIZECENTRALHEADER+size_filename+i) =
+              *(((const char*)extrafield_global)+i);
+
+    for (i=0;ici.central_header+SIZECENTRALHEADER+size_filename+
+              size_extrafield_global+i) = *(comment+i);
+    if (zi->ci.central_header == NULL)
+        return ZIP_INTERNALERROR;
+
+    zi->ci.zip64 = zip64;
+    zi->ci.totalCompressedData = 0;
+    zi->ci.totalUncompressedData = 0;
+    zi->ci.pos_zip64extrainfo = 0;
+
+    err = Write_LocalFileHeader(zi, filename, size_extrafield_local, extrafield_local);
+
+#ifdef HAVE_BZIP2
+    zi->ci.bstream.avail_in = (uInt)0;
+    zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+    zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+    zi->ci.bstream.total_in_hi32 = 0;
+    zi->ci.bstream.total_in_lo32 = 0;
+    zi->ci.bstream.total_out_hi32 = 0;
+    zi->ci.bstream.total_out_lo32 = 0;
+#endif
+
+    zi->ci.stream.avail_in = (uInt)0;
+    zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+    zi->ci.stream.next_out = zi->ci.buffered_data;
+    zi->ci.stream.total_in = 0;
+    zi->ci.stream.total_out = 0;
+    zi->ci.stream.data_type = Z_BINARY;
+
+#ifdef HAVE_BZIP2
+    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED || zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+#else
+    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+#endif
+    {
+        if(zi->ci.method == Z_DEFLATED)
+        {
+          zi->ci.stream.zalloc = (alloc_func)0;
+          zi->ci.stream.zfree = (free_func)0;
+          zi->ci.stream.opaque = (voidpf)0;
+
+          if (windowBits>0)
+              windowBits = -windowBits;
+
+          err = deflateInit2(&zi->ci.stream, level, Z_DEFLATED, windowBits, memLevel, strategy);
+
+          if (err==Z_OK)
+              zi->ci.stream_initialised = Z_DEFLATED;
+        }
+        else if(zi->ci.method == Z_BZIP2ED)
+        {
+#ifdef HAVE_BZIP2
+            // Init BZip stuff here
+          zi->ci.bstream.bzalloc = 0;
+          zi->ci.bstream.bzfree = 0;
+          zi->ci.bstream.opaque = (voidpf)0;
+
+          err = BZ2_bzCompressInit(&zi->ci.bstream, level, 0,35);
+          if(err == BZ_OK)
+            zi->ci.stream_initialised = Z_BZIP2ED;
+#endif
+        }
+
+    }
+
+#    ifndef NOCRYPT
+    zi->ci.crypt_header_size = 0;
+    if ((err==Z_OK) && (password != NULL))
+    {
+        unsigned char bufHead[RAND_HEAD_LEN];
+        unsigned int sizeHead;
+        zi->ci.encrypt = 1;
+        zi->ci.pcrc_32_tab = get_crc_table();
+        /*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/
+
+        sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting);
+        zi->ci.crypt_header_size = sizeHead;
+
+        if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead)
+                err = ZIP_ERRNO;
+    }
+#    endif
+
+    if (err==Z_OK)
+        zi->in_opened_file_inzip = 1;
+    return err;
+}
+
+extern int ZEXPORT zipOpenNewFileInZip4(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void* extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int raw,
+                                        int windowBits,int memLevel, int strategy,
+                                        const char* password, uLong crcForCrypting,
+                                        uLong versionMadeBy, uLong flagBase) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   windowBits, memLevel, strategy,
+                                   password, crcForCrypting, versionMadeBy, flagBase, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip3(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void* extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int raw,
+                                        int windowBits,int memLevel, int strategy,
+                                        const char* password, uLong crcForCrypting) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   windowBits, memLevel, strategy,
+                                   password, crcForCrypting, VERSIONMADEBY, 0, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                         const void* extrafield_local, uInt size_extrafield_local,
+                                         const void* extrafield_global, uInt size_extrafield_global,
+                                         const char* comment, int method, int level, int raw,
+                                         int windowBits,int memLevel, int strategy,
+                                         const char* password, uLong crcForCrypting, int zip64) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   windowBits, memLevel, strategy,
+                                   password, crcForCrypting, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip2(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void* extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int raw) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                           const void* extrafield_local, uInt size_extrafield_local,
+                                           const void* extrafield_global, uInt size_extrafield_global,
+                                           const char* comment, int method, int level, int raw, int zip64) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, raw,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                         const void* extrafield_local, uInt size_extrafield_local,
+                                         const void*extrafield_global, uInt size_extrafield_global,
+                                         const char* comment, int method, int level, int zip64) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, 0,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                       const void* extrafield_local, uInt size_extrafield_local,
+                                       const void*extrafield_global, uInt size_extrafield_global,
+                                       const char* comment, int method, int level) {
+    return zipOpenNewFileInZip4_64(file, filename, zipfi,
+                                   extrafield_local, size_extrafield_local,
+                                   extrafield_global, size_extrafield_global,
+                                   comment, method, level, 0,
+                                   -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                   NULL, 0, VERSIONMADEBY, 0, 0);
+}
+
+local int zip64FlushWriteBuffer(zip64_internal* zi) {
+    int err=ZIP_OK;
+
+    if (zi->ci.encrypt != 0)
+    {
+#ifndef NOCRYPT
+        uInt i;
+        int t;
+        for (i=0;ici.pos_in_buffered_data;i++)
+            zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t);
+#endif
+    }
+
+    if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)
+      err = ZIP_ERRNO;
+
+    zi->ci.totalCompressedData += zi->ci.pos_in_buffered_data;
+
+#ifdef HAVE_BZIP2
+    if(zi->ci.method == Z_BZIP2ED)
+    {
+      zi->ci.totalUncompressedData += zi->ci.bstream.total_in_lo32;
+      zi->ci.bstream.total_in_lo32 = 0;
+      zi->ci.bstream.total_in_hi32 = 0;
+    }
+    else
+#endif
+    {
+      zi->ci.totalUncompressedData += zi->ci.stream.total_in;
+      zi->ci.stream.total_in = 0;
+    }
+
+
+    zi->ci.pos_in_buffered_data = 0;
+
+    return err;
+}
+
+extern int ZEXPORT zipWriteInFileInZip(zipFile file, const void* buf, unsigned int len) {
+    zip64_internal* zi;
+    int err=ZIP_OK;
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 0)
+        return ZIP_PARAMERROR;
+
+    zi->ci.crc32 = crc32(zi->ci.crc32,buf,(uInt)len);
+
+#ifdef HAVE_BZIP2
+    if(zi->ci.method == Z_BZIP2ED && (!zi->ci.raw))
+    {
+      zi->ci.bstream.next_in = (void*)buf;
+      zi->ci.bstream.avail_in = len;
+      err = BZ_RUN_OK;
+
+      while ((err==BZ_RUN_OK) && (zi->ci.bstream.avail_in>0))
+      {
+        if (zi->ci.bstream.avail_out == 0)
+        {
+          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+            err = ZIP_ERRNO;
+          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+        }
+
+
+        if(err != BZ_RUN_OK)
+          break;
+
+        if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+        {
+          uLong uTotalOutBefore_lo = zi->ci.bstream.total_out_lo32;
+//          uLong uTotalOutBefore_hi = zi->ci.bstream.total_out_hi32;
+          err=BZ2_bzCompress(&zi->ci.bstream,  BZ_RUN);
+
+          zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore_lo) ;
+        }
+      }
+
+      if(err == BZ_RUN_OK)
+        err = ZIP_OK;
+    }
+    else
+#endif
+    {
+      zi->ci.stream.next_in = (Bytef*)(uintptr_t)buf;
+      zi->ci.stream.avail_in = len;
+
+      while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0))
+      {
+          if (zi->ci.stream.avail_out == 0)
+          {
+              if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+                  err = ZIP_ERRNO;
+              zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+              zi->ci.stream.next_out = zi->ci.buffered_data;
+          }
+
+
+          if(err != ZIP_OK)
+              break;
+
+          if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+          {
+              uLong uTotalOutBefore = zi->ci.stream.total_out;
+              err=deflate(&zi->ci.stream,  Z_NO_FLUSH);
+
+              zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
+          }
+          else
+          {
+              uInt copy_this,i;
+              if (zi->ci.stream.avail_in < zi->ci.stream.avail_out)
+                  copy_this = zi->ci.stream.avail_in;
+              else
+                  copy_this = zi->ci.stream.avail_out;
+
+              for (i = 0; i < copy_this; i++)
+                  *(((char*)zi->ci.stream.next_out)+i) =
+                      *(((const char*)zi->ci.stream.next_in)+i);
+              {
+                  zi->ci.stream.avail_in -= copy_this;
+                  zi->ci.stream.avail_out-= copy_this;
+                  zi->ci.stream.next_in+= copy_this;
+                  zi->ci.stream.next_out+= copy_this;
+                  zi->ci.stream.total_in+= copy_this;
+                  zi->ci.stream.total_out+= copy_this;
+                  zi->ci.pos_in_buffered_data += copy_this;
+              }
+          }
+      }// while(...)
+    }
+
+    return err;
+}
+
+extern int ZEXPORT zipCloseFileInZipRaw(zipFile file, uLong uncompressed_size, uLong crc32) {
+    return zipCloseFileInZipRaw64 (file, uncompressed_size, crc32);
+}
+
+extern int ZEXPORT zipCloseFileInZipRaw64(zipFile file, ZPOS64_T uncompressed_size, uLong crc32) {
+    zip64_internal* zi;
+    ZPOS64_T compressed_size;
+    uLong invalidValue = 0xffffffff;
+    unsigned datasize = 0;
+    int err=ZIP_OK;
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 0)
+        return ZIP_PARAMERROR;
+    zi->ci.stream.avail_in = 0;
+
+    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+                {
+                        while (err==ZIP_OK)
+                        {
+                                uLong uTotalOutBefore;
+                                if (zi->ci.stream.avail_out == 0)
+                                {
+                                        if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+                                                err = ZIP_ERRNO;
+                                        zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+                                        zi->ci.stream.next_out = zi->ci.buffered_data;
+                                }
+                                uTotalOutBefore = zi->ci.stream.total_out;
+                                err=deflate(&zi->ci.stream,  Z_FINISH);
+                                zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
+                        }
+                }
+    else if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+    {
+#ifdef HAVE_BZIP2
+      err = BZ_FINISH_OK;
+      while (err==BZ_FINISH_OK)
+      {
+        uLong uTotalOutBefore;
+        if (zi->ci.bstream.avail_out == 0)
+        {
+          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+            err = ZIP_ERRNO;
+          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+        }
+        uTotalOutBefore = zi->ci.bstream.total_out_lo32;
+        err=BZ2_bzCompress(&zi->ci.bstream,  BZ_FINISH);
+        if(err == BZ_STREAM_END)
+          err = Z_STREAM_END;
+
+        zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore);
+      }
+
+      if(err == BZ_FINISH_OK)
+        err = ZIP_OK;
+#endif
+    }
+
+    if (err==Z_STREAM_END)
+        err=ZIP_OK; /* this is normal */
+
+    if ((zi->ci.pos_in_buffered_data>0) && (err==ZIP_OK))
+                {
+        if (zip64FlushWriteBuffer(zi)==ZIP_ERRNO)
+            err = ZIP_ERRNO;
+                }
+
+    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+    {
+        int tmp_err = deflateEnd(&zi->ci.stream);
+        if (err == ZIP_OK)
+            err = tmp_err;
+        zi->ci.stream_initialised = 0;
+    }
+#ifdef HAVE_BZIP2
+    else if((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+    {
+      int tmperr = BZ2_bzCompressEnd(&zi->ci.bstream);
+                        if (err==ZIP_OK)
+                                err = tmperr;
+                        zi->ci.stream_initialised = 0;
+    }
+#endif
+
+    if (!zi->ci.raw)
+    {
+        crc32 = (uLong)zi->ci.crc32;
+        uncompressed_size = zi->ci.totalUncompressedData;
+    }
+    compressed_size = zi->ci.totalCompressedData;
+
+#    ifndef NOCRYPT
+    compressed_size += zi->ci.crypt_header_size;
+#    endif
+
+    // update Current Item crc and sizes,
+    if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)
+    {
+      /*version Made by*/
+      zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)45,2);
+      /*version needed*/
+      zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)45,2);
+
+    }
+
+    zip64local_putValue_inmemory(zi->ci.central_header+16,crc32,4); /*crc*/
+
+
+    if(compressed_size >= 0xffffffff)
+      zip64local_putValue_inmemory(zi->ci.central_header+20, invalidValue,4); /*compr size*/
+    else
+      zip64local_putValue_inmemory(zi->ci.central_header+20, compressed_size,4); /*compr size*/
+
+    /// set internal file attributes field
+    if (zi->ci.stream.data_type == Z_ASCII)
+        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)Z_ASCII,2);
+
+    if(uncompressed_size >= 0xffffffff)
+      zip64local_putValue_inmemory(zi->ci.central_header+24, invalidValue,4); /*uncompr size*/
+    else
+      zip64local_putValue_inmemory(zi->ci.central_header+24, uncompressed_size,4); /*uncompr size*/
+
+    // Add ZIP64 extra info field for uncompressed size
+    if(uncompressed_size >= 0xffffffff)
+      datasize += 8;
+
+    // Add ZIP64 extra info field for compressed size
+    if(compressed_size >= 0xffffffff)
+      datasize += 8;
+
+    // Add ZIP64 extra info field for relative offset to local file header of current file
+    if(zi->ci.pos_local_header >= 0xffffffff)
+      datasize += 8;
+
+    if(datasize > 0)
+    {
+      char* p = NULL;
+
+      if((uLong)(datasize + 4) > zi->ci.size_centralExtraFree)
+      {
+        // we can not write more data to the buffer that we have room for.
+        return ZIP_BADZIPFILE;
+      }
+
+      p = zi->ci.central_header + zi->ci.size_centralheader;
+
+      // Add Extra Information Header for 'ZIP64 information'
+      zip64local_putValue_inmemory(p, 0x0001, 2); // HeaderID
+      p += 2;
+      zip64local_putValue_inmemory(p, datasize, 2); // DataSize
+      p += 2;
+
+      if(uncompressed_size >= 0xffffffff)
+      {
+        zip64local_putValue_inmemory(p, uncompressed_size, 8);
+        p += 8;
+      }
+
+      if(compressed_size >= 0xffffffff)
+      {
+        zip64local_putValue_inmemory(p, compressed_size, 8);
+        p += 8;
+      }
+
+      if(zi->ci.pos_local_header >= 0xffffffff)
+      {
+        zip64local_putValue_inmemory(p, zi->ci.pos_local_header, 8);
+        p += 8;
+      }
+
+      // Update how much extra free space we got in the memory buffer
+      // and increase the centralheader size so the new ZIP64 fields are included
+      // ( 4 below is the size of HeaderID and DataSize field )
+      zi->ci.size_centralExtraFree -= datasize + 4;
+      zi->ci.size_centralheader += datasize + 4;
+
+      // Update the extra info size field
+      zi->ci.size_centralExtra += datasize + 4;
+      zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)zi->ci.size_centralExtra,2);
+    }
+
+    if (err==ZIP_OK)
+        err = add_data_in_datablock(&zi->central_dir, zi->ci.central_header, (uLong)zi->ci.size_centralheader);
+
+    free(zi->ci.central_header);
+
+    if (err==ZIP_OK)
+    {
+        // Update the LocalFileHeader with the new values.
+
+        ZPOS64_T cur_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
+
+        if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_local_header + 14,ZLIB_FILEFUNC_SEEK_SET)!=0)
+            err = ZIP_ERRNO;
+
+        if (err==ZIP_OK)
+            err = zip64local_putValue(&zi->z_filefunc,zi->filestream,crc32,4); /* crc 32, unknown */
+
+        if(uncompressed_size >= 0xffffffff || compressed_size >= 0xffffffff )
+        {
+          if(zi->ci.pos_zip64extrainfo > 0)
+          {
+            // Update the size in the ZIP64 extended field.
+            if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_zip64extrainfo + 4,ZLIB_FILEFUNC_SEEK_SET)!=0)
+              err = ZIP_ERRNO;
+
+            if (err==ZIP_OK) /* compressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, uncompressed_size, 8);
+
+            if (err==ZIP_OK) /* uncompressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, compressed_size, 8);
+          }
+          else
+              err = ZIP_BADZIPFILE; // Caller passed zip64 = 0, so no room for zip64 info -> fatal
+        }
+        else
+        {
+          if (err==ZIP_OK) /* compressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,compressed_size,4);
+
+          if (err==ZIP_OK) /* uncompressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,uncompressed_size,4);
+        }
+
+        if (ZSEEK64(zi->z_filefunc,zi->filestream, cur_pos_inzip,ZLIB_FILEFUNC_SEEK_SET)!=0)
+            err = ZIP_ERRNO;
+    }
+
+    zi->number_entry ++;
+    zi->in_opened_file_inzip = 0;
+
+    return err;
+}
+
+extern int ZEXPORT zipCloseFileInZip(zipFile file) {
+    return zipCloseFileInZipRaw (file,0,0);
+}
+
+local int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip) {
+  int err = ZIP_OK;
+  ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset;
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDLOCHEADERMAGIC,4);
+
+  /*num disks*/
+    if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+  /*relative offset*/
+    if (err==ZIP_OK) /* Relative offset to the Zip64EndOfCentralDirectory */
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, pos,8);
+
+  /*total disks*/ /* Do not support spawning of disk so always say 1 here*/
+    if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)1,4);
+
+    return err;
+}
+
+local int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) {
+  int err = ZIP_OK;
+
+  uLong Zip64DataSize = 44;
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDHEADERMAGIC,4);
+
+  if (err==ZIP_OK) /* size of this 'zip64 end of central directory' */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)Zip64DataSize,8); // why ZPOS64_T of this ?
+
+  if (err==ZIP_OK) /* version made by */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
+
+  if (err==ZIP_OK) /* version needed */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
+
+  if (err==ZIP_OK) /* number of this disk */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+  if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
+    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
+
+  if (err==ZIP_OK) /* total number of entries in the central dir */
+    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
+
+  if (err==ZIP_OK) /* size of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)size_centraldir,8);
+
+  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
+  {
+    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (ZPOS64_T)pos,8);
+  }
+  return err;
+}
+
+local int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip) {
+  int err = ZIP_OK;
+
+  /*signature*/
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ENDHEADERMAGIC,4);
+
+  if (err==ZIP_OK) /* number of this disk */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
+
+  if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
+
+  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
+  {
+    {
+      if(zi->number_entry >= 0xFFFF)
+        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
+      else
+        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
+    }
+  }
+
+  if (err==ZIP_OK) /* total number of entries in the central dir */
+  {
+    if(zi->number_entry >= 0xFFFF)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
+  }
+
+  if (err==ZIP_OK) /* size of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_centraldir,4);
+
+  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
+  {
+    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+    if(pos >= 0xffffffff)
+    {
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)0xffffffff,4);
+    }
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writing_offset),4);
+  }
+
+   return err;
+}
+
+local int Write_GlobalComment(zip64_internal* zi, const char* global_comment) {
+  int err = ZIP_OK;
+  uInt size_global_comment = 0;
+
+  if(global_comment != NULL)
+    size_global_comment = (uInt)strlen(global_comment);
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_global_comment,2);
+
+  if (err == ZIP_OK && size_global_comment > 0)
+  {
+    if (ZWRITE64(zi->z_filefunc,zi->filestream, global_comment, size_global_comment) != size_global_comment)
+      err = ZIP_ERRNO;
+  }
+  return err;
+}
+
+extern int ZEXPORT zipClose(zipFile file, const char* global_comment) {
+    zip64_internal* zi;
+    int err = 0;
+    uLong size_centraldir = 0;
+    ZPOS64_T centraldir_pos_inzip;
+    ZPOS64_T pos;
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 1)
+    {
+        err = zipCloseFileInZip (file);
+    }
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+    if (global_comment==NULL)
+        global_comment = zi->globalcomment;
+#endif
+
+    centraldir_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
+
+    if (err==ZIP_OK)
+    {
+        linkedlist_datablock_internal* ldi = zi->central_dir.first_block;
+        while (ldi!=NULL)
+        {
+            if ((err==ZIP_OK) && (ldi->filled_in_this_block>0))
+            {
+                if (ZWRITE64(zi->z_filefunc,zi->filestream, ldi->data, ldi->filled_in_this_block) != ldi->filled_in_this_block)
+                    err = ZIP_ERRNO;
+            }
+
+            size_centraldir += ldi->filled_in_this_block;
+            ldi = ldi->next_datablock;
+        }
+    }
+    free_linkedlist(&(zi->central_dir));
+
+    pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+    if(pos >= 0xffffffff || zi->number_entry >= 0xFFFF)
+    {
+      ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream);
+      Write_Zip64EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
+
+      Write_Zip64EndOfCentralDirectoryLocator(zi, Zip64EOCDpos);
+    }
+
+    if (err==ZIP_OK)
+      err = Write_EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
+
+    if(err == ZIP_OK)
+      err = Write_GlobalComment(zi, global_comment);
+
+    if (ZCLOSE64(zi->z_filefunc,zi->filestream) != 0)
+        if (err == ZIP_OK)
+            err = ZIP_ERRNO;
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+    free(zi->globalcomment);
+#endif
+    free(zi);
+
+    return err;
+}
+
+extern int ZEXPORT zipRemoveExtraInfoBlock(char* pData, int* dataLen, short sHeader) {
+  char* p = pData;
+  int size = 0;
+  char* pNewHeader;
+  char* pTmp;
+  short header;
+  short dataSize;
+
+  int retVal = ZIP_OK;
+
+  if(pData == NULL || dataLen == NULL || *dataLen < 4)
+    return ZIP_PARAMERROR;
+
+  pNewHeader = (char*)ALLOC((unsigned)*dataLen);
+  pTmp = pNewHeader;
+
+  while(p < (pData + *dataLen))
+  {
+    header = *(short*)p;
+    dataSize = *(((short*)p)+1);
+
+    if( header == sHeader ) // Header found.
+    {
+      p += dataSize + 4; // skip it. do not copy to temp buffer
+    }
+    else
+    {
+      // Extra Info block should not be removed, So copy it to the temp buffer.
+      memcpy(pTmp, p, dataSize + 4);
+      p += dataSize + 4;
+      size += dataSize + 4;
+    }
+
+  }
+
+  if(size < *dataLen)
+  {
+    // clean old extra info block.
+    memset(pData,0, *dataLen);
+
+    // copy the new extra info block over the old
+    if(size > 0)
+      memcpy(pData, pNewHeader, size);
+
+    // set the new extra info size
+    *dataLen = size;
+
+    retVal = ZIP_OK;
+  }
+  else
+    retVal = ZIP_ERRNO;
+
+  free(pNewHeader);
+
+  return retVal;
+}
diff --git a/OrthancFramework/Resources/ThirdParty/minizip/zip.h b/OrthancFramework/Resources/ThirdParty/minizip/zip.h
new file mode 100644
index 0000000..3e230d3
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/minizip/zip.h
@@ -0,0 +1,364 @@
+/* zip.h -- IO on .zip files using zlib
+   Version 1.1, February 14h, 2010
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+         ---------------------------------------------------------------------------
+
+   Condition of use and distribution are the same than zlib :
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+        ---------------------------------------------------------------------------
+
+        Changes
+
+        See header of zip.h
+
+*/
+
+#ifndef _zip12_H
+#define _zip12_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//#define HAVE_BZIP2
+
+#ifndef _ZLIB_H
+#include "zlib.h"
+#endif
+
+#ifndef _ZLIBIOAPI_H
+#include "ioapi.h"
+#endif
+
+#ifdef HAVE_BZIP2
+#include "bzlib.h"
+#endif
+
+#define Z_BZIP2ED 12
+
+#if defined(STRICTZIP) || defined(STRICTZIPUNZIP)
+/* like the STRICT of WIN32, we define a pointer that cannot be converted
+    from (void*) without cast */
+typedef struct TagzipFile__ { int unused; } zipFile__;
+typedef zipFile__ *zipFile;
+#else
+typedef voidp zipFile;
+#endif
+
+#define ZIP_OK                          (0)
+#define ZIP_EOF                         (0)
+#define ZIP_ERRNO                       (Z_ERRNO)
+#define ZIP_PARAMERROR                  (-102)
+#define ZIP_BADZIPFILE                  (-103)
+#define ZIP_INTERNALERROR               (-104)
+
+#ifndef DEF_MEM_LEVEL
+#  if MAX_MEM_LEVEL >= 8
+#    define DEF_MEM_LEVEL 8
+#  else
+#    define DEF_MEM_LEVEL  MAX_MEM_LEVEL
+#  endif
+#endif
+/* default memLevel */
+
+/* tm_zip contain date/time info */
+typedef struct tm_zip_s
+{
+    int tm_sec;             /* seconds after the minute - [0,59] */
+    int tm_min;             /* minutes after the hour - [0,59] */
+    int tm_hour;            /* hours since midnight - [0,23] */
+    int tm_mday;            /* day of the month - [1,31] */
+    int tm_mon;             /* months since January - [0,11] */
+    int tm_year;            /* years - [1980..2044] */
+} tm_zip;
+
+typedef struct
+{
+    tm_zip      tmz_date;       /* date in understandable format           */
+    uLong       dosDate;       /* if dos_date == 0, tmu_date is used      */
+/*    uLong       flag;        */   /* general purpose bit flag        2 bytes */
+
+    uLong       internal_fa;    /* internal file attributes        2 bytes */
+    uLong       external_fa;    /* external file attributes        4 bytes */
+} zip_fileinfo;
+
+typedef const char* zipcharpc;
+
+
+#define APPEND_STATUS_CREATE        (0)
+#define APPEND_STATUS_CREATEAFTER   (1)
+#define APPEND_STATUS_ADDINZIP      (2)
+
+extern zipFile ZEXPORT zipOpen(const char *pathname, int append);
+extern zipFile ZEXPORT zipOpen64(const void *pathname, int append);
+/*
+  Create a zipfile.
+     pathname contain on Windows XP a filename like "c:\\zlib\\zlib113.zip" or on
+       an Unix computer "zlib/zlib113.zip".
+     if the file pathname exist and append==APPEND_STATUS_CREATEAFTER, the zip
+       will be created at the end of the file.
+         (useful if the file contain a self extractor code)
+     if the file pathname exist and append==APPEND_STATUS_ADDINZIP, we will
+       add files in existing zip (be sure you don't add file that doesn't exist)
+     If the zipfile cannot be opened, the return value is NULL.
+     Else, the return value is a zipFile Handle, usable with other function
+       of this zip package.
+*/
+
+/* Note : there is no delete function into a zipfile.
+   If you want delete file into a zipfile, you must open a zipfile, and create another
+   Of course, you can use RAW reading and writing to copy the file you did not want delete
+*/
+
+extern zipFile ZEXPORT zipOpen2(const char *pathname,
+                                int append,
+                                zipcharpc* globalcomment,
+                                zlib_filefunc_def* pzlib_filefunc_def);
+
+extern zipFile ZEXPORT zipOpen2_64(const void *pathname,
+                                   int append,
+                                   zipcharpc* globalcomment,
+                                   zlib_filefunc64_def* pzlib_filefunc_def);
+
+extern zipFile ZEXPORT zipOpen3(const void *pathname,
+                                int append,
+                                zipcharpc* globalcomment,
+                                zlib_filefunc64_32_def* pzlib_filefunc64_32_def);
+
+extern int ZEXPORT zipOpenNewFileInZip(zipFile file,
+                                       const char* filename,
+                                       const zip_fileinfo* zipfi,
+                                       const void* extrafield_local,
+                                       uInt size_extrafield_local,
+                                       const void* extrafield_global,
+                                       uInt size_extrafield_global,
+                                       const char* comment,
+                                       int method,
+                                       int level);
+
+extern int ZEXPORT zipOpenNewFileInZip64(zipFile file,
+                                         const char* filename,
+                                         const zip_fileinfo* zipfi,
+                                         const void* extrafield_local,
+                                         uInt size_extrafield_local,
+                                         const void* extrafield_global,
+                                         uInt size_extrafield_global,
+                                         const char* comment,
+                                         int method,
+                                         int level,
+                                         int zip64);
+
+/*
+  Open a file in the ZIP for writing.
+  filename : the filename in zip (if NULL, '-' without quote will be used
+  *zipfi contain supplemental information
+  if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local
+    contains the extrafield data for the local header
+  if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global
+    contains the extrafield data for the global header
+  if comment != NULL, comment contain the comment string
+  method contain the compression method (0 for store, Z_DEFLATED for deflate)
+  level contain the level of compression (can be Z_DEFAULT_COMPRESSION)
+  zip64 is set to 1 if a zip64 extended information block should be added to the local file header.
+                    this MUST be '1' if the uncompressed size is >= 0xffffffff.
+
+*/
+
+
+extern int ZEXPORT zipOpenNewFileInZip2(zipFile file,
+                                        const char* filename,
+                                        const zip_fileinfo* zipfi,
+                                        const void* extrafield_local,
+                                        uInt size_extrafield_local,
+                                        const void* extrafield_global,
+                                        uInt size_extrafield_global,
+                                        const char* comment,
+                                        int method,
+                                        int level,
+                                        int raw);
+
+
+extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file,
+                                           const char* filename,
+                                           const zip_fileinfo* zipfi,
+                                           const void* extrafield_local,
+                                           uInt size_extrafield_local,
+                                           const void* extrafield_global,
+                                           uInt size_extrafield_global,
+                                           const char* comment,
+                                           int method,
+                                           int level,
+                                           int raw,
+                                           int zip64);
+/*
+  Same than zipOpenNewFileInZip, except if raw=1, we write raw file
+ */
+
+extern int ZEXPORT zipOpenNewFileInZip3(zipFile file,
+                                        const char* filename,
+                                        const zip_fileinfo* zipfi,
+                                        const void* extrafield_local,
+                                        uInt size_extrafield_local,
+                                        const void* extrafield_global,
+                                        uInt size_extrafield_global,
+                                        const char* comment,
+                                        int method,
+                                        int level,
+                                        int raw,
+                                        int windowBits,
+                                        int memLevel,
+                                        int strategy,
+                                        const char* password,
+                                        uLong crcForCrypting);
+
+extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file,
+                                           const char* filename,
+                                           const zip_fileinfo* zipfi,
+                                           const void* extrafield_local,
+                                           uInt size_extrafield_local,
+                                           const void* extrafield_global,
+                                           uInt size_extrafield_global,
+                                           const char* comment,
+                                           int method,
+                                           int level,
+                                           int raw,
+                                           int windowBits,
+                                           int memLevel,
+                                           int strategy,
+                                           const char* password,
+                                           uLong crcForCrypting,
+                                           int zip64);
+
+/*
+  Same than zipOpenNewFileInZip2, except
+    windowBits,memLevel,,strategy : see parameter strategy in deflateInit2
+    password : crypting password (NULL for no crypting)
+    crcForCrypting : crc of file to compress (needed for crypting)
+ */
+
+extern int ZEXPORT zipOpenNewFileInZip4(zipFile file,
+                                        const char* filename,
+                                        const zip_fileinfo* zipfi,
+                                        const void* extrafield_local,
+                                        uInt size_extrafield_local,
+                                        const void* extrafield_global,
+                                        uInt size_extrafield_global,
+                                        const char* comment,
+                                        int method,
+                                        int level,
+                                        int raw,
+                                        int windowBits,
+                                        int memLevel,
+                                        int strategy,
+                                        const char* password,
+                                        uLong crcForCrypting,
+                                        uLong versionMadeBy,
+                                        uLong flagBase);
+
+
+extern int ZEXPORT zipOpenNewFileInZip4_64(zipFile file,
+                                           const char* filename,
+                                           const zip_fileinfo* zipfi,
+                                           const void* extrafield_local,
+                                           uInt size_extrafield_local,
+                                           const void* extrafield_global,
+                                           uInt size_extrafield_global,
+                                           const char* comment,
+                                           int method,
+                                           int level,
+                                           int raw,
+                                           int windowBits,
+                                           int memLevel,
+                                           int strategy,
+                                           const char* password,
+                                           uLong crcForCrypting,
+                                           uLong versionMadeBy,
+                                           uLong flagBase,
+                                           int zip64);
+/*
+  Same than zipOpenNewFileInZip4, except
+    versionMadeBy : value for Version made by field
+    flag : value for flag field (compression level info will be added)
+ */
+
+
+extern int ZEXPORT zipWriteInFileInZip(zipFile file,
+                                       const void* buf,
+                                       unsigned len);
+/*
+  Write data in the zipfile
+*/
+
+extern int ZEXPORT zipCloseFileInZip(zipFile file);
+/*
+  Close the current file in the zipfile
+*/
+
+extern int ZEXPORT zipCloseFileInZipRaw(zipFile file,
+                                        uLong uncompressed_size,
+                                        uLong crc32);
+
+extern int ZEXPORT zipCloseFileInZipRaw64(zipFile file,
+                                          ZPOS64_T uncompressed_size,
+                                          uLong crc32);
+
+/*
+  Close the current file in the zipfile, for file opened with
+    parameter raw=1 in zipOpenNewFileInZip2
+  uncompressed_size and crc32 are value for the uncompressed size
+*/
+
+extern int ZEXPORT zipClose(zipFile file,
+                            const char* global_comment);
+/*
+  Close the zipfile
+*/
+
+
+extern int ZEXPORT zipRemoveExtraInfoBlock(char* pData, int* dataLen, short sHeader);
+/*
+  zipRemoveExtraInfoBlock -  Added by Mathias Svensson
+
+  Remove extra information block from a extra information data for the local file header or central directory header
+
+  It is needed to remove ZIP64 extra information blocks when before data is written if using RAW mode.
+
+  0x0001 is the signature header for the ZIP64 extra information blocks
+
+  usage.
+                        Remove ZIP64 Extra information from a central director extra field data
+              zipRemoveExtraInfoBlock(pCenDirExtraFieldData, &nCenDirExtraFieldDataLen, 0x0001);
+
+                        Remove ZIP64 Extra information from a Local File Header extra field data
+        zipRemoveExtraInfoBlock(pLocalHeaderExtraFieldData, &nLocalHeaderExtraFieldDataLen, 0x0001);
+*/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _zip64_H */
diff --git a/OrthancFramework/Resources/ThirdParty/patch/NOTES.txt b/OrthancFramework/Resources/ThirdParty/patch/NOTES.txt
new file mode 100644
index 0000000..7b6e180
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/patch/NOTES.txt
@@ -0,0 +1,70 @@
+===========
+INFORMATION
+===========
+
+This is a precompiled version of the "patch" standard tool for
+Windows. It was compiled using the MSYS framework.
+
+The binaries originate from the "Git for Windows 1.9.5" package
+(https://msysgit.github.io/). The build instructions have been
+provided on the discussion group of Git for Windows [1]. They are
+copied/pasted below for reference.
+
+
+
+================
+UPSTREAM PROJECT
+================
+
+URL to the upstream project:
+http://savannah.gnu.org/projects/patch/
+
+License of patch: GPLv2 (GNU General Public License v2)
+
+Copyright (C) 1988 Larry Wall "with lots o' patches by Paul Eggert"
+Copyright (C) 1997 Free Software Foundation, Inc.
+
+
+
+======================
+BUILD INSTRUCTIONS [1]
+======================
+
+The easiest way to find out about this is to install the Git SDK, then 
+run 
+
+     pacman -Qu $(which patch.exe) 
+
+to find out which package contains the `patch.exe` binary. It so happens 
+to be patch.2.7.5-1 at the moment. Since this is an MSys2 package (not a 
+MinGW one, otherwise the patch utility would be in /mingw64/bin/, not 
+/usr/bin/), this package is built from the recipes in 
+
+     https://github.com/msys2/MSYS2-packages 
+
+The `patch` package is obviously built from the subdirectory 
+
+     https://github.com/Alexpux/MSYS2-packages/tree/master/patch 
+
+and the PKGBUILD file specifies that the source is fetched from 
+ftp://ftp.gnu.org/gnu/patch/patch-2.7.5.tar.xz: 
+
+https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/PKGBUILD#L14 
+
+and then these two patches are applied before building: 
+     
+https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/msys2-patch-2.7.1.patch 
+
+and 
+      
+https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/msys2-patch-manifest.patch 
+
+As you can see, some light changes are applied, i.e. `patch.exe` will 
+always write in binary mode with MSys2, and the executable will have a 
+manifest embedded that allows it to run as non-administrator. 
+
+Ciao, 
+Johannes Schindelin
+
+
+[1] https://groups.google.com/d/msg/git-for-windows/xWyVr4z6Ri0/6RKeV028EAAJ
diff --git a/OrthancFramework/Resources/ThirdParty/patch/patch.exe.manifest b/OrthancFramework/Resources/ThirdParty/patch/patch.exe.manifest
new file mode 100644
index 0000000..33b727b
--- /dev/null
+++ b/OrthancFramework/Resources/ThirdParty/patch/patch.exe.manifest
@@ -0,0 +1,19 @@
+
+
+  
+
+  
+  
+    
+      
+        
+        
+       
+  
+
+
diff --git a/OrthancFramework/Resources/Toolchains/CrossToolchain.cmake b/OrthancFramework/Resources/Toolchains/CrossToolchain.cmake
new file mode 100644
index 0000000..7491b67
--- /dev/null
+++ b/OrthancFramework/Resources/Toolchains/CrossToolchain.cmake
@@ -0,0 +1,78 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+#
+#  $ CROSSTOOL_NG_ARCH=mips CROSSTOOL_NG_BOARD=malta CROSSTOOL_NG_IMAGE=/tmp/mips cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/CrossToolchain.cmake -DBUILD_CONNECTIVITY_CHECKS=OFF -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON -DUSE_SYSTEM_JSONCPP=OFF -DUSE_SYSTEM_UUID=OFF -DENABLE_DCMTK_JPEG_LOSSLESS=OFF -G Ninja && ninja
+#
+
+INCLUDE(CMakeForceCompiler)
+
+SET(CROSSTOOL_NG_ROOT $ENV{CROSSTOOL_NG_ROOT} CACHE STRING "")
+SET(CROSSTOOL_NG_ARCH $ENV{CROSSTOOL_NG_ARCH} CACHE STRING "")
+SET(CROSSTOOL_NG_BOARD $ENV{CROSSTOOL_NG_BOARD} CACHE STRING "")
+SET(CROSSTOOL_NG_SUFFIX $ENV{CROSSTOOL_NG_SUFFIX} CACHE STRING "")
+SET(CROSSTOOL_NG_IMAGE $ENV{CROSSTOOL_NG_IMAGE} CACHE STRING "")
+
+IF ("${CROSSTOOL_NG_ROOT}" STREQUAL "")
+  SET(CROSSTOOL_NG_ROOT "/home/$ENV{USER}/x-tools")
+ENDIF()
+
+IF ("${CROSSTOOL_NG_SUFFIX}" STREQUAL "")
+  SET(CROSSTOOL_NG_SUFFIX "linux-gnu")
+ENDIF()
+
+SET(CROSSTOOL_NG_NAME ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_BOARD}-${CROSSTOOL_NG_SUFFIX})
+SET(CROSSTOOL_NG_BASE ${CROSSTOOL_NG_ROOT}/${CROSSTOOL_NG_NAME})
+
+# the name of the target operating system
+SET(CMAKE_SYSTEM_NAME Linux)
+SET(CMAKE_SYSTEM_VERSION CrossToolNg)
+SET(CMAKE_SYSTEM_PROCESSOR ${CROSSTOOL_NG_ARCH})
+
+# which compilers to use for C and C++
+SET(CMAKE_C_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-gcc)
+
+if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
+  CMAKE_FORCE_CXX_COMPILER(${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++ GNU)
+else()
+  SET(CMAKE_CXX_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++)
+endif()
+
+# here is the target environment located
+SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_IMAGE})
+#SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+
+SET(CMAKE_CROSSCOMPILING ON)
+#SET(CROSS_COMPILER_PREFIX ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX})
+
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_SHARED_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
diff --git a/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake b/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake
new file mode 100644
index 0000000..4ace2e3
--- /dev/null
+++ b/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake
@@ -0,0 +1,101 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+#
+# Full build, as used on the BuildBot CIS:
+#
+#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DUSE_LEGACY_BOOST=ON -DBOOST_LOCALE_BACKEND=icu -DENABLE_PKCS11=ON -G Ninja
+#
+# Or, more lightweight version (without libp11 and ICU):
+#
+#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G Ninja
+#
+
+INCLUDE(CMakeForceCompiler)
+
+SET(LSB_PATH $ENV{LSB_PATH} CACHE STRING "")
+SET(LSB_CC $ENV{LSB_CC} CACHE STRING "")
+SET(LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
+SET(LSB_TARGET_VERSION "4.0" CACHE STRING "")
+
+IF ("${LSB_PATH}" STREQUAL "")
+  SET(LSB_PATH "/opt/lsb")
+ENDIF()
+
+IF (EXISTS ${LSB_PATH}/lib64)
+  SET(LSB_TARGET_PROCESSOR "x86_64")
+  SET(LSB_LIBPATH ${LSB_PATH}/lib64-${LSB_TARGET_VERSION})
+ELSEIF (EXISTS ${LSB_PATH}/lib)
+  SET(LSB_TARGET_PROCESSOR "x86")
+  SET(LSB_LIBPATH ${LSB_PATH}/lib-${LSB_TARGET_VERSION})
+ELSE()
+  MESSAGE(FATAL_ERROR "Unable to detect the target processor architecture. Check the LSB_PATH environment variable.")
+ENDIF()
+
+SET(LSB_CPPPATH ${LSB_PATH}/include)
+SET(PKG_CONFIG_PATH ${LSB_LIBPATH}/pkgconfig/)
+
+# the name of the target operating system
+SET(CMAKE_SYSTEM_NAME Linux)
+SET(CMAKE_SYSTEM_VERSION LinuxStandardBase)
+SET(CMAKE_SYSTEM_PROCESSOR ${LSB_TARGET_PROCESSOR})
+
+# which compilers to use for C and C++
+SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc)
+
+if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
+  CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU)
+else()
+  SET(CMAKE_CXX_COMPILER ${LSB_PATH}/bin/lsbc++)
+endif()
+
+# here is the target environment located
+SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH})
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
+SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
+
+SET(CMAKE_CROSSCOMPILING OFF)
+
+
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include" CACHE INTERNAL "" FORCE)
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward" CACHE INTERNAL "" FORCE)
+SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
+SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
+
+if (NOT "${LSB_CXX}" STREQUAL "")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cxx=${LSB_CXX}")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
+endif()
+
+if (NOT "${LSB_CC}" STREQUAL "")
+  SET(CMAKE_C_FLAGS "${CMAKE_CC_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
+endif()
+
diff --git a/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake
new file mode 100644
index 0000000..765578b
--- /dev/null
+++ b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake
@@ -0,0 +1,39 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
+set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
+set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
diff --git a/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake
new file mode 100644
index 0000000..3fc8fb9
--- /dev/null
+++ b/OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake
@@ -0,0 +1,39 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
+set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
+set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
diff --git a/OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake b/OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake
new file mode 100644
index 0000000..4eb297b
--- /dev/null
+++ b/OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake
@@ -0,0 +1,42 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
+set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
+set(CMAKE_RC_COMPILER i586-mingw32msvc-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
diff --git a/OrthancFramework/Resources/WebAssembly.txt b/OrthancFramework/Resources/WebAssembly.txt
new file mode 100644
index 0000000..0767339
--- /dev/null
+++ b/OrthancFramework/Resources/WebAssembly.txt
@@ -0,0 +1,62 @@
+
+======================================
+== Notes about WebAssembly ===========
+======================================
+
+
+
+Trap mode
+=========
+
+The BINARYEN_TRAP_MODE specifies what to do when divisions per zero
+(and similar conditions like integer overflows) are encountered. This
+can be set through the option "EMSCRIPTEN_TRAP_MODE" in Orthanc.
+
+
+
+Previous versions (<= 1.7.1)
+----------------------------
+
+The "clamp" mode avoids throwing errors, as they cannot be properly
+catched by "try {} catch (...)" constructions. HOWEVER, the "upstream"
+backend of Emscripten (which is now the default) doesn't support this
+option.
+
+In Orthanc <= 1.7.1, the "clamp" mode was used by default. As a
+consequence, there was an old flag "EMSCRIPTEN_SET_LLVM_WASM_BACKEND"
+to setting BINARYEN_TRAP_MODE.
+
+Here is the definition of the parameter that was in
+"OrthancFrameworkParameters.cmake":
+
+>>>>>
+set(EMSCRIPTEN_SET_LLVM_WASM_BACKEND OFF CACHE BOOL "Sets the compiler flags required to use the LLVM Web Assembly backend in emscripten")
+<<<<<
+
+Setting this option to "ON" fixes error: "shared:ERROR:
+BINARYEN_TRAP_MODE is not supported by the LLVM wasm backend" if using
+the "upstream" backend of Emscripten. Here is the corresponding
+implementation that was in "Compiler.cmake":
+
+>>>>>
+if (NOT EMSCRIPTEN_SET_LLVM_WASM_BACKEND)
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s BINARYEN_TRAP_MODE='\"clamp\"'")
+endif()
+<<<<<
+
+
+The "EMSCRIPTEN_SET_LLVM_WASM_BACKEND" option was totally removed in
+Orthanc 1.8.1.
+
+
+
+
+Linker flags
+============
+
+Since Orthanc 1.7.2, "Compiler.cmake" doesn't set any linking option
+for Emscripten. In Orthanc <= 1.7.1, the following was done:
+
+>>>>>
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+<<<<<
diff --git a/OrthancFramework/Resources/WindowsResources.py b/OrthancFramework/Resources/WindowsResources.py
new file mode 100755
index 0000000..0522900
--- /dev/null
+++ b/OrthancFramework/Resources/WindowsResources.py
@@ -0,0 +1,81 @@
+#!/usr/bin/python
+
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+import os
+import sys
+import datetime
+
+if len(sys.argv) != 5:
+    sys.stderr.write('Usage: %s    \n\n' % sys.argv[0])
+    sys.stderr.write('Example: %s 0.9.1 Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging"\n' % sys.argv[0])
+    sys.exit(-1)
+
+SOURCE = os.path.join(os.path.dirname(__file__), 'WindowsResources.rc')
+
+VERSION = sys.argv[1]
+PRODUCT = sys.argv[2]
+FILENAME = sys.argv[3]
+DESCRIPTION = sys.argv[4]
+
+if VERSION == 'mainline':
+    VERSION = '999.999.999'
+    RELEASE = 'This is a mainline build, not an official release'
+else:
+    RELEASE = 'Release %s' % VERSION
+
+v = VERSION.split('.')
+if len(v) != 2 and len(v) != 3:
+    sys.stderr.write('Bad version number: %s\n' % VERSION)
+    sys.exit(-1)
+
+if len(v) == 2:
+    v.append('0')
+
+extension = os.path.splitext(FILENAME)[1]
+if extension.lower() == '.dll':
+    BLOCK = '040904E4'
+    TYPE = 'VFT_DLL'
+elif extension.lower() == '.exe':
+    #BLOCK = '040904B0'   # LANG_ENGLISH/SUBLANG_ENGLISH_US,
+    BLOCK = '040904E4'   # Lang=US English, CharSet=Windows Multilingual
+    TYPE = 'VFT_APP'
+else:
+    sys.stderr.write('Unsupported extension (.EXE or .DLL only): %s\n' % extension)
+    sys.exit(-1)
+
+
+with open(SOURCE, 'r') as source:
+    content = source.read()
+    content = content.replace('${VERSION_MAJOR}', v[0])
+    content = content.replace('${VERSION_MINOR}', v[1])
+    content = content.replace('${VERSION_PATCH}', v[2])
+    content = content.replace('${RELEASE}', RELEASE)
+    content = content.replace('${DESCRIPTION}', DESCRIPTION)
+    content = content.replace('${PRODUCT}', PRODUCT)   
+    content = content.replace('${FILENAME}', FILENAME)   
+    content = content.replace('${YEAR}', str(datetime.datetime.now().year))
+    content = content.replace('${BLOCK}', BLOCK)
+    content = content.replace('${TYPE}', TYPE)
+
+    sys.stdout.write(content)
diff --git a/OrthancFramework/Resources/WindowsResources.rc b/OrthancFramework/Resources/WindowsResources.rc
new file mode 100644
index 0000000..d364328
--- /dev/null
+++ b/OrthancFramework/Resources/WindowsResources.rc
@@ -0,0 +1,30 @@
+#include 
+
+VS_VERSION_INFO VERSIONINFO
+   FILEVERSION ${VERSION_MAJOR},${VERSION_MINOR},0,${VERSION_PATCH}
+   PRODUCTVERSION ${VERSION_MAJOR},${VERSION_MINOR},0,0
+   FILEOS VOS_NT_WINDOWS32
+   FILETYPE ${TYPE}
+   BEGIN
+      BLOCK "StringFileInfo"
+      BEGIN
+         BLOCK "${BLOCK}"
+         BEGIN
+            VALUE "Comments", "${RELEASE}"
+            VALUE "CompanyName", "The Orthanc project"
+            VALUE "FileDescription", "${DESCRIPTION}"
+            VALUE "FileVersion", "${VERSION_MAJOR}.${VERSION_MINOR}.0.${VERSION_PATCH}"
+            VALUE "InternalName", "${PRODUCT}"
+            VALUE "LegalCopyright", "(c) 2012-${YEAR}, Sebastien Jodogne, University Hospital of Liege, Osimis S.A., Orthanc Team SRL, and ICTEAM UCLouvain"
+            VALUE "LegalTrademarks", "Licensing information is available at https://orthanc.uclouvain.be/book/faq/licensing.html"
+            VALUE "OriginalFilename", "${FILENAME}"
+            VALUE "ProductName", "${PRODUCT}"
+            VALUE "ProductVersion", "${VERSION_MAJOR}.${VERSION_MINOR}"
+         END
+      END
+
+      BLOCK "VarFileInfo"
+      BEGIN
+        VALUE "Translation", 0x409, 1252  // U.S. English
+      END
+   END
diff --git a/OrthancFramework/SharedLibrary/CMakeLists.txt b/OrthancFramework/SharedLibrary/CMakeLists.txt
new file mode 100644
index 0000000..091b80d
--- /dev/null
+++ b/OrthancFramework/SharedLibrary/CMakeLists.txt
@@ -0,0 +1,572 @@
+# 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 Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# .
+
+
+
+## To see all the exported symbols in the DLL:
+##
+##  $ i686-w64-mingw32-objdump -p ./libOrthancFramework.dll
+##
+## IMPORTANT: "-static-libgcc" prevents catching exception in the
+## .EXE, which makes throwing exceptions crash the software!
+##
+
+
+cmake_minimum_required(VERSION 2.8...4.0)
+cmake_policy(SET CMP0058 NEW)
+
+project(OrthancFramework)
+
+
+
+#####################################################################
+## Additional parameters
+#####################################################################
+
+# *Do not* use CMAKE_INSTALL_PREFIX, otherwise CMake automatically
+# adds CMAKE_INSTALL_PREFIX to the include_directories(), which causes
+# issues if re-building the shared library after install!
+set(ORTHANC_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" CACHE PATH "")
+SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
+set(BUILD_SHARED_LIBRARY ON CACHE BOOL "Whether to build a shared library instead of a static library")
+set(ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES "" CACHE STRING "Additional libraries to link against, separated by whitespaces, typically needed if building the static library (a common minimal value is \"boost_filesystem boost_iostreams boost_locale boost_regex boost_thread jsoncpp pugixml uuid\")")
+
+
+
+#####################################################################
+## Configuration of the Orthanc framework
+#####################################################################
+
+# This must be before inclusion of "OrthancFrameworkParameters.cmake" to take effect
+if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND
+    CMAKE_COMPILER_IS_GNUCXX) # MinGW
+  set(DYNAMIC_MINGW_STDLIB ON)   # Disable static linking against libc (to throw exceptions)
+  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libstdc++")
+endif()
+
+include(${CMAKE_SOURCE_DIR}/../Resources/CMake/OrthancFrameworkParameters.cmake)
+
+# "ORTHANC_VERSION" is initialized by "OrthancFrameworkParameters.cmake"
+set(ORTHANC_FRAMEWORK_SOVERSION "${ORTHANC_VERSION}" CACHE STRING "On GNU/Linux, the SOVERSION to be used for the shared library")
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK)
+  set(STANDALONE_BUILD ON)
+else()
+  set(STANDALONE_BUILD OFF)
+endif()
+
+set(ENABLE_DCMTK ON)
+set(ENABLE_DCMTK_TRANSCODING ON)
+set(ENABLE_GOOGLE_TEST ON)
+set(ENABLE_JPEG ON)
+set(ENABLE_LOCALE ON)
+set(ENABLE_LUA ON)
+set(ENABLE_PNG ON)
+set(ENABLE_PUGIXML ON)
+set(ENABLE_ZLIB ON)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  # WebAssembly or asm.js
+  set(BOOST_LOCALE_BACKEND "libiconv")
+  set(ORTHANC_SANDBOXED ON)
+  set(STANDALONE_BUILD ON)
+
+  # Will be used by "../Resources/CMake/EmscriptenParameters.cmake"
+  set(WASM_FLAGS "-s SIDE_MODULE=1 -s EXPORT_ALL=1")
+
+else()
+  # Enable all the remaining modules for other targets
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_DCMTK_NETWORKING ON)
+  set(ENABLE_OPENSSL_ENGINES ON)
+  set(ENABLE_SQLITE ON)
+  set(ENABLE_WEB_CLIENT ON)
+  set(ENABLE_WEB_SERVER ON)
+
+  set(BOOST_LOCALE_BACKEND "icu")
+  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+    set(USE_LEGACY_JSONCPP ON)
+    set(USE_LEGACY_LIBICU ON)
+  endif()
+endif()
+
+
+set(ORTHANC_BUILDING_FRAMEWORK_LIBRARY ON)
+
+include(${CMAKE_SOURCE_DIR}/../Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+
+
+#####################################################################
+## Configuration the visibility of the third-party libraries in the
+## shared library
+#####################################################################
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_JSONCPP)
+  set(ORTHANC_STATIC_JSONCPP ON)
+else()
+  set(ORTHANC_STATIC_JSONCPP OFF)
+endif()
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_BOOST)
+  set(ORTHANC_STATIC_BOOST ON)
+else()
+  set(ORTHANC_STATIC_BOOST OFF)
+endif()
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_SQLITE)
+  set(ORTHANC_STATIC_SQLITE ON)
+else()
+  set(ORTHANC_STATIC_SQLITE OFF)
+endif()
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
+  set(ORTHANC_STATIC_PUGIXML ON)
+else()
+  set(ORTHANC_STATIC_PUGIXML OFF)
+endif()
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK)
+  set(ORTHANC_STATIC_DCMTK ON)
+else()
+  set(ORTHANC_STATIC_DCMTK OFF)
+endif()
+
+
+if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
+    CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+
+  # Control the visibility of JsonCpp
+  if (ORTHANC_STATIC_JSONCPP)
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+      set_source_files_properties(${JSONCPP_SOURCES}
+        PROPERTIES COMPILE_DEFINITIONS "JSON_API=__declspec(dllexport)"
+        )
+      set(ORTHANC_JSON_API "__declspec(dllimport)")
+    else()
+      set(ORTHANC_JSON_API "__attribute__((visibility(\"default\")))")
+      set_source_files_properties(${JSONCPP_SOURCES}
+        PROPERTIES COMPILE_DEFINITIONS "JSON_API=${ORTHANC_JSON_API}"
+        )
+    endif()
+  endif()
+
+  # Control the visibility of SQLite
+  if (ORTHANC_STATIC_SQLITE)
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+      set_source_files_properties(${SQLITE_SOURCES}
+        PROPERTIES COMPILE_DEFINITIONS "SQLITE_API=__declspec(dllexport)"
+        )
+      set(ORTHANC_SQLITE_API "__declspec(dllimport)")
+    else()
+      set(ORTHANC_SQLITE_API "__attribute__((visibility(\"default\")))")
+      set_source_files_properties(${SQLITE_SOURCES}
+        PROPERTIES COMPILE_DEFINITIONS "SQLITE_API=${ORTHANC_SQLITE_API}"
+        )
+    endif()
+  endif()
+
+  # Control the visibility of Boost
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND
+      ORTHANC_STATIC_BOOST)
+    set_source_files_properties(${ORTHANC_CORE_SOURCES_INTERNAL}
+      PROPERTIES COMPILE_DEFINITIONS "BOOST_DATE_TIME_SOURCE;BOOST_FILESYSTEM_SOURCE;BOOST_LOCALE_SOURCE;BOOST_REGEX_SOURCE"
+      )
+  endif()
+
+  # Control the visibility of Pugixml
+  if (ORTHANC_STATIC_PUGIXML)
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+      set_source_files_properties(${PUGIXML_SOURCES}
+        PROPERTIES COMPILE_DEFINITIONS "PUGIXML_API=__declspec(dllexport)"
+        )
+      set(ORTHANC_PUGIXML_API "__declspec(dllimport)")
+    else()
+      set(ORTHANC_PUGIXML_API "__attribute__((visibility(\"default\")))")
+      set_source_files_properties(${PUGIXML_SOURCES}
+        PROPERTIES COMPILE_DEFINITIONS "PUGIXML_API=${ORTHANC_PUGIXML_API}"
+        )
+    endif()
+  endif()
+
+  # Control the visibility of DCMTK: We only export the "dcmdata" module
+  if (ORTHANC_STATIC_DCMTK)
+    set_source_files_properties(${DCMTK_SOURCES}
+      PROPERTIES COMPILE_DEFINITIONS "DCMTK_BUILD_IN_PROGRESS;DCMTK_BUILD_SINGLE_SHARED_LIBRARY;DCMTK_SHARED;HAVE_HIDDEN_VISIBILITY;dcmdata_EXPORTS"
+      )
+  endif()
+endif()
+
+
+add_definitions(
+  -DCIVETWEB_API=    # Don't export the public symbols from CivetWeb
+  )
+
+
+
+#####################################################################
+## Building the shared library
+#####################################################################
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../Resources/WindowsResources.py
+    ${ORTHANC_VERSION} "OrthancFramework" OrthancFramework.dll "Shared library containing the Orthanc framework"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/Version.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  list(APPEND AUTOGENERATED_SOURCES  ${AUTOGENERATED_DIR}/Version.rc)
+endif()
+
+
+# Those two files collide with each other, and thus are merged into a
+# single "DllMain.cpp"
+list(REMOVE_ITEM ORTHANC_CORE_SOURCES
+  ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp
+  ${OPENSSL_SOURCES_DIR}/crypto/dllmain.c
+  )
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  # In WebAssembly, a SIDE_MODULE is an executable
+  add_executable(OrthancFramework
+    ${AUTOGENERATED_SOURCES}
+    ${ORTHANC_CORE_SOURCES}
+    ${ORTHANC_DICOM_SOURCES}
+    )
+
+  DefineSourceBasenameForTarget(OrthancFramework)
+
+  # CMake does not natively handle SIDE_MODULE, and believes that
+  # Emscripten produces a ".js" file (whereas it creates only the
+  # ".wasm"). Create a dummy ".js" for target to work.
+  add_custom_command(
+    TARGET OrthancFramework POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/OrthancFramework.js
+    )
+else()
+  if (BUILD_SHARED_LIBRARY)
+    add_library(OrthancFramework SHARED
+      ${AUTOGENERATED_SOURCES}
+      ${ORTHANC_CORE_SOURCES}
+      ${ORTHANC_DICOM_SOURCES}
+      DllMain.cpp
+      )
+
+    DefineSourceBasenameForTarget(OrthancFramework)
+
+    # By default, hide all the symbols
+    set_target_properties(OrthancFramework PROPERTIES C_VISIBILITY_PRESET hidden)
+    set_target_properties(OrthancFramework PROPERTIES CXX_VISIBILITY_PRESET hidden)
+
+    # Configure the version of the shared library
+    set_target_properties(
+      OrthancFramework PROPERTIES 
+      VERSION ${ORTHANC_VERSION} 
+      SOVERSION ${ORTHANC_FRAMEWORK_SOVERSION}
+      )    
+
+    target_link_libraries(OrthancFramework ${DCMTK_LIBRARIES})
+
+    if (LIBICU_LIBRARIES)
+      target_link_libraries(OrthancFramework ${LIBICU_LIBRARIES})
+    endif()
+
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+      target_link_libraries(OrthancFramework winpthread)
+    endif()
+  else()
+    # Building a static library
+    add_library(OrthancFramework STATIC
+      ${AUTOGENERATED_SOURCES}
+      ${ORTHANC_CORE_SOURCES}
+      ${ORTHANC_DICOM_SOURCES}
+      )
+
+    DefineSourceBasenameForTarget(OrthancFramework)
+
+    # Add the "-fPIC" option to use the static library from Orthanc
+    # plugins (the latter being shared libraries)
+    set_property(TARGET OrthancFramework PROPERTY POSITION_INDEPENDENT_CODE ON)
+  endif()
+
+  DefineSourceBasenameForTarget(OrthancFramework)
+endif()
+
+
+
+#####################################################################
+## Publish the headers into the "Include" folder of the build
+## directory
+#####################################################################
+
+file(
+  COPY ${CMAKE_SOURCE_DIR}/../Sources/
+  DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework
+  NO_SOURCE_PERMISSIONS
+  FILES_MATCHING
+  PATTERN "*.h"
+  PATTERN OrthancFramework.h EXCLUDE
+  )
+
+configure_file(
+  ${CMAKE_SOURCE_DIR}/OrthancFramework.h.in
+  ${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework/OrthancFramework.h
+  )
+
+
+if (ORTHANC_STATIC_BOOST)
+  file(
+    COPY ${BOOST_SOURCES_DIR}/boost/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/boost/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    PATTERN "*.hpp"
+    PATTERN "*.ipp"
+    )
+endif()
+
+
+if (ENABLE_SQLITE AND ORTHANC_STATIC_SQLITE)
+  file(
+    COPY ${SQLITE_SOURCES_DIR}/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+endif()
+
+
+if (ORTHANC_STATIC_JSONCPP)
+  file(
+    COPY ${JSONCPP_SOURCES_DIR}/include/json/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/json/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+endif()
+
+
+if (ENABLE_DCMTK AND (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK))
+  file(
+    COPY ${DCMTK_SOURCES_DIR}/dcmdata/include/dcmtk/dcmdata/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk/dcmdata/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+
+  file(
+    COPY ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk/config/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+
+  file(
+    COPY ${DCMTK_SOURCES_DIR}/ofstd/include/dcmtk/ofstd/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk/ofstd/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+
+  file(
+    COPY ${DCMTK_SOURCES_DIR}/oflog/include/dcmtk/oflog/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/dcmtk/oflog/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+endif()
+
+
+if (ENABLE_PUGIXML AND (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML))
+  file(
+    COPY ${PUGIXML_SOURCES_DIR}/src/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.hpp"
+    )
+endif()
+
+
+if (ENABLE_LUA AND (STATIC_BUILD OR NOT USE_SYSTEM_LUA))
+  file(
+    COPY ${LUA_SOURCES_DIR}/src/
+    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/
+    NO_SOURCE_PERMISSIONS
+    FILES_MATCHING
+    PATTERN "*.h"
+    )
+endif()
+
+
+if (OFF)
+  # These files are fully abstracted by the Orthanc Framework classes
+  if (ENABLE_PNG AND (STATIC_BUILD OR NOT USE_SYSTEM_LIBPNG))
+    file(
+      COPY ${LIBPNG_SOURCES_DIR}/
+      DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/
+      NO_SOURCE_PERMISSIONS
+      FILES_MATCHING
+      PATTERN "*.h"
+      )
+  endif()
+
+  if (ENABLE_ZLIB AND (STATIC_BUILD OR NOT USE_SYSTEM_ZLIB))
+    file(
+      COPY ${ZLIB_SOURCES_DIR}/
+      DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/
+      NO_SOURCE_PERMISSIONS
+      FILES_MATCHING
+      PATTERN "*.h"
+      )
+  endif()
+
+  if (ENABLE_JPEG AND (STATIC_BUILD OR NOT USE_SYSTEM_LIBJPEG))
+    file(
+      COPY ${LIBJPEG_SOURCES_DIR}/
+      DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/
+      NO_SOURCE_PERMISSIONS
+      FILES_MATCHING
+      PATTERN "*.h"
+      )
+  endif()
+
+  if (ENABLE_WEB_CLIENT AND (STATIC_BUILD OR NOT USE_SYSTEM_CURL))
+    file(
+      COPY ${CURL_SOURCES_DIR}/include/curl/
+      DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/Include/curl/
+      NO_SOURCE_PERMISSIONS
+      FILES_MATCHING
+      PATTERN "*.h"
+      )
+  endif()
+endif()
+
+
+
+#####################################################################
+## Possibly install the headers and the binaries
+#####################################################################
+
+install(
+  TARGETS OrthancFramework
+  RUNTIME DESTINATION ${ORTHANC_INSTALL_PREFIX}/lib    # Destination for Windows
+  LIBRARY DESTINATION ${ORTHANC_INSTALL_PREFIX}/lib    # Destination for Linux
+  ARCHIVE DESTINATION ${ORTHANC_INSTALL_PREFIX}/lib    # Destination for static library
+  )
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  install(FILES
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancFramework.wasm
+    DESTINATION ${ORTHANC_INSTALL_PREFIX}/lib
+    )
+endif()
+
+install(
+  DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Include/
+  DESTINATION ${ORTHANC_INSTALL_PREFIX}/include/
+  )
+
+
+
+#####################################################################
+## Compile the unit tests
+#####################################################################
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  include(ExternalProject)
+
+  if (CMAKE_TOOLCHAIN_FILE)
+    # Take absolute path to the toolchain
+    get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR}/..)
+    list(APPEND Flags
+      -DCMAKE_TOOLCHAIN_FILE=${TMP}
+      -DLSB_CC=${LSB_CC}
+      -DLSB_CXX=${LSB_CXX}
+      )
+  endif()
+
+  if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK)
+    list(APPEND Flags
+      # This is necessary to compile "dcmtk/dcmdata/dctagkey.h" since
+      # DCMTK 3.6.7 because it includes the file provided in macro
+      # "DCMTK_DIAGNOSTIC_IGNORE_ATTRIBUTE_REDECLARATION"
+      -DCMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES=${DCMTK_SOURCES_DIR}/ofstd/include
+      )
+  endif()
+
+  # Build the unit tests, linking them against the just-created
+  # "OrthancFramework" library
+  externalproject_add(UnitTests
+    SOURCE_DIR "${CMAKE_SOURCE_DIR}/../UnitTestsSources"
+    CMAKE_ARGS
+    ${Flags}
+    -DALLOW_DOWNLOADS:BOOL=${ALLOW_DOWNLOADS}
+    -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+    -DORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES:STRING=${ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES}
+    -DORTHANC_FRAMEWORK_LIBDIR:PATH=${CMAKE_CURRENT_BINARY_DIR}
+    -DORTHANC_FRAMEWORK_ROOT:PATH=${CMAKE_CURRENT_BINARY_DIR}/Include/orthanc-framework
+    -DORTHANC_FRAMEWORK_SOURCE:STRING=system
+    -DORTHANC_FRAMEWORK_STATIC:BOOL=${STATIC_BUILD}
+    -DORTHANC_FRAMEWORK_USE_SHARED:BOOL=${BUILD_SHARED_LIBRARY}
+    -DSTATIC_BUILD:BOOL=${STATIC_BUILD}
+    -DUNIT_TESTS_WITH_HTTP_CONNEXIONS:BOOL=${UNIT_TESTS_WITH_HTTP_CONNEXIONS}
+    -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE:BOOL=${USE_GOOGLE_TEST_DEBIAN_PACKAGE}
+    -DUSE_SYSTEM_GOOGLE_TEST:BOOL=${USE_SYSTEM_GOOGLE_TEST}
+
+    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
+    -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
+    -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
+    -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
+    -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}
+    -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}
+    )
+
+  add_dependencies(UnitTests OrthancFramework)
+endif()
+
+
+
+#####################################################################
+## Prepare the "uninstall" target
+## http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F
+#####################################################################
+
+configure_file(
+  "${CMAKE_SOURCE_DIR}/../Resources/CMake/Uninstall.cmake.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
+  IMMEDIATE @ONLY)
+
+add_custom_target(uninstall
+  COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
diff --git a/OrthancFramework/SharedLibrary/DllMain.cpp b/OrthancFramework/SharedLibrary/DllMain.cpp
new file mode 100644
index 0000000..f1f810e
--- /dev/null
+++ b/OrthancFramework/SharedLibrary/DllMain.cpp
@@ -0,0 +1,105 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+/**
+
+   This file merges 2 files:
+   ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp
+   ${OPENSSL_SOURCES_DIR}/crypto/dllmain.c
+
+**/
+
+#if defined(_WIN32) || defined(__CYGWIN__)
+
+#include 
+
+#include 
+
+#include 
+
+#if defined(__BORLANDC__)
+extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE /*hInstance*/, DWORD dwReason, LPVOID /*lpReserved*/)
+#elif defined(_WIN32_WCE)
+  extern "C" BOOL WINAPI DllMain(HANDLE /*hInstance*/, DWORD dwReason, LPVOID /*lpReserved*/)
+#else
+  extern "C" BOOL WINAPI DllMain(HINSTANCE /*hInstance*/, DWORD dwReason, LPVOID /*lpReserved*/)
+#endif
+{
+  switch(dwReason)
+  {
+    case DLL_PROCESS_ATTACH:
+    {
+      //OPENSSL_cpuid_setup();  // TODO - Is this necessary?
+      boost::on_process_enter();
+      boost::on_thread_enter();
+      break;
+    }
+
+    case DLL_THREAD_ATTACH:
+    {
+      boost::on_thread_enter();
+      break;
+    }
+
+    case DLL_THREAD_DETACH:
+    {
+      OPENSSL_thread_stop();
+      boost::on_thread_exit();
+      break;
+    }
+
+    case DLL_PROCESS_DETACH:
+    {
+      boost::on_thread_exit();
+      boost::on_process_exit();
+      break;
+    }
+  }
+
+  return TRUE;
+}
+
+#endif
+
+
+namespace boost
+{
+  void tss_cleanup_implemented()
+  {
+    /*
+      This function's sole purpose is to cause a link error in cases where
+      automatic tss cleanup is not implemented by Boost.Threads as a
+      reminder that user code is responsible for calling the necessary
+      functions at the appropriate times (and for implementing an a
+      tss_cleanup_implemented() function to eliminate the linker's
+      missing symbol error).
+
+      If Boost.Threads later implements automatic tss cleanup in cases
+      where it currently doesn't (which is the plan), the duplicate
+      symbol error will warn the user that their custom solution is no
+      longer needed and can be removed.
+    */
+  }
+}
+
diff --git a/OrthancFramework/SharedLibrary/NOTES.txt b/OrthancFramework/SharedLibrary/NOTES.txt
new file mode 100644
index 0000000..fc1b0f0
--- /dev/null
+++ b/OrthancFramework/SharedLibrary/NOTES.txt
@@ -0,0 +1,67 @@
+
+
+NB: CMake option "ORTHANC_INSTALL_PREFIX" can be used to specify an
+installation directory (to be used with "make install" or "ninja
+install").
+
+
+Dynamic linking under Ubuntu 18.04
+==================================
+
+$ cd i
+$ cmake .. -DCMAKE_BUILD_TYPE=Debug -DALLOW_DOWNLOADS=ON \
+           -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_SYSTEM_GOOGLE_TEST=OFF -G Ninja
+$ ninja -j4
+$ ./UnitTests
+
+
+
+Static linking under GNU/Linux
+==============================
+
+$ cd s
+$ cmake .. -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -G Ninja
+$ ninja -j4
+$ ./UnitTests
+
+
+
+Linux Standard Base
+===================
+
+$ cd lsb
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake /home/jodogne/Subversion/orthanc/OrthancFramework/SharedLibrary -G Ninja \
+  -DCMAKE_TOOLCHAIN_FILE=/home/jodogne/Subversion/orthanc/OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake \
+  -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD=ON
+$ ninja -j4
+
+
+!! For some reason, the linking step works on Ubuntu 16.04, but *not*
+   on Ubuntu 18.04. It looks as it the symbols from the C++ standard
+   were missing.
+
+
+
+Cross-compilation to Windows 32 (using MinGW)
+===============================
+
+$ cd w32
+$ cmake .. -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD=ON -DUSE_LEGACY_LIBICU=ON -G Ninja \
+        -DCMAKE_TOOLCHAIN_FILE=/home/jodogne/Subversion/orthanc/Resources/MinGW-W64-Toolchain32.cmake
+$ ninja -j4
+
+$ cp /usr/i686-w64-mingw32/lib/libwinpthread-1.dll .
+$ cp /usr/lib/gcc/i686-w64-mingw32/7.3-win32/libgcc_s_sjlj-1.dll .
+$ wine ./UnitTests.exe
+
+
+
+WebAssembly (for the "upstream" version of emscripten)
+===========
+
+$ cd wasm
+$ source ~/Downloads/emsdk/emsdk_env.sh
+$ cmake .. -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON -G Ninja \
+        -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake
+$ ninja -j4
+
diff --git a/OrthancFramework/SharedLibrary/OrthancFramework.h.in b/OrthancFramework/SharedLibrary/OrthancFramework.h.in
new file mode 100644
index 0000000..4177133
--- /dev/null
+++ b/OrthancFramework/SharedLibrary/OrthancFramework.h.in
@@ -0,0 +1,308 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+/**
+ * Besides the "pragma once" above that only protects this file,
+ * define a macro to prevent including different versions of
+ * "OrthancFramework.h"
+ **/
+#ifndef __ORTHANC_FRAMEWORK_H
+#define __ORTHANC_FRAMEWORK_H
+
+
+#if defined(_WIN32) || defined (__CYGWIN__)
+#  define ORTHANC_PUBLIC __declspec(dllimport)
+#  define ORTHANC_LOCAL
+#else
+#  if __GNUC__ >= 4
+#    define ORTHANC_PUBLIC __attribute__ ((visibility ("default")))
+#    define ORTHANC_LOCAL  __attribute__ ((visibility ("hidden")))
+#  else
+#    define ORTHANC_PUBLIC
+#    define ORTHANC_LOCAL
+#    pragma warning Unknown dynamic link import/export semantics
+#  endif
+#endif
+
+
+/**
+ * Configuration macros that are always set to the same value if using
+ * "OrthancFrameworkConfiguration.cmake"
+ **/
+
+#define ORTHANC_BUILDING_FRAMEWORK_LIBRARY 0
+#define ORTHANC_ENABLE_BASE64 1
+#define ORTHANC_ENABLE_MD5 1
+
+
+
+/**
+ * Configuration macros that needn't to be renamed
+ **/
+
+#define ORTHANC_SQLITE_VERSION @ORTHANC_SQLITE_VERSION@
+#define ORTHANC_VERSION "@ORTHANC_VERSION@"
+#define ORTHANC_VERSION_MAJOR @ORTHANC_VERSION_MAJOR@
+#define ORTHANC_VERSION_MINOR @ORTHANC_VERSION_MINOR@
+#define ORTHANC_VERSION_REVISION @ORTHANC_VERSION_REVISION@
+
+#cmakedefine01 ORTHANC_ENABLE_CIVETWEB
+#cmakedefine01 ORTHANC_ENABLE_LOGGING
+#cmakedefine01 ORTHANC_ENABLE_LOGGING_STDIO
+#cmakedefine01 ORTHANC_ENABLE_MONGOOSE
+#cmakedefine01 ORTHANC_SANDBOXED
+#cmakedefine01 ORTHANC_STATIC_BOOST
+#cmakedefine01 ORTHANC_STATIC_JSONCPP
+#cmakedefine01 ORTHANC_STATIC_SQLITE
+
+#if ORTHANC_STATIC_BOOST == 1 && !defined(BOOST_LEXICAL_CAST_ASSUME_C_LOCALE)
+#  define BOOST_LEXICAL_CAST_ASSUME_C_LOCALE
+#endif
+
+#if ORTHANC_STATIC_JSONCPP == 1
+#  if defined(JSON_API)
+#    error JSON_API should not be defined
+#  else
+#    define JSON_API @ORTHANC_JSON_API@
+#  endif
+#endif
+
+#if ORTHANC_STATIC_SQLITE == 1
+#  if defined(SQLITE_API)
+#    error SQLITE_API should not be defined
+#  else
+#    define SQLITE_API @ORTHANC_SQLITE_API@
+#  endif
+#endif
+
+#if ORTHANC_STATIC_PUGIXML == 1
+#  if defined(PUGIXML_API)
+#    error PUGIXML_API should not be defined
+#  else
+#    define PUGIXML_API @ORTHANC_PUGIXML_API@
+#  endif
+#endif
+
+
+#define ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(major, minor, revision)      \
+  (ORTHANC_VERSION_MAJOR > major ||                                     \
+   (ORTHANC_VERSION_MAJOR == major &&                                   \
+    (ORTHANC_VERSION_MINOR > minor ||                                   \
+     (ORTHANC_VERSION_MINOR == minor &&                                 \
+      ORTHANC_VERSION_REVISION >= revision))))
+
+
+/**
+ * Configuration macros that must be renamed, prefixing them by
+ * "ORTHANC_"
+ **/
+
+#cmakedefine01 ENABLE_DCMTK
+#if !defined(ENABLE_DCMTK)
+#  error CMake error
+#elif ENABLE_DCMTK == 1
+#  define ORTHANC_ENABLE_DCMTK 1
+#else
+#  define ORTHANC_ENABLE_DCMTK 0
+#endif
+#undef ENABLE_DCMTK
+
+
+#cmakedefine01 ENABLE_DCMTK_NETWORKING
+#if !defined(ENABLE_DCMTK_NETWORKING)
+#  error CMake error
+#elif ENABLE_DCMTK_NETWORKING == 1
+#  define ORTHANC_ENABLE_DCMTK_NETWORKING 1
+#else
+#  define ORTHANC_ENABLE_DCMTK_NETWORKING 0
+#endif
+#undef ENABLE_DCMTK_NETWORKING
+
+
+#cmakedefine01 ENABLE_DCMTK_JPEG
+#if !defined(ENABLE_DCMTK_JPEG)
+#  error CMake error
+#elif ENABLE_DCMTK_JPEG == 1
+#  define ORTHANC_ENABLE_DCMTK_JPEG 1
+#else
+#  define ORTHANC_ENABLE_DCMTK_JPEG 0
+#endif
+#undef ENABLE_DCMTK_JPEG
+
+
+#cmakedefine01 ENABLE_DCMTK_JPEG_LOSSLESS
+#if !defined(ENABLE_DCMTK_JPEG_LOSSLESS)
+#  error CMake error
+#elif ENABLE_DCMTK_JPEG_LOSSLESS == 1
+#  define ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS 1
+#else
+#  define ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS 0
+#endif
+#undef ENABLE_DCMTK_JPEG_LOSSLESS
+
+
+#cmakedefine01 ENABLE_DCMTK_TRANSCODING
+#if !defined(ENABLE_DCMTK_TRANSCODING)
+#  error CMake error
+#elif ENABLE_DCMTK_TRANSCODING == 1
+#  define ORTHANC_ENABLE_DCMTK_TRANSCODING 1
+#else
+#  define ORTHANC_ENABLE_DCMTK_TRANSCODING 0
+#endif
+#undef ENABLE_DCMTK_TRANSCODING
+
+
+#cmakedefine01 ENABLE_JPEG
+#if !defined(ENABLE_JPEG)
+#  error CMake error
+#elif ENABLE_JPEG == 1
+#  define ORTHANC_ENABLE_JPEG 1
+#else
+#  define ORTHANC_ENABLE_JPEG 0
+#endif
+#undef ENABLE_JPEG
+
+
+#cmakedefine01 ENABLE_LOCALE
+#if !defined(ENABLE_LOCALE)
+#  error CMake error
+#elif ENABLE_LOCALE == 1
+#  define ORTHANC_ENABLE_LOCALE 1
+#else
+#  define ORTHANC_ENABLE_LOCALE 0
+#endif
+#undef ENABLE_LOCALE
+
+
+#cmakedefine01 ENABLE_LUA
+#if !defined(ENABLE_LUA)
+#  error CMake error
+#elif ENABLE_LUA == 1
+#  define ORTHANC_ENABLE_LUA 1
+#else
+#  define ORTHANC_ENABLE_LUA 0
+#endif
+#undef ENABLE_LUA
+
+
+#cmakedefine01 ENABLE_PKCS11
+#if !defined(ENABLE_PKCS11)
+#  error CMake error
+#elif ENABLE_PKCS11 == 1
+#  define ORTHANC_ENABLE_PKCS11 1
+#else
+#  define ORTHANC_ENABLE_PKCS11 0
+#endif
+#undef ENABLE_PKCS11
+
+
+#cmakedefine01 ENABLE_PNG
+#if !defined(ENABLE_PNG)
+#  error CMake error
+#elif ENABLE_PNG == 1
+#  define ORTHANC_ENABLE_PNG 1
+#else
+#  define ORTHANC_ENABLE_PNG 0
+#endif
+#undef ENABLE_PNG
+
+
+#cmakedefine01 ENABLE_PUGIXML
+#if !defined(ENABLE_PUGIXML)
+#  error CMake error
+#elif ENABLE_PUGIXML == 1
+#  define ORTHANC_ENABLE_PUGIXML 1
+#else
+#  define ORTHANC_ENABLE_PUGIXML 0
+#endif
+#undef ENABLE_PUGIXML
+
+
+#cmakedefine01 ENABLE_SQLITE
+#if !defined(ENABLE_SQLITE)
+#  error CMake error
+#elif ENABLE_SQLITE == 1
+#  define ORTHANC_ENABLE_SQLITE 1
+#else
+#  define ORTHANC_ENABLE_SQLITE 0
+#endif
+#undef ENABLE_SQLITE
+
+
+#cmakedefine01 ENABLE_SSL
+#if !defined(ENABLE_SSL)
+#  error CMake error
+#elif ENABLE_SSL == 1
+#  define ORTHANC_ENABLE_SSL 1
+#else
+#  define ORTHANC_ENABLE_SSL 0
+#endif
+#undef ENABLE_SSL
+
+
+#cmakedefine01 ENABLE_WEB_CLIENT
+#if !defined(ENABLE_WEB_CLIENT)
+#  error CMake error
+#elif ENABLE_WEB_CLIENT == 1
+#  define ORTHANC_ENABLE_CURL 1
+#else
+#  define ORTHANC_ENABLE_CURL 0
+#endif
+#undef ENABLE_WEB_CLIENT
+
+
+#cmakedefine01 ENABLE_ZLIB
+#if !defined(ENABLE_ZLIB)
+#  error CMake error
+#elif ENABLE_ZLIB == 1
+#  define ORTHANC_ENABLE_ZLIB 1
+#else
+#  define ORTHANC_ENABLE_ZLIB 0
+#endif
+#undef ENABLE_ZLIB
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  define DCMTK_VERSION_NUMBER @DCMTK_VERSION_NUMBER@
+#endif
+
+
+/**
+ * Initialization functions.
+ **/
+
+#include 
+
+namespace Orthanc
+{
+  ORTHANC_PUBLIC void InitializeFramework(const std::string& locale,
+                                          bool loadPrivateDictionary);
+  
+  ORTHANC_PUBLIC void FinalizeFramework();
+}
+
+
+#endif /* __ORTHANC_FRAMEWORK_H */
diff --git a/OrthancFramework/Sources/Cache/ICachePageProvider.h b/OrthancFramework/Sources/Cache/ICachePageProvider.h
new file mode 100644
index 0000000..e463629
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/ICachePageProvider.h
@@ -0,0 +1,44 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include 
+#include "../IDynamicObject.h"
+
+namespace Orthanc
+{
+  namespace Deprecated
+  {
+    class ICachePageProvider
+    {
+    public:
+      virtual ~ICachePageProvider()
+      {
+      }
+
+      virtual IDynamicObject* Provide(const std::string& id) = 0;
+    };
+  }
+}
diff --git a/OrthancFramework/Sources/Cache/ICacheable.h b/OrthancFramework/Sources/Cache/ICacheable.h
new file mode 100644
index 0000000..270e310
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/ICacheable.h
@@ -0,0 +1,40 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include 
+
+namespace Orthanc
+{
+  class ICacheable : public boost::noncopyable
+  {
+  public:
+    virtual ~ICacheable()
+    {
+    }
+
+    virtual size_t GetMemoryUsage() const = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h b/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h
new file mode 100644
index 0000000..d5e3fa8
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h
@@ -0,0 +1,349 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+namespace Orthanc
+{
+  /**
+   * This class implements the index of a cache with least recently
+   * used (LRU) recycling policy. All the items of the cache index
+   * can be associated with a payload.
+   * Reference: http://stackoverflow.com/a/2504317
+   **/
+  template 
+  class LeastRecentlyUsedIndex : public boost::noncopyable
+  {
+  private:
+    typedef std::list< std::pair >  Queue;
+    typedef std::map  Index;
+
+    Index  index_;
+    Queue  queue_;
+
+    /**
+     * Internal method for debug builds to check whether the internal
+     * data structures are not corrupted.
+     **/
+    void CheckInvariants() const;
+
+  public:
+    /**
+     * Add a new element to the cache index, and make it the most
+     * recent element.
+     * \param id The ID of the element.
+     * \param payload The payload of the element.
+     **/
+    void Add(T id, Payload payload = Payload());
+
+    void AddOrMakeMostRecent(T id, Payload payload = Payload());
+
+    /**
+     * When accessing an element of the cache, this method tags the
+     * element as the most recently used.
+     * \param id The most recently accessed item.
+     **/
+    void MakeMostRecent(T id);
+
+    void MakeMostRecent(T id, Payload updatedPayload);
+
+    /**
+     * Remove an element from the cache index.
+     * \param id The item to remove.
+     **/
+    Payload Invalidate(T id);
+
+    /**
+     * Get the oldest element in the cache and remove it.
+     * \return The oldest item.
+     **/
+    T RemoveOldest();
+
+    /**
+     * Get the oldest element in the cache, remove it and return the
+     * associated payload.
+     * \param payload Where to store the associated payload.
+     * \return The oldest item.
+     **/
+    T RemoveOldest(Payload& payload);
+
+    /**
+     * Check whether an element is contained in the cache.
+     * \param id The item.
+     * \return \c true iff the item is indexed by the cache.
+     **/
+    bool Contains(T id) const
+    {
+      return index_.find(id) != index_.end();
+    }
+
+    bool Contains(T id, Payload& payload) const
+    {
+      typename Index::const_iterator it = index_.find(id);
+      if (it == index_.end())
+      {
+        return false;
+      }
+      else
+      {
+        payload = it->second->second;
+        return true;
+      }
+    }
+
+    /**
+     * Return the number of elements in the cache.
+     * \return The number of elements.
+     **/
+    size_t GetSize() const
+    {
+      assert(index_.size() == queue_.size());
+      return queue_.size();
+    }
+
+    /**
+     * Check whether the cache index is empty.
+     * \return \c true iff the cache is empty.
+     **/
+    bool IsEmpty() const
+    {
+      return index_.empty();
+    }
+
+    const T& GetOldest() const;
+    
+    const Payload& GetOldestPayload() const;
+
+    void GetAllKeys(std::vector& keys) const
+    {
+      keys.clear();
+      keys.reserve(GetSize());
+      for (typename Index::const_iterator it = index_.begin(); it != index_.end(); ++it)
+      {
+        keys.push_back(it->first);
+      }
+    }
+  };
+
+
+
+
+  /******************************************************************
+   ** Implementation of the template
+   ******************************************************************/
+
+  template 
+  void LeastRecentlyUsedIndex::CheckInvariants() const
+  {
+#ifndef NDEBUG
+    assert(index_.size() == queue_.size());
+
+    for (typename Index::const_iterator 
+           it = index_.begin(); it != index_.end(); ++it)
+    {
+      assert(it->second != queue_.end());
+      assert(it->second->first == it->first);
+    }
+#endif
+  }
+
+
+  template 
+  void LeastRecentlyUsedIndex::Add(T id, Payload payload)
+  {
+    if (Contains(id))
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    queue_.push_front(std::make_pair(id, payload));
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template 
+  void LeastRecentlyUsedIndex::MakeMostRecent(T id)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    std::pair item = *(it->second);
+    
+    queue_.erase(it->second);
+    queue_.push_front(item);
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template 
+  void LeastRecentlyUsedIndex::AddOrMakeMostRecent(T id, Payload payload)
+  {
+    typename Index::iterator it = index_.find(id);
+
+    if (it != index_.end())
+    {
+      // Already existing. Make it most recent.
+      std::pair item = *(it->second);
+      item.second = payload;
+      queue_.erase(it->second);
+      queue_.push_front(item);
+    }
+    else
+    {
+      // New item
+      queue_.push_front(std::make_pair(id, payload));
+    }
+
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template 
+  void LeastRecentlyUsedIndex::MakeMostRecent(T id, Payload updatedPayload)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    std::pair item = *(it->second);
+    item.second = updatedPayload;
+    
+    queue_.erase(it->second);
+    queue_.push_front(item);
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template 
+  Payload LeastRecentlyUsedIndex::Invalidate(T id)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    Payload payload = it->second->second;
+    queue_.erase(it->second);
+    index_.erase(it);
+
+    CheckInvariants();
+    return payload;
+  }
+
+
+  template 
+  T LeastRecentlyUsedIndex::RemoveOldest(Payload& payload)
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::pair item = queue_.back();
+    T oldest = item.first;
+    payload = item.second;
+
+    queue_.pop_back();
+    assert(index_.find(oldest) != index_.end());
+    index_.erase(oldest);
+
+    CheckInvariants();
+
+    return oldest;
+  }
+
+
+  template 
+  T LeastRecentlyUsedIndex::RemoveOldest()
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::pair item = queue_.back();
+    T oldest = item.first;
+
+    queue_.pop_back();
+    assert(index_.find(oldest) != index_.end());
+    index_.erase(oldest);
+
+    CheckInvariants();
+
+    return oldest;
+  }
+
+
+  template 
+  const T& LeastRecentlyUsedIndex::GetOldest() const
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return queue_.back().first;
+  }
+
+
+  template 
+  const Payload& LeastRecentlyUsedIndex::GetOldestPayload() const
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return queue_.back().second;
+  }
+}
diff --git a/OrthancFramework/Sources/Cache/MemoryCache.cpp b/OrthancFramework/Sources/Cache/MemoryCache.cpp
new file mode 100644
index 0000000..f20056e
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/MemoryCache.cpp
@@ -0,0 +1,102 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryCache.h"
+
+#include "../Logging.h"
+
+namespace Orthanc
+{
+  namespace Deprecated
+  {
+    MemoryCache::Page& MemoryCache::Load(const std::string& id)
+    {
+      // Reuse the cache entry if it already exists
+      Page* p = NULL;
+      if (index_.Contains(id, p))
+      {
+        LOG(TRACE) << "Reusing a cache page";
+        assert(p != NULL);
+        index_.MakeMostRecent(id);
+        return *p;
+      }
+
+      // The id is not in the cache yet. Make some room if the cache
+      // is full.
+      if (index_.GetSize() == cacheSize_)
+      {
+        LOG(TRACE) << "Dropping the oldest cache page";
+        index_.RemoveOldest(p);
+        delete p;
+      }
+
+      // Create a new cache page
+      std::unique_ptr result(new Page);
+      result->id_ = id;
+      result->content_.reset(provider_.Provide(id));
+
+      // Add the newly create page to the cache
+      LOG(TRACE) << "Registering new data in a cache page";
+      p = result.release();
+      index_.Add(id, p);
+      return *p;
+    }
+
+    MemoryCache::MemoryCache(ICachePageProvider& provider,
+                             size_t cacheSize) : 
+      provider_(provider),
+      cacheSize_(cacheSize)
+    {
+    }
+
+    void MemoryCache::Invalidate(const std::string& id)
+    {
+      Page* p = NULL;
+      if (index_.Contains(id, p))
+      {
+        LOG(TRACE) << "Invalidating a cache page";
+        assert(p != NULL);
+        delete p;
+        index_.Invalidate(id);
+      }
+    }
+
+    MemoryCache::~MemoryCache()
+    {
+      while (!index_.IsEmpty())
+      {
+        Page* element = NULL;
+        index_.RemoveOldest(element);
+        assert(element != NULL);
+        delete element;
+      }
+    }
+
+    IDynamicObject& MemoryCache::Access(const std::string& id)
+    {
+      return *Load(id).content_;
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/Cache/MemoryCache.h b/OrthancFramework/Sources/Cache/MemoryCache.h
new file mode 100644
index 0000000..86a9ef4
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/MemoryCache.h
@@ -0,0 +1,66 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Compatibility.h"
+#include "ICachePageProvider.h"
+#include "LeastRecentlyUsedIndex.h"
+
+#include 
+
+namespace Orthanc
+{
+  namespace Deprecated
+  {
+    /**
+     * WARNING: This class is NOT thread-safe.
+     **/
+    class ORTHANC_PUBLIC MemoryCache : public boost::noncopyable
+    {
+    private:
+      struct Page
+      {
+        std::string id_;
+        std::unique_ptr content_;
+      };
+
+      ICachePageProvider& provider_;
+      size_t cacheSize_;
+      LeastRecentlyUsedIndex  index_;
+
+      Page& Load(const std::string& id);
+
+    public:
+      MemoryCache(ICachePageProvider& provider,
+                  size_t cacheSize);
+
+      ~MemoryCache();
+
+      IDynamicObject& Access(const std::string& id);
+
+      void Invalidate(const std::string& id);
+    };
+  }
+}
diff --git a/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp
new file mode 100644
index 0000000..693484c
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp
@@ -0,0 +1,293 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryObjectCache.h"
+
+#include "../Compatibility.h"
+
+namespace Orthanc
+{
+  class MemoryObjectCache::Item : public boost::noncopyable
+  {
+  private:
+    ICacheable*               value_;
+    boost::posix_time::ptime  time_;
+
+  public:
+    explicit Item(ICacheable* value) :   // Takes ownership
+    value_(value),
+    time_(boost::posix_time::second_clock::local_time())
+    {
+      if (value == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    ~Item()
+    {
+      assert(value_ != NULL);
+      delete value_;
+    }
+
+    ICacheable& GetValue() const
+    {
+      assert(value_ != NULL);
+      return *value_;
+    }
+
+    const boost::posix_time::ptime& GetTime() const
+    {
+      return time_;
+    }
+  };
+
+
+  void MemoryObjectCache::Recycle(size_t targetSize)
+  {
+    // WARNING: "cacheMutex_" must be locked
+    while (currentSize_ > targetSize)
+    {
+      assert(!content_.IsEmpty());
+        
+      Item* item = NULL;
+      content_.RemoveOldest(item);
+
+      assert(item != NULL);
+      const size_t size = item->GetValue().GetMemoryUsage();
+      delete item;
+
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    // Post-condition: "currentSize_ <= targetSize"
+  }
+    
+
+  MemoryObjectCache::MemoryObjectCache() :
+    currentSize_(0),
+    maxSize_(100 * 1024 * 1024)  // 100 MB
+  {
+  }
+
+
+  MemoryObjectCache::~MemoryObjectCache()
+  {
+    Recycle(0);
+    assert(content_.IsEmpty());
+  }
+
+
+  size_t MemoryObjectCache::GetNumberOfItems()
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(cacheMutex_);
+#endif
+
+    return content_.GetSize();
+  }
+  
+
+  size_t MemoryObjectCache::GetCurrentSize()
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(cacheMutex_);
+#endif
+
+    return currentSize_;
+  }
+
+
+  size_t MemoryObjectCache::GetMaximumSize()
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(cacheMutex_);
+#endif
+
+    return maxSize_;
+  }
+
+
+  void MemoryObjectCache::SetMaximumSize(size_t size)
+  {
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+      
+#if !defined(__EMSCRIPTEN__)
+    // Make sure no accessor is currently open (as its data may be
+    // removed if recycling is needed)
+    WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+#endif
+
+    Recycle(size);
+    maxSize_ = size;
+  }
+
+
+  void MemoryObjectCache::Acquire(const std::string& key,
+                                  ICacheable* value)
+  {
+    std::unique_ptr item(new Item(value));
+
+    if (value == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+#if !defined(__EMSCRIPTEN__)
+      // Make sure no accessor is currently open (as its data may be
+      // removed if recycling is needed)
+      WriterLock contentLock(contentMutex_);
+
+      // Lock the global structure of the cache
+      boost::mutex::scoped_lock cacheLock(cacheMutex_);
+#endif
+
+      const size_t size = item->GetValue().GetMemoryUsage();
+
+      if (size > maxSize_)
+      {
+        // This object is too large to be stored in the cache, discard it
+      }
+      else if (content_.Contains(key))
+      {
+        // Value already stored, don't overwrite the old value
+        content_.MakeMostRecent(key);
+      }
+      else
+      {
+        Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
+        assert(currentSize_ + size <= maxSize_);
+
+        content_.Add(key, item.release());
+        currentSize_ += size;
+      }
+    }
+  }
+
+
+  void MemoryObjectCache::Invalidate(const std::string& key)
+  {
+#if !defined(__EMSCRIPTEN__)
+    // Make sure no accessor is currently open (as it may correspond
+    // to the key to remove)
+    WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+#endif
+
+    Item* item = NULL;
+    if (content_.Contains(key, item))
+    {
+      assert(item != NULL);
+      const size_t size = item->GetValue().GetMemoryUsage();
+      delete item;
+
+      content_.Invalidate(key);
+          
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+  }
+
+
+  MemoryObjectCache::Accessor::Accessor(MemoryObjectCache& cache,
+                                        const std::string& key,
+                                        bool unique) :
+    item_(NULL)
+  {
+#if !defined(__EMSCRIPTEN__)
+    if (unique)
+    {
+      writerLock_ = WriterLock(cache.contentMutex_);
+    }
+    else
+    {
+      readerLock_ = ReaderLock(cache.contentMutex_);
+    }
+
+    // Lock the global structure of the cache, must be *after* the
+    // reader/writer lock
+    cacheLock_ = boost::mutex::scoped_lock(cache.cacheMutex_);
+#endif
+
+    if (cache.content_.Contains(key, item_))
+    {
+      cache.content_.MakeMostRecent(key);
+    }
+    
+#if !defined(__EMSCRIPTEN__)
+    cacheLock_.unlock();
+
+    if (item_ == NULL)
+    {
+      // This item does not exist in the cache, we can release the
+      // reader/writer lock
+      if (unique)
+      {
+        writerLock_.unlock();
+      }
+      else
+      {
+        readerLock_.unlock();
+      }
+    }
+#endif
+  }
+
+
+  ICacheable& MemoryObjectCache::Accessor::GetValue() const
+  {
+    if (IsValid())
+    {
+      return item_->GetValue();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const boost::posix_time::ptime& MemoryObjectCache::Accessor::GetTime() const
+  {
+    if (IsValid())
+    {
+      return item_->GetTime();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }        
+  }
+}
diff --git a/OrthancFramework/Sources/Cache/MemoryObjectCache.h b/OrthancFramework/Sources/Cache/MemoryObjectCache.h
new file mode 100644
index 0000000..9ff4334
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h
@@ -0,0 +1,111 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+#include "ICacheable.h"
+#include "LeastRecentlyUsedIndex.h"
+
+#if !defined(__EMSCRIPTEN__)
+// Multithreading is not supported in WebAssembly
+#  include 
+#  include 
+#endif
+
+#include 
+
+
+namespace Orthanc
+{
+  /**
+   *  Note: this class is thread safe
+   **/
+  class ORTHANC_PUBLIC MemoryObjectCache : public boost::noncopyable
+  {
+  private:
+    class Item;
+
+#if !defined(__EMSCRIPTEN__)
+    typedef boost::unique_lock WriterLock;
+    typedef boost::shared_lock ReaderLock;
+
+    // This mutex protects modifications to the structure of the cache (monitor)
+    boost::mutex   cacheMutex_;
+
+    // This mutex protects modifications to the items that are stored in the cache
+    boost::shared_mutex contentMutex_;
+#endif
+
+    size_t currentSize_;
+    size_t maxSize_;
+    LeastRecentlyUsedIndex  content_;
+
+    void Recycle(size_t targetSize);
+    
+  public:
+    MemoryObjectCache();
+
+    ~MemoryObjectCache();
+
+    size_t GetNumberOfItems();  // For unit tests only
+
+    size_t GetCurrentSize();  // For unit tests only
+
+    size_t GetMaximumSize();
+
+    void SetMaximumSize(size_t size);
+
+    void Acquire(const std::string& key,
+                 ICacheable* value);
+
+    void Invalidate(const std::string& key);
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock                 readerLock_;
+      WriterLock                 writerLock_;
+      boost::mutex::scoped_lock  cacheLock_;
+#endif
+      
+      Item*  item_;
+
+    public:
+      Accessor(MemoryObjectCache& cache,
+               const std::string& key,
+               bool unique);
+
+      bool IsValid() const
+      {
+        return item_ != NULL;
+      }
+
+      ICacheable& GetValue() const;
+
+      const boost::posix_time::ptime& GetTime() const;
+    };
+  };
+}
diff --git a/OrthancFramework/Sources/Cache/MemoryStringCache.cpp b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp
new file mode 100644
index 0000000..204f2b8
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp
@@ -0,0 +1,288 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryStringCache.h"
+
+namespace Orthanc
+{
+  class MemoryStringCache::StringValue : public ICacheable
+  {
+  private:
+    std::string  content_;
+
+  public:
+    explicit StringValue(const std::string& content) :
+      content_(content)
+    {
+    }
+
+    explicit StringValue(const char* buffer, size_t size) :
+      content_(buffer, size)
+    {
+    }
+
+    const std::string& GetContent() const
+    {
+      return content_;
+    }
+
+    virtual size_t GetMemoryUsage() const
+    {
+      return content_.size();
+    }      
+  };
+
+
+  MemoryStringCache::Accessor::Accessor(MemoryStringCache& cache)
+  : cache_(cache),
+    shouldAdd_(false)
+  {
+  }
+
+
+  MemoryStringCache::Accessor::~Accessor()
+  {
+    // if this accessor was the one in charge of loading and adding the data into the cache
+    // and it failed to add, remove the key from the list to make sure others accessor
+    // stop waiting for it.
+    if (shouldAdd_)
+    {
+      cache_.RemoveFromItemsBeingLoaded(keyToAdd_);
+    }
+  }
+
+
+  bool MemoryStringCache::Accessor::Fetch(std::string& value, const std::string& key)
+  {
+    // if multiple accessors are fetching at the same time:
+    // the first one will return false and will be in charge of adding to the cache.
+    // others will wait.
+    // if the first one fails to add, or, if the content was too large to fit in the cache,
+    // the next one will be in charge of adding ...
+    if (!cache_.Fetch(value, key))
+    {
+      shouldAdd_ = true;
+      keyToAdd_ = key;
+      return false;
+    }
+
+    shouldAdd_ = false;
+    keyToAdd_.clear();
+
+    return true;
+  }
+
+
+  void MemoryStringCache::Accessor::Add(const std::string& key, const std::string& value)
+  {
+    cache_.Add(key, value);
+    shouldAdd_ = false;
+  }
+
+
+  void MemoryStringCache::Accessor::Add(const std::string& key, const char* buffer, size_t size)
+  {
+    cache_.Add(key, buffer, size);
+    shouldAdd_ = false;
+  }
+
+
+  MemoryStringCache::MemoryStringCache() :
+    currentSize_(0),
+    maxSize_(100 * 1024 * 1024)  // 100 MB
+  {
+  }
+
+
+  MemoryStringCache::~MemoryStringCache()
+  {
+    Recycle(0);
+    assert(content_.IsEmpty());
+  }
+
+
+  size_t MemoryStringCache::GetMaximumSize()
+  {
+    return maxSize_;
+  }
+
+
+  void MemoryStringCache::SetMaximumSize(size_t size)
+  {
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+      
+    // // Make sure no accessor is currently open (as its data may be
+    // // removed if recycling is needed)
+    // WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    Recycle(size);
+    maxSize_ = size;
+  }
+
+
+  void MemoryStringCache::Add(const std::string& key,
+                               const std::string& value)
+  {
+    std::unique_ptr item(new StringValue(value));
+    size_t size = value.size();
+
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    if (size > maxSize_)
+    {
+      // This object is too large to be stored in the cache, discard it
+    }
+    else if (content_.Contains(key))
+    {
+      // Value already stored, don't overwrite the old value but put it on top of the cache
+      content_.MakeMostRecent(key);
+    }
+    else
+    {
+      Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
+      assert(currentSize_ + size <= maxSize_);
+
+      content_.Add(key, item.release());
+      currentSize_ += size;
+    }
+
+    RemoveFromItemsBeingLoadedInternal(key);
+  }
+
+
+  void MemoryStringCache::Add(const std::string& key,
+                              const void* buffer,
+                              size_t size)
+  {
+    Add(key, std::string(reinterpret_cast(buffer), size));
+  }
+
+
+  void MemoryStringCache::Invalidate(const std::string &key)
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    StringValue* item = NULL;
+    if (content_.Contains(key, item))
+    {
+      assert(item != NULL);
+      const size_t size = item->GetMemoryUsage();
+      delete item;
+
+      content_.Invalidate(key);
+          
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    RemoveFromItemsBeingLoadedInternal(key);
+  }
+
+
+  bool MemoryStringCache::Fetch(std::string& value,
+                                const std::string& key)
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    StringValue* item;
+
+    // if another client is currently loading the item, wait for it.
+    while (itemsBeingLoaded_.find(key) != itemsBeingLoaded_.end() && !content_.Contains(key, item))
+    {
+      cacheCond_.wait(cacheLock);
+    }
+
+    if (content_.Contains(key, item))
+    {
+      value = dynamic_cast(*item).GetContent();
+      content_.MakeMostRecent(key);
+
+      return true;
+    }
+    else
+    {
+      // note that this accessor will be in charge of loading and adding.
+      itemsBeingLoaded_.insert(key);
+      return false;
+    }
+  }
+
+
+  void MemoryStringCache::RemoveFromItemsBeingLoaded(const std::string& key)
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+    RemoveFromItemsBeingLoadedInternal(key);
+  }
+
+
+  void MemoryStringCache::RemoveFromItemsBeingLoadedInternal(const std::string& key)
+  {
+    // notify all waiting users, some of them potentially waiting for this item
+    itemsBeingLoaded_.erase(key);
+    cacheCond_.notify_all();
+  }
+
+  void MemoryStringCache::Recycle(size_t targetSize)
+  {
+    // WARNING: "cacheMutex_" must be locked
+    while (currentSize_ > targetSize)
+    {
+      assert(!content_.IsEmpty());
+        
+      StringValue* item = NULL;
+      content_.RemoveOldest(item);
+
+      assert(item != NULL);
+      const size_t size = item->GetMemoryUsage();
+      delete item;
+
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    // Post-condition: "currentSize_ <= targetSize"
+  }
+
+  size_t MemoryStringCache::GetCurrentSize() const
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+
+    return currentSize_;
+  }
+    
+  size_t MemoryStringCache::GetNumberOfItems() const
+  {
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+    return content_.GetSize();
+
+  }
+
+}
diff --git a/OrthancFramework/Sources/Cache/MemoryStringCache.h b/OrthancFramework/Sources/Cache/MemoryStringCache.h
new file mode 100644
index 0000000..a89395c
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h
@@ -0,0 +1,115 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+#include "ICacheable.h"
+#include "LeastRecentlyUsedIndex.h"
+
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  /**
+   * Class that caches a dictionary
+   * of strings, using the "fetch/add" paradigm of memcached.
+   * 
+   * Starting from 1.12.2, if multiple clients are trying to access
+   * an inexistent item at the same time, only one of them will load it
+   * and the others will wait until the first one has loaded the data.
+   * 
+   * The MemoryStringCache is only accessible through an Accessor.
+   * 
+   * Note: this class is thread safe
+   **/
+  class ORTHANC_PUBLIC MemoryStringCache : public boost::noncopyable
+  {
+  public:
+    class ORTHANC_PUBLIC Accessor : public boost::noncopyable
+    {
+    protected:
+      MemoryStringCache& cache_;
+
+    private:
+      bool                shouldAdd_;  // when this accessor is the one who should load and add the data
+      std::string         keyToAdd_;
+
+
+    public:
+      explicit Accessor(MemoryStringCache& cache);
+      ~Accessor();
+
+      bool Fetch(std::string& value, const std::string& key);
+      void Add(const std::string& key, const std::string& value);
+      void Add(const std::string& key,const char* buffer, size_t size);
+    };
+
+  private:
+    class StringValue;
+
+    mutable boost::mutex      cacheMutex_;  // note: we can not use recursive_mutex with condition_variable
+    boost::condition_variable cacheCond_;
+    std::set     itemsBeingLoaded_;
+
+    size_t currentSize_;
+    size_t maxSize_;
+    LeastRecentlyUsedIndex  content_;
+
+    void Recycle(size_t targetSize);
+
+  public:
+    MemoryStringCache();
+
+    ~MemoryStringCache();
+
+    size_t GetMaximumSize();
+    
+    void SetMaximumSize(size_t size);
+
+    void Invalidate(const std::string& key);
+
+    size_t GetCurrentSize() const;
+    
+    size_t GetNumberOfItems() const;
+
+  private:
+    void Add(const std::string& key,
+             const std::string& value);
+
+    void Add(const std::string& key,
+             const void* buffer,
+             size_t size);
+
+    bool Fetch(std::string& value,
+               const std::string& key);
+
+    void RemoveFromItemsBeingLoaded(const std::string& key);
+    void RemoveFromItemsBeingLoadedInternal(const std::string& key);
+
+    void AddToItemsBeingLoadedInternal(const std::string& key);
+  };
+}
diff --git a/OrthancFramework/Sources/Cache/SharedArchive.cpp b/OrthancFramework/Sources/Cache/SharedArchive.cpp
new file mode 100644
index 0000000..9dd0917
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/SharedArchive.cpp
@@ -0,0 +1,145 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "SharedArchive.h"
+
+#include "../Toolbox.h"
+
+
+namespace Orthanc
+{
+  void SharedArchive::RemoveInternal(const std::string& id)
+  {
+    Archive::iterator it = archive_.find(id);
+
+    if (it != archive_.end())
+    {
+      delete it->second;
+      archive_.erase(it);
+
+      lru_.Invalidate(id);
+    }
+  }
+
+
+  SharedArchive::Accessor::Accessor(SharedArchive& that,
+                                    const std::string& id) :
+    lock_(that.mutex_)
+  {
+    Archive::iterator it = that.archive_.find(id);
+
+    if (it == that.archive_.end())
+    {
+      item_ = NULL;
+    }
+    else
+    {
+      that.lru_.MakeMostRecent(id);
+      item_ = it->second;
+    }
+  }
+
+  bool SharedArchive::Accessor::IsValid() const
+  {
+    return item_ != NULL;
+  }
+
+
+  IDynamicObject& SharedArchive::Accessor::GetItem() const
+  {
+    if (item_ == NULL)
+    {
+      // "IsValid()" should have been called
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *item_;
+    }
+  }  
+
+
+  SharedArchive::SharedArchive(size_t maxSize) : 
+    maxSize_(maxSize)
+  {
+    if (maxSize == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  SharedArchive::~SharedArchive()
+  {
+    for (Archive::iterator it = archive_.begin();
+         it != archive_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  std::string SharedArchive::Add(IDynamicObject* obj)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (archive_.size() == maxSize_)
+    {
+      // The quota has been reached, remove the oldest element
+      RemoveInternal(lru_.GetOldest());
+    }
+
+    std::string id = Toolbox::GenerateUuid();
+    RemoveInternal(id);  // Should never be useful because of UUID
+
+    archive_[id] = obj;
+    lru_.Add(id);
+
+    return id;
+  }
+
+
+  void SharedArchive::Remove(const std::string& id)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    RemoveInternal(id);      
+  }
+
+
+  void SharedArchive::List(std::list& items)
+  {
+    items.clear();
+
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);
+
+      for (Archive::const_iterator it = archive_.begin();
+           it != archive_.end(); ++it)
+      {
+        items.push_back(it->first);
+      }
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/Cache/SharedArchive.h b/OrthancFramework/Sources/Cache/SharedArchive.h
new file mode 100644
index 0000000..53396e7
--- /dev/null
+++ b/OrthancFramework/Sources/Cache/SharedArchive.h
@@ -0,0 +1,84 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class SharedArchive cannot be used in sandboxed environments
+#endif
+
+#include "LeastRecentlyUsedIndex.h"
+#include "../IDynamicObject.h"
+
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC SharedArchive : public boost::noncopyable
+  {
+  private:
+    typedef std::map  Archive;
+
+    size_t                  maxSize_;
+    boost::recursive_mutex  mutex_;
+    Archive                 archive_;
+    LeastRecentlyUsedIndex lru_;
+
+    void RemoveInternal(const std::string& id);
+
+  public:
+    class ORTHANC_PUBLIC Accessor : public boost::noncopyable
+    {
+    private:
+      boost::recursive_mutex::scoped_lock lock_;
+      IDynamicObject*                     item_;
+
+    public:
+      Accessor(SharedArchive& that,
+               const std::string& id);
+
+      bool IsValid() const;
+      
+      IDynamicObject& GetItem() const;
+    };
+
+
+    explicit SharedArchive(size_t maxSize);
+
+    ~SharedArchive();
+
+    std::string Add(IDynamicObject* obj);  // Takes the ownership
+
+    void Remove(const std::string& id);
+
+    void List(std::list& items);
+  };
+}
+
+
diff --git a/OrthancFramework/Sources/ChunkedBuffer.cpp b/OrthancFramework/Sources/ChunkedBuffer.cpp
new file mode 100644
index 0000000..8f6a807
--- /dev/null
+++ b/OrthancFramework/Sources/ChunkedBuffer.cpp
@@ -0,0 +1,242 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "ChunkedBuffer.h"
+
+#include "OrthancException.h"
+
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  void ChunkedBuffer::Clear()
+  {
+    numBytes_ = 0;
+    pendingPos_ = 0;
+
+    for (Chunks::iterator it = chunks_.begin(); 
+         it != chunks_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  void ChunkedBuffer::AddChunkInternal(const void* chunkData,
+                                       size_t chunkSize)
+  {
+    if (chunkSize == 0)
+    {
+      return;
+    }
+    else
+    {
+      assert(chunkData != NULL);
+
+      try
+      {
+        chunks_.push_back(new std::string(reinterpret_cast(chunkData), chunkSize));
+      }
+      catch (...)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      numBytes_ += chunkSize;
+    }
+  }
+
+
+  void ChunkedBuffer::FlushPendingBuffer()
+  {
+    assert(pendingPos_ <= pendingBuffer_.size());
+    
+    if (!pendingBuffer_.empty())
+    {
+      AddChunkInternal(pendingBuffer_.c_str(), pendingPos_);
+    }
+    else
+    {
+      assert(pendingPos_ == 0);
+    }
+
+    pendingPos_ = 0;
+  }
+
+
+  ChunkedBuffer::ChunkedBuffer() :
+    numBytes_(0),
+    pendingPos_(0)
+  {
+    pendingBuffer_.resize(16 * 1024);  // Default size of the pending buffer: 16KB
+  }
+
+
+  ChunkedBuffer::~ChunkedBuffer()
+  {
+    Clear();
+  }
+
+
+  size_t ChunkedBuffer::GetNumBytes() const
+  {
+    return numBytes_ + pendingPos_;
+  }
+  
+
+  void ChunkedBuffer::SetPendingBufferSize(size_t size)
+  {
+    FlushPendingBuffer();
+    pendingBuffer_.resize(size);
+  }
+  
+
+  size_t ChunkedBuffer::GetPendingBufferSize() const
+  {
+    return pendingBuffer_.size();
+  }
+
+  
+  void ChunkedBuffer::AddChunk(const void* chunkData,
+                               size_t chunkSize)
+  {
+    if (chunkSize > 0)
+    {
+#if 1
+      assert(sizeof(char) == 1);
+      
+      // Optimization if Orthanc >= 1.7.3, to speed up in the presence of many small chunks
+      if (pendingPos_ + chunkSize <= pendingBuffer_.size())
+      {
+        // There remains enough place in the pending buffer
+        memcpy(&pendingBuffer_[pendingPos_], chunkData, chunkSize);
+        pendingPos_ += chunkSize;
+      }
+      else
+      {
+        FlushPendingBuffer();
+
+        if (!pendingBuffer_.empty() &&
+            chunkSize < pendingBuffer_.size())
+        {
+          memcpy(&pendingBuffer_[0], chunkData, chunkSize);
+          pendingPos_ = chunkSize;
+        }
+        else
+        {
+          AddChunkInternal(chunkData, chunkSize);
+        }
+      }
+#else
+      // Non-optimized implementation in Orthanc <= 1.7.2
+      AddChunkInternal(chunkData, chunkSize);
+#endif
+    }
+  }
+
+
+  void ChunkedBuffer::AddChunk(const std::string& chunk)
+  {
+    if (chunk.size() > 0)
+    {
+      AddChunk(&chunk[0], chunk.size());
+    }
+  }
+
+
+  void ChunkedBuffer::AddChunk(const std::string::const_iterator& begin,
+                               const std::string::const_iterator& end)
+  {
+    const size_t s = end - begin;
+
+    if (s > 0)
+    {
+      AddChunk(&begin[0], s);
+    }
+  }
+  
+
+  void ChunkedBuffer::Flatten(std::string& result)
+  {
+    FlushPendingBuffer();
+
+    if (chunks_.empty())
+    {
+      if (numBytes_ != 0)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      result.clear();
+    }
+    else if (chunks_.size() == 1)
+    {
+      // Avoid reallocating a buffer if there is a single chunk
+      assert(chunks_.front() != NULL);
+      if (chunks_.front()->size() != numBytes_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        chunks_.front()->swap(result);
+        delete chunks_.front();
+      }
+    }
+    else
+    {
+      try
+      {
+        result.resize(numBytes_);
+      }
+      catch (...)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      size_t pos = 0;
+      for (Chunks::iterator it = chunks_.begin();
+           it != chunks_.end(); ++it)
+      {
+        assert(*it != NULL);
+
+        size_t s = (*it)->size();
+        if (s != 0)
+        {
+          memcpy(&result[pos], (*it)->c_str(), s);
+          pos += s;
+        }
+
+        delete *it;
+      }
+    }
+
+    // Reset the data structure
+    chunks_.clear();
+    numBytes_ = 0;
+  }
+}
diff --git a/OrthancFramework/Sources/ChunkedBuffer.h b/OrthancFramework/Sources/ChunkedBuffer.h
new file mode 100644
index 0000000..4fd1408
--- /dev/null
+++ b/OrthancFramework/Sources/ChunkedBuffer.h
@@ -0,0 +1,73 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "OrthancFramework.h"
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ChunkedBuffer : public boost::noncopyable
+  {
+  private:
+    typedef std::list  Chunks;
+    
+    size_t       numBytes_;
+    Chunks       chunks_;
+    std::string  pendingBuffer_;   // Buffer to speed up if adding many small chunks
+    size_t       pendingPos_;
+  
+    void Clear();
+
+    void AddChunkInternal(const void* chunkData,
+                          size_t chunkSize);
+
+    void FlushPendingBuffer();
+
+  public:
+    ChunkedBuffer();
+
+    ~ChunkedBuffer();
+
+    size_t GetNumBytes() const;
+
+    void SetPendingBufferSize(size_t size);
+
+    size_t GetPendingBufferSize() const;
+
+    void AddChunk(const void* chunkData,
+                  size_t chunkSize);
+
+    void AddChunk(const std::string& chunk);
+
+    void AddChunk(const std::string::const_iterator& begin,
+                  const std::string::const_iterator& end);
+
+    void Flatten(std::string& result);
+  };
+}
diff --git a/OrthancFramework/Sources/Compatibility.h b/OrthancFramework/Sources/Compatibility.h
new file mode 100644
index 0000000..42908d9
--- /dev/null
+++ b/OrthancFramework/Sources/Compatibility.h
@@ -0,0 +1,152 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+
+// Macro "ORTHANC_FORCE_INLINE" forces a function/method to be inlined
+#if defined(_MSC_VER)
+#  define ORTHANC_FORCE_INLINE __forceinline
+#elif defined(__GNUC__) || defined(__clang__) || defined(__EMSCRIPTEN__)
+#  define ORTHANC_FORCE_INLINE inline __attribute((always_inline))
+#else
+#  error Please support your compiler here
+#endif
+
+
+// Macro "ORTHANC_DEPRECATED" tags a function as having been deprecated
+#if (__cplusplus >= 201402L)  // C++14
+#  define ORTHANC_DEPRECATED(f) [[deprecated]] f
+#elif defined(__GNUC__) || defined(__clang__)
+#  define ORTHANC_DEPRECATED(f) f __attribute__((deprecated))
+#elif defined(_MSC_VER)
+#  define ORTHANC_DEPRECATED(f) __declspec(deprecated) f
+#else
+#  define ORTHANC_DEPRECATED
+#endif
+
+
+// Macros "ORTHANC_OVERRIDE" and "ORTHANC_FINAL" wrap the "override"
+// and "final" keywords introduced in C++11, to do compile-time
+// checking of virtual methods
+// The __cplusplus macro is broken in Visual Studio up to 15.6 and, in
+// later versions, require the usage of the /Zc:__cplusplus flag
+// We thus use an alternate way of checking for 'override' support
+#ifdef ORTHANC_OVERRIDE_SUPPORTED
+#  error ORTHANC_OVERRIDE_SUPPORTED cannot be defined at this point
+#endif 
+
+#if __cplusplus >= 201103L   // C++11
+#  define ORTHANC_OVERRIDE_SUPPORTED 1
+#else
+#  ifdef _MSC_VER
+#    if _MSC_VER >= 1600  // Visual Studio 2010 (10.0)
+#      define ORTHANC_OVERRIDE_SUPPORTED 1
+#    endif
+#  endif
+#endif
+
+
+#if ORTHANC_OVERRIDE_SUPPORTED
+// The override keyword (C++11) is enabled
+#  define ORTHANC_OVERRIDE  override 
+#  define ORTHANC_FINAL     final
+#else
+// The override keyword (C++11) is not available
+#  define ORTHANC_OVERRIDE
+#  define ORTHANC_FINAL
+#endif
+
+
+
+//#define Orthanc_Compatibility_h_STR2(x) #x
+//#define Orthanc_Compatibility_h_STR1(x) Orthanc_Compatibility_h_STR2(x)
+
+//#pragma message("__cplusplus = " Orthanc_Compatibility_h_STR1(__cplusplus))
+
+#if (defined _MSC_VER)
+//#  pragma message("_MSC_VER = " Orthanc_Compatibility_h_STR1(_MSC_VER))
+//#  pragma message("_MSVC_LANG = " Orthanc_Compatibility_h_STR1(_MSVC_LANG))
+// The __cplusplus macro cannot be used in Visual C++ < 1914 (VC++ 15.7)
+// However, even in recent versions, __cplusplus will only be correct (that is,
+// correctly defines the supported C++ version) if a special flag is passed to
+// the compiler ("/Zc:__cplusplus")
+// To make this header more robust, we use the _MSVC_LANG equivalent macro.
+
+// please note that not all C++11 features are supported when _MSC_VER == 1600
+// (or higher). This header file can be made for fine-grained, if required, 
+// based on specific _MSC_VER values
+
+#  if _MSC_VER >= 1600  // Visual Studio 2010 (10.0)
+#    define ORTHANC_Cxx03_DETECTED 0
+#  else
+#    define ORTHANC_Cxx03_DETECTED 1
+#  endif
+
+#else
+// of _MSC_VER is not defined, we assume __cplusplus is correctly defined
+// if __cplusplus is not defined (very old compilers??), then the following
+// test will compare 0 < 201103L and will be true --> safe.
+#  if __cplusplus < 201103L  // C++11
+#    define ORTHANC_Cxx03_DETECTED 1
+#  else
+#    define ORTHANC_Cxx03_DETECTED 0
+#  endif
+#endif
+
+#if ORTHANC_Cxx03_DETECTED == 1
+//#pragma message("C++ 11 support is not present.")
+
+/**
+ * "std::unique_ptr" was introduced in C++11, and "std::auto_ptr" was
+ * removed in C++17. We emulate "std::auto_ptr" using boost: "The
+ * smart pointer unique_ptr [is] a drop-in replacement for
+ * std::unique_ptr, usable also from C++03 compilers." This is only
+ * available if Boost >= 1.57.0 (from November 2014).
+ * https://www.boost.org/doc/libs/1_57_0/doc/html/move/reference.html#header.boost.move.unique_ptr_hpp
+ **/
+
+#include 
+
+namespace std
+{
+  template 
+  class unique_ptr : public boost::movelib::unique_ptr
+  {
+  public:
+    explicit unique_ptr() :
+      boost::movelib::unique_ptr()
+    {
+    }      
+
+    explicit unique_ptr(T* p) :
+      boost::movelib::unique_ptr(p)
+    {
+    }      
+  };
+}
+#else
+//# pragma message("C++ 11 support is present.")
+# include 
+#endif
diff --git a/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp
new file mode 100644
index 0000000..be7a0ae
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp
@@ -0,0 +1,88 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DeflateBaseCompressor.h"
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+#include 
+
+namespace Orthanc
+{
+  DeflateBaseCompressor::DeflateBaseCompressor() :
+    compressionLevel_(6),
+    prefixWithUncompressedSize_(false)
+  {
+  }
+
+
+  void DeflateBaseCompressor::SetCompressionLevel(uint8_t level)
+  {
+    if (level >= 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Zlib compression level must be between 0 (no compression) and 9 (highest compression)");
+    }
+
+    compressionLevel_ = level;
+  }
+
+
+  uint64_t DeflateBaseCompressor::ReadUncompressedSizePrefix(const void* compressed,
+                                                             size_t compressedSize)
+  {
+    if (compressedSize == 0)
+    {
+      return 0;
+    }
+
+    if (compressedSize < sizeof(uint64_t))
+    {
+      throw OrthancException(ErrorCode_CorruptedFile, "The compressed buffer is ill-formed");
+    }
+
+    uint64_t size;
+    memcpy(&size, compressed, sizeof(uint64_t));
+
+    return size;
+  }
+
+
+  void DeflateBaseCompressor::SetPrefixWithUncompressedSize(bool prefix)
+  {
+    prefixWithUncompressedSize_ = prefix;
+  }
+
+  bool DeflateBaseCompressor::HasPrefixWithUncompressedSize() const
+  {
+    return prefixWithUncompressedSize_;
+  }
+
+  uint8_t DeflateBaseCompressor::GetCompressionLevel() const
+  {
+    return compressionLevel_;
+  }
+}
diff --git a/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h
new file mode 100644
index 0000000..cd17188
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h
@@ -0,0 +1,63 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IBufferCompressor.h"
+
+#if !defined(ORTHANC_ENABLE_ZLIB)
+#  error The macro ORTHANC_ENABLE_ZLIB must be defined
+#endif
+
+#if ORTHANC_ENABLE_ZLIB != 1
+#  error ZLIB support must be enabled to include this file
+#endif
+
+
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DeflateBaseCompressor : public IBufferCompressor
+  {
+  private:
+    uint8_t compressionLevel_;
+    bool    prefixWithUncompressedSize_;
+
+  protected:
+    uint64_t ReadUncompressedSizePrefix(const void* compressed,
+                                        size_t compressedSize);
+
+  public:
+    DeflateBaseCompressor();
+
+    void SetCompressionLevel(uint8_t level);
+    
+    void SetPrefixWithUncompressedSize(bool prefix);
+
+    bool HasPrefixWithUncompressedSize() const;
+
+    uint8_t GetCompressionLevel() const;
+  };
+}
diff --git a/OrthancFramework/Sources/Compression/GzipCompressor.cpp b/OrthancFramework/Sources/Compression/GzipCompressor.cpp
new file mode 100644
index 0000000..7c05a81
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.cpp
@@ -0,0 +1,276 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "GzipCompressor.h"
+
+#include 
+#include 
+#include 
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+namespace Orthanc
+{
+  uint64_t GzipCompressor::GuessUncompressedSize(const void* compressed,
+                                                 size_t compressedSize)
+  {
+    /**
+     * "Is there a way to find out the size of the original file which
+     * is inside a GZIP file? [...] There is no truly reliable way,
+     * other than gunzipping the stream. You do not need to save the
+     * result of the decompression, so you can determine the size by
+     * simply reading and decoding the entire file without taking up
+     * space with the decompressed result.
+     *
+     * There is an unreliable way to determine the uncompressed size,
+     * which is to look at the last four bytes of the gzip file, which
+     * is the uncompressed length of that entry modulo 232 in little
+     * endian order.
+     * 
+     * It is unreliable because a) the uncompressed data may be longer
+     * than 2^32 bytes, and b) the gzip file may consist of multiple
+     * gzip streams, in which case you would find the length of only
+     * the last of those streams.
+     * 
+     * If you are in control of the source of the gzip files, you know
+     * that they consist of single gzip streams, and you know that
+     * they are less than 2^32 bytes uncompressed, then and only then
+     * can you use those last four bytes with confidence."
+     *
+     * http://stackoverflow.com/a/9727599/881731
+     **/
+
+    if (compressedSize < 4)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    const uint8_t* p = reinterpret_cast(compressed) + compressedSize - 4;
+
+    return ((static_cast(p[0]) << 0) +
+            (static_cast(p[1]) << 8) +
+            (static_cast(p[2]) << 16) +
+            (static_cast(p[3]) << 24));            
+  }
+
+
+  GzipCompressor::GzipCompressor()
+  {
+    SetPrefixWithUncompressedSize(false);
+  }
+
+
+  void GzipCompressor::Compress(std::string& compressed,
+                                const void* uncompressed,
+                                size_t uncompressedSize)
+  {
+    uLongf compressedSize = compressBound(static_cast(uncompressedSize))
+      + 1024 /* security margin */;
+    
+    if (compressedSize == 0)
+    {
+      compressedSize = 1;
+    }
+
+    uint8_t* target;
+    if (HasPrefixWithUncompressedSize())
+    {
+      compressed.resize(compressedSize + sizeof(uint64_t));
+      target = reinterpret_cast(&compressed[0]) + sizeof(uint64_t);
+    }
+    else
+    {
+      compressed.resize(compressedSize);
+      target = reinterpret_cast(&compressed[0]);
+    }
+
+    z_stream stream;
+    memset(&stream, 0, sizeof(stream));
+
+    stream.next_in = const_cast(reinterpret_cast(uncompressed));
+    stream.next_out = reinterpret_cast(target);
+
+    stream.avail_in = static_cast(uncompressedSize);
+    stream.avail_out = static_cast(compressedSize);
+
+    // Ensure no overflow (if the buffer is too large for the current archicture)
+    if (static_cast(stream.avail_in) != uncompressedSize ||
+        static_cast(stream.avail_out) != compressedSize)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+    
+    // Initialize the compression engine
+    int error = deflateInit2(&stream, 
+                             GetCompressionLevel(), 
+                             Z_DEFLATED,
+                             MAX_WBITS + 16,      // ask for gzip output
+                             8,                   // default memory level
+                             Z_DEFAULT_STRATEGY);
+
+    if (error != Z_OK)
+    {
+      // Cannot initialize zlib
+      compressed.clear();
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Compress the input buffer
+    error = deflate(&stream, Z_FINISH);
+
+    if (error != Z_STREAM_END)
+    {
+      deflateEnd(&stream);
+      compressed.clear();
+
+      switch (error)
+      {
+      case Z_MEM_ERROR:
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+
+    size_t size = stream.total_out;
+
+    if (deflateEnd(&stream) != Z_OK)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // The compression was successful
+    if (HasPrefixWithUncompressedSize())
+    {
+      uint64_t s = static_cast(uncompressedSize);
+      memcpy(&compressed[0], &s, sizeof(uint64_t));
+      compressed.resize(size + sizeof(uint64_t));
+    }
+    else
+    {
+      compressed.resize(size);
+    }
+  }
+
+
+  void GzipCompressor::Uncompress(std::string& uncompressed,
+                                  const void* compressed,
+                                  size_t compressedSize)
+  {
+    uint64_t uncompressedSize;
+    const uint8_t* source = reinterpret_cast(compressed);
+
+    if (HasPrefixWithUncompressedSize())
+    {
+      uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
+      source += sizeof(uint64_t);
+      compressedSize -= sizeof(uint64_t);
+    }
+    else
+    {
+      uncompressedSize = GuessUncompressedSize(compressed, compressedSize);
+    }
+
+    try
+    {
+      uncompressed.resize(static_cast(uncompressedSize));
+    }
+    catch (...)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    z_stream stream;
+    memset(&stream, 0, sizeof(stream));
+
+    char dummy = '\0';  // zlib does not like NULL output buffers (even if the uncompressed data is empty)
+    stream.next_in = const_cast(source);
+    stream.next_out = reinterpret_cast(uncompressedSize == 0 ? &dummy : &uncompressed[0]);
+
+    stream.avail_in = static_cast(compressedSize);
+    stream.avail_out = static_cast(uncompressedSize);
+
+    // Ensure no overflow (if the buffer is too large for the current archicture)
+    if (static_cast(stream.avail_in) != compressedSize ||
+        static_cast(stream.avail_out) != uncompressedSize)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    // Initialize the compression engine
+    int error = inflateInit2(&stream, 
+                             MAX_WBITS + 16);  // this is a gzip input
+
+    if (error != Z_OK)
+    {
+      // Cannot initialize zlib
+      uncompressed.clear();
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Uncompress the input buffer
+    error = inflate(&stream, Z_FINISH);
+
+    if (error != Z_STREAM_END)
+    {
+      inflateEnd(&stream);
+      uncompressed.clear();
+
+      switch (error)
+      {
+        case Z_MEM_ERROR:
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+          
+        case Z_BUF_ERROR:
+        case Z_NEED_DICT:
+          throw OrthancException(ErrorCode_BadFileFormat);
+          
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    size_t size = stream.total_out;
+
+    if (inflateEnd(&stream) != Z_OK)
+    {
+      uncompressed.clear();
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (size != uncompressedSize)
+    {
+      uncompressed.clear();
+
+      // The uncompressed size was not that properly guess, presumably
+      // because of a file size over 4GB. Should fallback to
+      // stream-based decompression.
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The uncompressed size of a gzip-encoded buffer was not properly guessed");
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/Compression/GzipCompressor.h b/OrthancFramework/Sources/Compression/GzipCompressor.h
new file mode 100644
index 0000000..1d2d41b
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.h
@@ -0,0 +1,49 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DeflateBaseCompressor.h"
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC GzipCompressor : public DeflateBaseCompressor
+  {
+  private:
+    uint64_t GuessUncompressedSize(const void* compressed,
+                                   size_t compressedSize);
+
+  public:
+    GzipCompressor();
+
+    virtual void Compress(std::string& compressed,
+                          const void* uncompressed,
+                          size_t uncompressedSize) ORTHANC_OVERRIDE;
+
+    virtual void Uncompress(std::string& uncompressed,
+                            const void* compressed,
+                            size_t compressedSize) ORTHANC_OVERRIDE;
+  };
+}
diff --git a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp
new file mode 100644
index 0000000..15c71ec
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp
@@ -0,0 +1,253 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "HierarchicalZipWriter.h"
+
+#include "../Toolbox.h"
+#include "../OrthancException.h"
+
+#include 
+
+namespace Orthanc
+{
+  std::string HierarchicalZipWriter::Index::KeepAlphanumeric(const std::string& source)
+  {
+    std::string result;
+
+    bool lastSpace = false;
+
+    result.reserve(source.size());
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      char c = source[i];
+      if (c == '^')
+        c = ' ';
+
+      if (c <= 127 && 
+          c >= 0)
+      {
+        if (isspace(c)) 
+        {
+          if (!lastSpace)
+          {
+            lastSpace = true;
+            result.push_back(' ');
+          }
+        }
+        else if (isalnum(c) || 
+                 c == '.' || 
+                 c == '_')
+        {
+          result.push_back(c);
+          lastSpace = false;
+        }
+      }
+    }
+
+    return Toolbox::StripSpaces(result);
+  }
+
+  std::string HierarchicalZipWriter::Index::GetCurrentDirectoryPath() const
+  {
+    std::string result;
+
+    Stack::const_iterator it = stack_.begin();
+    ++it;  // Skip the root node (to avoid absolute paths)
+
+    while (it != stack_.end())
+    {
+      result += (*it)->name_ + "/";
+      ++it;
+    }
+
+    return result;
+  }
+
+  std::string HierarchicalZipWriter::Index::EnsureUniqueFilename(const char* filename)
+  {
+    std::string standardized = KeepAlphanumeric(filename);
+
+    Directory& d = *stack_.back();
+    Directory::Content::iterator it = d.content_.find(standardized);
+
+    if (it == d.content_.end())
+    {
+      d.content_[standardized] = 1;
+      return standardized;
+    }
+    else
+    {
+      it->second++;
+      return standardized + "-" + boost::lexical_cast(it->second);
+    }    
+  }
+
+  HierarchicalZipWriter::Index::Index()
+  {
+    stack_.push_back(new Directory);
+  }
+
+  HierarchicalZipWriter::Index::~Index()
+  {
+    for (Stack::iterator it = stack_.begin(); it != stack_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+  bool HierarchicalZipWriter::Index::IsRoot() const
+  {
+    return stack_.size() == 1;
+  }
+
+  std::string HierarchicalZipWriter::Index::OpenFile(const char* name)
+  {
+    return GetCurrentDirectoryPath() + EnsureUniqueFilename(name);
+  }
+
+  void HierarchicalZipWriter::Index::OpenDirectory(const char* name)
+  {
+    std::string d = EnsureUniqueFilename(name);
+
+    // Push the new directory onto the stack
+    stack_.push_back(new Directory);
+    stack_.back()->name_ = d;
+  }
+
+  void HierarchicalZipWriter::Index::CloseDirectory()
+  {
+    if (IsRoot())
+    {
+      // Cannot close the root node
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    delete stack_.back();
+    stack_.pop_back();
+  }
+
+
+  HierarchicalZipWriter::HierarchicalZipWriter(const char* path)
+  {
+    writer_.SetOutputPath(path);
+    writer_.Open();
+  }
+
+  
+  HierarchicalZipWriter::HierarchicalZipWriter(ZipWriter::IOutputStream* stream,
+                                               bool isZip64)
+  {
+    writer_.AcquireOutputStream(stream, isZip64);
+    writer_.Open();    
+  }
+
+
+  HierarchicalZipWriter::~HierarchicalZipWriter()
+  {
+    writer_.Close();
+  }
+
+  void HierarchicalZipWriter::SetZip64(bool isZip64)
+  {
+    writer_.SetZip64(isZip64);
+  }
+
+  bool HierarchicalZipWriter::IsZip64() const
+  {
+    return writer_.IsZip64();
+  }
+
+  void HierarchicalZipWriter::SetCompressionLevel(uint8_t level)
+  {
+    writer_.SetCompressionLevel(level);
+  }
+
+  uint8_t HierarchicalZipWriter::GetCompressionLevel() const
+  {
+    return writer_.GetCompressionLevel();
+  }
+
+  void HierarchicalZipWriter::SetAppendToExisting(bool append)
+  {
+    writer_.SetAppendToExisting(append);
+  }
+
+  bool HierarchicalZipWriter::IsAppendToExisting() const
+  {
+    return writer_.IsAppendToExisting();
+  }
+
+  void HierarchicalZipWriter::OpenFile(const char* name)
+  {
+    std::string p = indexer_.OpenFile(name);
+    writer_.OpenFile(p.c_str());
+  }
+
+  void HierarchicalZipWriter::OpenDirectory(const char* name)
+  {
+    indexer_.OpenDirectory(name);
+  }
+
+  void HierarchicalZipWriter::CloseDirectory()
+  {
+    indexer_.CloseDirectory();
+  }
+
+  std::string HierarchicalZipWriter::GetCurrentDirectoryPath() const
+  {
+    return indexer_.GetCurrentDirectoryPath();
+  }
+
+  void HierarchicalZipWriter::Write(const void *data, size_t length)
+  {
+    writer_.Write(data, length);
+  }
+
+  void HierarchicalZipWriter::Write(const std::string &data)
+  {
+    writer_.Write(data);
+  }
+
+  HierarchicalZipWriter* HierarchicalZipWriter::CreateToMemory(std::string& target,
+                                                               bool isZip64)
+  {
+    return new HierarchicalZipWriter(new ZipWriter::MemoryStream(target), isZip64);
+  }
+
+  void HierarchicalZipWriter::CancelStream()
+  {
+    writer_.CancelStream();
+  }
+
+  void HierarchicalZipWriter::Close()
+  {
+    writer_.Close();
+  }
+
+  uint64_t HierarchicalZipWriter::GetArchiveSize() const
+  {
+    return writer_.GetArchiveSize();
+  }
+}
diff --git a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h
new file mode 100644
index 0000000..2b9ab5f
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h
@@ -0,0 +1,127 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "ZipWriter.h"
+
+#include 
+#include 
+#include 
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include 
+#endif
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC HierarchicalZipWriter : public boost::noncopyable
+  {
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+    FRIEND_TEST(HierarchicalZipWriter, Index);
+    FRIEND_TEST(HierarchicalZipWriter, Filenames);
+#endif
+
+  private:
+    class ORTHANC_PUBLIC Index
+    {
+    private:
+      struct Directory
+      {
+        typedef std::map  Content;
+
+        std::string name_;
+        Content  content_;
+      };
+
+      typedef std::list Stack;
+  
+      Stack stack_;
+
+      std::string EnsureUniqueFilename(const char* filename);
+
+    public:
+      Index();
+
+      ~Index();
+
+      bool IsRoot() const;
+
+      std::string OpenFile(const char* name);
+
+      void OpenDirectory(const char* name);
+
+      void CloseDirectory();
+
+      std::string GetCurrentDirectoryPath() const;
+
+      static std::string KeepAlphanumeric(const std::string& source);
+    };
+
+    Index indexer_;
+    ZipWriter writer_;
+
+  public:
+    explicit HierarchicalZipWriter(const char* path);
+
+    HierarchicalZipWriter(ZipWriter::IOutputStream* stream,  // transfers ownership
+                          bool isZip64);
+
+    ~HierarchicalZipWriter();
+
+    void SetZip64(bool isZip64);
+
+    bool IsZip64() const;
+
+    void SetCompressionLevel(uint8_t level);
+
+    uint8_t GetCompressionLevel() const;
+
+    void SetAppendToExisting(bool append);
+    
+    bool IsAppendToExisting() const;
+    
+    void OpenFile(const char* name);
+
+    void OpenDirectory(const char* name);
+
+    void CloseDirectory();
+
+    std::string GetCurrentDirectoryPath() const;
+
+    void Write(const void* data, size_t length);
+
+    void Write(const std::string& data);
+
+    // The lifetime of the "target" buffer must be larger than that of HierarchicalZipWriter
+    static HierarchicalZipWriter* CreateToMemory(std::string& target,
+                                                 bool isZip64);
+
+    void CancelStream();
+
+    void Close();
+
+    uint64_t GetArchiveSize() const;
+  };
+}
diff --git a/OrthancFramework/Sources/Compression/IBufferCompressor.cpp b/OrthancFramework/Sources/Compression/IBufferCompressor.cpp
new file mode 100644
index 0000000..c2f5552
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/IBufferCompressor.cpp
@@ -0,0 +1,48 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "IBufferCompressor.h"
+
+
+namespace Orthanc
+{
+  void IBufferCompressor::Compress(std::string& compressed,
+                                   IBufferCompressor& compressor,
+                                   const std::string& uncompressed)
+  {
+    compressor.Compress(compressed, 
+                        uncompressed.size() == 0 ? NULL : uncompressed.c_str(), 
+                        uncompressed.size());
+  }
+
+  void IBufferCompressor::Uncompress(std::string& uncompressed,
+                                     IBufferCompressor& compressor,
+                                     const std::string& compressed)
+  {
+    compressor.Uncompress(uncompressed, 
+                          compressed.size() == 0 ? NULL : compressed.c_str(), 
+                          compressed.size());
+  }
+}
diff --git a/OrthancFramework/Sources/Compression/IBufferCompressor.h b/OrthancFramework/Sources/Compression/IBufferCompressor.h
new file mode 100644
index 0000000..cb5b0fc
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/IBufferCompressor.h
@@ -0,0 +1,57 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC IBufferCompressor : public boost::noncopyable
+  {
+  public:
+    virtual ~IBufferCompressor()
+    {
+    }
+
+    virtual void Compress(std::string& compressed,
+                          const void* uncompressed,
+                          size_t uncompressedSize) = 0;
+
+    virtual void Uncompress(std::string& uncompressed,
+                            const void* compressed,
+                            size_t compressedSize) = 0;
+
+    static void Compress(std::string& compressed,
+                         IBufferCompressor& compressor,
+                         const std::string& uncompressed);
+
+    static void Uncompress(std::string& uncompressed,
+                           IBufferCompressor& compressor,
+                           const std::string& compressed);
+  };
+}
diff --git a/OrthancFramework/Sources/Compression/ZipReader.cpp b/OrthancFramework/Sources/Compression/ZipReader.cpp
new file mode 100644
index 0000000..f1fb5e0
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/ZipReader.cpp
@@ -0,0 +1,435 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "ZipReader.h"
+
+#include "../OrthancException.h"
+#include "../../Resources/ThirdParty/minizip/unzip.h"
+
+#if ORTHANC_SANDBOXED != 1
+#  include "../SystemToolbox.h"
+#endif
+
+
+/**
+ * I have not been able to correctly define "ssize_t" on all versions
+ * of Visual Studio. As a consequence, I preferred to switch "ssize_t"
+ * to "SSIZE_T", that is properly defined on both MSVC 2008 and 2015.
+ * I define the macro "SSIZE_T" as an alias to "ssize_t" on
+ * POSIX-compliant platforms that wouldn't have "SSIZE_T" defined.
+ **/
+#if defined(_MSC_VER)
+#  include    // Definition of SSIZE_T
+#else
+#  if !defined(SSIZE_T)
+typedef ssize_t SSIZE_T;
+#  endif
+#endif
+
+#include 
+
+
+namespace Orthanc
+{
+  // ZPOS64_T corresponds to "uint64_t"
+  
+  class ZipReader::MemoryBuffer : public boost::noncopyable
+  {
+  private:
+    const uint8_t*  content_;
+    size_t          size_;
+    size_t          pos_;
+
+  public:
+    MemoryBuffer(const void* p,
+                 size_t size) :
+      content_(reinterpret_cast(p)),
+      size_(size),
+      pos_(0)
+    {
+    }
+  
+    explicit MemoryBuffer(const std::string& s) :
+      content_(s.empty() ? NULL : reinterpret_cast(s.c_str())),
+      size_(s.size()),
+      pos_(0)
+    {
+    }
+
+    // Returns the number of bytes actually read
+    uLong Read(void *target,
+               uLong size)
+    {
+      if (size <= 0)
+      {
+        return 0;
+      }
+      else
+      {
+        size_t s = static_cast(size);
+        if (s + pos_ > size_)
+        {
+          s = size_ - pos_;
+        }
+
+        if (s != 0)
+        {
+          memcpy(target, content_ + pos_, s);
+        }
+      
+        pos_ += s;
+        return static_cast(s);
+      }             
+    }
+
+    ZPOS64_T Tell() const
+    {
+      return static_cast(pos_);
+    }
+
+    long Seek(ZPOS64_T offset,
+              int origin)
+    {
+      SSIZE_T next;
+    
+      switch (origin)
+      {
+        case ZLIB_FILEFUNC_SEEK_CUR:
+          next = static_cast(offset) + static_cast(pos_);
+          break;
+
+        case ZLIB_FILEFUNC_SEEK_SET:
+          next = static_cast(offset);
+          break;
+
+        case ZLIB_FILEFUNC_SEEK_END:
+          next = static_cast(offset) + static_cast(size_);
+          break;
+
+        default:  // Should never occur
+          return 1;  // Error
+      }
+
+      if (next < 0)
+      {
+        pos_ = 0;
+      }
+      else if (next >= static_cast(size_))
+      {
+        pos_ = size_;
+      }
+      else
+      {
+        pos_ = static_cast(next);
+      }
+
+      return 0;
+    }
+
+
+    static voidpf OpenWrapper(voidpf opaque,
+                              const void* filename,
+                              int mode)
+    {
+      // Don't return NULL to make "unzip.c" happy
+      return opaque;
+    }
+
+    static uLong ReadWrapper(voidpf opaque,
+                             voidpf stream,
+                             void* buf,
+                             uLong size)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->Read(buf, size);
+    }
+
+    static ZPOS64_T TellWrapper(voidpf opaque,
+                                voidpf stream)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->Tell();
+    }
+
+    static long SeekWrapper(voidpf opaque,
+                            voidpf stream,
+                            ZPOS64_T offset,
+                            int origin)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->Seek(offset, origin);
+    }
+
+    static int CloseWrapper(voidpf opaque,
+                            voidpf stream)
+    {
+      return 0;
+    }
+
+    static int TestErrorWrapper(voidpf opaque,
+                                voidpf stream)
+    {
+      return 0;  // ??
+    }
+  };
+
+
+
+  ZipReader* ZipReader::CreateFromMemory(const std::string& buffer)
+  {
+    if (buffer.empty())
+    {
+      return CreateFromMemory(NULL, 0);
+    }
+    else
+    {
+      return CreateFromMemory(buffer.c_str(), buffer.size());
+    }
+  }
+
+
+  bool ZipReader::IsZipMemoryBuffer(const void* buffer,
+                                    size_t size)
+  {
+    if (size < 4)
+    {
+      return false;
+    }
+    else
+    {
+      const uint8_t* c = reinterpret_cast(buffer);
+      return (c[0] == 0x50 &&  // 'P'
+              c[1] == 0x4b &&  // 'K'
+              ((c[2] == 0x03 && c[3] == 0x04) ||
+               (c[2] == 0x05 && c[3] == 0x06) ||
+               (c[2] == 0x07 && c[3] == 0x08)));
+    }
+  }
+  
+
+  bool ZipReader::IsZipMemoryBuffer(const std::string& content)
+  {
+    if (content.empty())
+    {
+      return false;
+    }
+    else
+    {
+      return IsZipMemoryBuffer(content.c_str(), content.size());
+    }
+  }
+
+
+#if ORTHANC_SANDBOXED != 1
+  bool ZipReader::IsZipFile(const std::string& path)
+  {
+    std::string content;
+    SystemToolbox::ReadFileRange(content, path, 0, 4,
+                                 false /* don't throw if file is too small */);
+
+    return IsZipMemoryBuffer(content);
+  }
+#endif
+
+
+  struct ZipReader::PImpl
+  {
+    unzFile                       unzip_;
+    std::unique_ptr reader_;
+    bool                          done_;
+
+    PImpl() :
+      unzip_(NULL),
+      done_(true)
+    {
+    }
+  };
+
+
+  ZipReader::ZipReader() :
+    pimpl_(new PImpl)
+  {
+  }
+
+  
+  ZipReader::~ZipReader()
+  {
+    if (pimpl_->unzip_ != NULL)
+    {
+      unzClose(pimpl_->unzip_);
+      pimpl_->unzip_ = NULL;
+    }
+  }
+
+  
+  uint64_t ZipReader::GetFilesCount() const
+  {
+    assert(pimpl_->unzip_ != NULL);
+    
+    unz_global_info64_s info;
+    
+    if (unzGetGlobalInfo64(pimpl_->unzip_, &info) == 0)
+    {
+      return info.number_entry;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+  
+  void ZipReader::SeekFirst()
+  {
+    assert(pimpl_->unzip_ != NULL);    
+    pimpl_->done_ = (unzGoToFirstFile(pimpl_->unzip_) != 0);
+  }
+
+
+  bool ZipReader::ReadNextFile(std::string& filename,
+                               std::string& content)
+  {
+    assert(pimpl_->unzip_ != NULL);
+
+    if (pimpl_->done_)
+    {
+      return false;
+    }
+    else
+    {
+      unz_file_info64_s info;
+      if (unzGetCurrentFileInfo64(pimpl_->unzip_, &info, NULL, 0, NULL, 0, NULL, 0) != 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      filename.resize(info.size_filename);
+      if (!filename.empty() &&
+          unzGetCurrentFileInfo64(pimpl_->unzip_, &info, &filename[0],
+                                  static_cast(filename.size()), NULL, 0, NULL, 0) != 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      content.resize(info.uncompressed_size);
+
+      if (!content.empty())
+      {
+        if (unzOpenCurrentFile(pimpl_->unzip_) == 0)
+        {
+          bool success = (unzReadCurrentFile(pimpl_->unzip_, &content[0],
+                                             static_cast(content.size())) != 0);
+                          
+          if (unzCloseCurrentFile(pimpl_->unzip_) != 0 ||
+              !success)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "Invalid file or unsupported compression method (e.g. Deflate64)");
+        }
+      }
+      
+      pimpl_->done_ = (unzGoToNextFile(pimpl_->unzip_) != 0);
+ 
+      return true;
+    }
+  }    
+
+  
+  ZipReader* ZipReader::CreateFromMemory(const void* buffer,
+                                         size_t size)
+  {
+    if (!IsZipMemoryBuffer(buffer, size))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "The memory buffer doesn't contain a ZIP archive");
+    }
+    else
+    {
+      std::unique_ptr reader(new ZipReader);
+
+      reader->pimpl_->reader_.reset(new MemoryBuffer(buffer, size));
+      if (reader->pimpl_->reader_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    
+      zlib_filefunc64_def funcs;
+      memset(&funcs, 0, sizeof(funcs));
+
+      funcs.opaque = reader->pimpl_->reader_.get();
+      funcs.zopen64_file = MemoryBuffer::OpenWrapper;
+      funcs.zread_file = MemoryBuffer::ReadWrapper;
+      funcs.ztell64_file = MemoryBuffer::TellWrapper;
+      funcs.zseek64_file = MemoryBuffer::SeekWrapper;
+      funcs.zclose_file = MemoryBuffer::CloseWrapper;
+      funcs.zerror_file = MemoryBuffer::TestErrorWrapper;
+
+      reader->pimpl_->unzip_ = unzOpen2_64(NULL, &funcs);
+      if (reader->pimpl_->unzip_ == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Cannot open ZIP archive from memory buffer");
+      }
+      else
+      {
+        reader->SeekFirst();
+        return reader.release();
+      }
+    }
+  }
+  
+
+#if ORTHANC_SANDBOXED != 1
+  ZipReader* ZipReader::CreateFromFile(const std::string& path)
+  {
+    if (!IsZipFile(path))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "The file doesn't contain a ZIP archive: " + path);
+    }
+    else
+    {
+      std::unique_ptr reader(new ZipReader);
+
+      reader->pimpl_->unzip_ = unzOpen64(path.c_str());
+      if (reader->pimpl_->unzip_ == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Cannot open ZIP archive from file: " + path);
+      }
+      else
+      {
+        reader->SeekFirst();
+        return reader.release();
+      }
+    }
+  }
+#endif
+}
diff --git a/OrthancFramework/Sources/Compression/ZipReader.h b/OrthancFramework/Sources/Compression/ZipReader.h
new file mode 100644
index 0000000..db3c18d
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/ZipReader.h
@@ -0,0 +1,88 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_ZLIB)
+#  error The macro ORTHANC_ENABLE_ZLIB must be defined
+#endif
+
+#if ORTHANC_ENABLE_ZLIB != 1
+#  error ZLIB support must be enabled to include this file
+#endif
+
+
+#include 
+#include 
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ZipReader : public boost::noncopyable
+  {
+  private:
+    class MemoryBuffer;
+    
+    struct PImpl;
+    boost::shared_ptr   pimpl_;
+
+    ZipReader();
+
+    void SeekFirst();
+
+  public:
+    ~ZipReader();
+
+    uint64_t GetFilesCount() const;
+
+    bool ReadNextFile(std::string& filename,
+                      std::string& content);
+    
+    static ZipReader* CreateFromMemory(const void* buffer,
+                                       size_t size);
+
+    static ZipReader* CreateFromMemory(const std::string& buffer);
+
+#if ORTHANC_SANDBOXED != 1
+    static ZipReader* CreateFromFile(const std::string& path);    
+#endif
+
+    static bool IsZipMemoryBuffer(const void* buffer,
+                                  size_t size);
+
+    static bool IsZipMemoryBuffer(const std::string& content);
+
+#if ORTHANC_SANDBOXED != 1
+    static bool IsZipFile(const std::string& path);
+#endif
+  };
+}
diff --git a/OrthancFramework/Sources/Compression/ZipWriter.cpp b/OrthancFramework/Sources/Compression/ZipWriter.cpp
new file mode 100644
index 0000000..973cc8e
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/ZipWriter.cpp
@@ -0,0 +1,756 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "ZipWriter.h"
+
+#include 
+#include 
+#include 
+
+#include "../../Resources/ThirdParty/minizip/zip.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SystemToolbox.h"
+
+
+static void PrepareFileInfo(zip_fileinfo& zfi)
+{
+  memset(&zfi, 0, sizeof(zfi));
+
+  using namespace boost::posix_time;
+  ptime now = second_clock::local_time();
+
+  boost::gregorian::date today = now.date();
+  ptime midnight(today);
+
+  time_duration sinceMidnight = now - midnight;
+  zfi.tmz_date.tm_sec = static_cast(sinceMidnight.seconds());  // seconds after the minute - [0,59]
+  zfi.tmz_date.tm_min = static_cast(sinceMidnight.minutes());  // minutes after the hour - [0,59]
+  zfi.tmz_date.tm_hour = static_cast(sinceMidnight.hours());  // hours since midnight - [0,23]
+
+  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_day.html
+  zfi.tmz_date.tm_mday = today.day();  // day of the month - [1,31]
+
+  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_month.html
+  zfi.tmz_date.tm_mon = today.month() - 1;  // months since January - [0,11]
+
+  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_year.html
+  zfi.tmz_date.tm_year = today.year();  // years - [1980..2044]
+}
+
+
+
+namespace Orthanc
+{
+  ZipWriter::MemoryStream::MemoryStream(std::string& target) :
+    target_(target),
+    archiveSize_(0)
+  {
+  }
+
+  
+  void ZipWriter::MemoryStream::Write(const std::string& chunk)
+  {
+    chunked_.AddChunk(chunk);
+    archiveSize_ += chunk.size();
+  }
+  
+  
+  uint64_t ZipWriter::MemoryStream::GetArchiveSize() const
+  {
+    return archiveSize_;
+  }
+
+
+  void ZipWriter::MemoryStream::Close()
+  {
+    chunked_.Flatten(target_);
+  }
+  
+
+  void ZipWriter::BufferWithSeek::CheckInvariants() const
+  {
+#if !defined(NDEBUG)
+    assert(chunks_.GetNumBytes() == 0 ||
+           flattened_.empty());
+
+    assert(currentPosition_ <= GetSize());
+    
+    if (currentPosition_ < GetSize())
+    {
+      assert(chunks_.GetNumBytes() == 0);
+      assert(!flattened_.empty());
+    }
+#endif
+  }
+  
+
+  ZipWriter::BufferWithSeek::BufferWithSeek() :
+    currentPosition_(0)
+  {
+    CheckInvariants();
+  }
+
+  
+  ZipWriter::BufferWithSeek::~BufferWithSeek()
+  {
+    CheckInvariants();
+  }
+  
+  
+  size_t ZipWriter::BufferWithSeek::GetPosition() const
+  {
+    return currentPosition_;
+  }
+  
+  
+  size_t ZipWriter::BufferWithSeek::GetSize() const
+  {
+    if (flattened_.empty())
+    {
+      return chunks_.GetNumBytes();
+    }
+    else
+    {
+      return flattened_.size();
+    }
+  }
+
+  
+  void ZipWriter::BufferWithSeek::Write(const void* data,
+                                        size_t size)
+  {
+    CheckInvariants();
+
+    if (size != 0)
+    {
+      if (currentPosition_ < GetSize())
+      {
+        if (currentPosition_ + size > flattened_.size())
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          memcpy(&flattened_[currentPosition_], data, size);
+        }
+      }
+      else
+      {
+        if (!flattened_.empty())
+        {
+          assert(chunks_.GetNumBytes() == 0);
+          chunks_.AddChunk(flattened_);
+          flattened_.clear();
+        }
+        
+        chunks_.AddChunk(data, size);
+      }
+
+      currentPosition_ += size;
+    }
+
+    CheckInvariants();
+  }
+
+      
+  void ZipWriter::BufferWithSeek::Write(const std::string& data)
+  {
+    if (!data.empty())
+    {
+      Write(data.c_str(), data.size());
+    }
+  }
+
+      
+  void ZipWriter::BufferWithSeek::Seek(size_t position)
+  {
+    CheckInvariants();
+
+    if (currentPosition_ != position)
+    {
+      if (position < GetSize())
+      {
+        if (chunks_.GetNumBytes() != 0)
+        {
+          assert(flattened_.empty());
+          chunks_.Flatten(flattened_);
+        }
+
+        assert(chunks_.GetNumBytes() == 0);
+      }
+      else if (position > GetSize())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      currentPosition_ = position;
+    }
+
+    CheckInvariants();
+  }
+      
+
+  void ZipWriter::BufferWithSeek::Flush(std::string& target)
+  {
+    CheckInvariants();
+
+    if (flattened_.empty())
+    {
+      chunks_.Flatten(target);
+    }
+    else
+    {
+      flattened_.swap(target);
+      flattened_.clear();
+    }
+
+    currentPosition_ = 0;
+
+    CheckInvariants();
+  }
+
+
+  /**
+   * Inside a ZIP archive, compressed files are concatenated, each
+   * file being prefixed by its "Local file header". The ZIP archive
+   * ends with the "central directory" structure.
+   * https://en.wikipedia.org/wiki/ZIP_(file_format)
+   * 
+   * When writing one file, the minizip implementation first TELLS to
+   * know the current size of the archive, then WRITES the header and
+   * data bytes, then SEEKS backward to update the "local file header"
+   * with info about the compressed data (at the 14 offset, containing
+   * CRC-32, compressed size and uncompressed size), and finally SEEKS
+   * to get back at the end of the stream in order to continue adding
+   * files.
+   * 
+   * The minizip implementation will *never* SEEK *before* the "local
+   * file header" of the current file. However, the current file must
+   * *not* be immediately sent to the stream as new bytes are written,
+   * because the "local file header" will be updated.
+   *
+   * Consequently, this buffer class only sends the pending bytes to
+   * the output stream once it receives a SEEK command that moves the
+   * cursor at the end of the archive. In the minizip implementation,
+   * such a SEEK indicates that the current file has been properly
+   * added to the archive.
+   **/  
+  class ZipWriter::StreamBuffer : public boost::noncopyable
+  {
+  private:
+    IOutputStream&  stream_;
+    bool            success_;
+    ZPOS64_T        startCurrentFile_;
+    BufferWithSeek  buffer_;
+    
+  public:
+    explicit StreamBuffer(IOutputStream& stream) :
+      stream_(stream),
+      success_(true),
+      startCurrentFile_(0)
+    {
+    }
+    
+    int Close()
+    {
+      try
+      {
+        if (success_)
+        {
+          std::string s;
+          buffer_.Flush(s);
+          stream_.Write(s);
+        }
+        
+        return 0;
+      }
+      catch (...)
+      {
+        success_ = false;
+        return 1;
+      }
+    }
+
+    ZPOS64_T Tell() const
+    {
+      return startCurrentFile_ + static_cast(buffer_.GetPosition());
+    }
+
+    uLong Write(const void* buf,
+                uLong size)
+    {
+      if (size == 0)
+      {
+        return 0;
+      }
+      else if (!success_)
+      {
+        return 0;  // Error
+      }
+      else
+      {
+        try
+        {
+          buffer_.Write(buf, size);
+          return size;
+        }
+        catch (...)
+        {
+          return 0;
+        }
+      }
+    }
+    
+
+    long Seek(ZPOS64_T offset,
+              int origin)
+    {
+      try
+      {
+        if (origin == ZLIB_FILEFUNC_SEEK_SET &&
+            offset >= startCurrentFile_ &&
+            success_)
+        {
+          ZPOS64_T fullSize = startCurrentFile_ + static_cast(buffer_.GetSize());
+          assert(offset <= fullSize);
+
+          if (offset == fullSize)
+          {
+            // We can flush to the output stream
+            std::string s;
+            buffer_.Flush(s);
+            stream_.Write(s);
+            startCurrentFile_ = fullSize;
+          }
+          else
+          {          
+            buffer_.Seek(offset - startCurrentFile_);
+          }
+          
+          return 0;  // OK
+        }
+        else
+        {
+          return 1;
+        }
+      }
+      catch (...)
+      {
+        return 1;
+      }
+    }
+
+
+    void Cancel()
+    {
+      success_ = false;
+    }
+    
+
+    static int CloseWrapper(voidpf opaque,
+                            voidpf stream)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->Close();
+    }
+
+    static voidpf OpenWrapper(voidpf opaque,
+                              const void* filename,
+                              int mode)
+    {
+      assert(opaque != NULL);
+      return opaque;
+    }
+
+    static long SeekWrapper(voidpf opaque,
+                            voidpf stream,
+                            ZPOS64_T offset,
+                            int origin)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->Seek(offset, origin);
+    }
+
+    static ZPOS64_T TellWrapper(voidpf opaque,
+                                voidpf stream)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->Tell();
+    }
+
+    static int TestErrorWrapper(voidpf opaque,
+                                voidpf stream)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->success_ ? 0 : 1;
+    }
+
+    static uLong WriteWrapper(voidpf opaque,
+                              voidpf stream,
+                              const void* buf,
+                              uLong size)
+    {
+      assert(opaque != NULL);
+      return reinterpret_cast(opaque)->Write(buf, size);
+    }
+  };
+  
+
+  struct ZipWriter::PImpl : public boost::noncopyable
+  {
+    zipFile file_;
+    std::unique_ptr streamBuffer_;
+    uint64_t  archiveSize_;
+
+    PImpl() :
+      file_(NULL),
+      archiveSize_(0)
+    {
+    }
+  };
+
+  ZipWriter::ZipWriter() :
+    pimpl_(new PImpl),
+    isZip64_(false),
+    hasFileInZip_(false),
+    append_(false),
+    compressionLevel_(6)
+  {
+  }
+
+  ZipWriter::~ZipWriter()
+  {
+    try
+    {
+      Close();
+    }
+    catch (OrthancException& e)  // Don't throw exceptions in destructors
+    {
+      LOG(ERROR) << "Caught exception in destructor: " << e.What();
+    }
+  }
+
+  void ZipWriter::Close()
+  {
+    if (IsOpen())
+    {
+      zipClose(pimpl_->file_, "Created by Orthanc");
+      pimpl_->file_ = NULL;
+      hasFileInZip_ = false;
+
+      pimpl_->streamBuffer_.reset(NULL);
+
+      if (outputStream_.get() != NULL)
+      {
+        outputStream_->Close();
+        pimpl_->archiveSize_ = outputStream_->GetArchiveSize();
+        outputStream_.reset(NULL);
+      }
+    }
+  }
+
+  bool ZipWriter::IsOpen() const
+  {
+    return pimpl_->file_ != NULL;
+  }
+
+  void ZipWriter::Open()
+  {
+    if (IsOpen())
+    {
+      return;
+    }
+    else if (outputStream_.get() != NULL)
+    {
+      // New in Orthanc 1.9.4
+      if (IsAppendToExisting())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot append to output streams");
+      }
+      
+      hasFileInZip_ = false;
+
+      zlib_filefunc64_def funcs;
+      memset(&funcs, 0, sizeof(funcs));
+
+      pimpl_->streamBuffer_.reset(new StreamBuffer(*outputStream_));
+      funcs.opaque = pimpl_->streamBuffer_.get();
+      funcs.zclose_file = StreamBuffer::CloseWrapper;
+      funcs.zerror_file = StreamBuffer::TestErrorWrapper;
+      funcs.zopen64_file = StreamBuffer::OpenWrapper;
+      funcs.ztell64_file = StreamBuffer::TellWrapper;
+      funcs.zwrite_file = StreamBuffer::WriteWrapper;
+      funcs.zseek64_file = StreamBuffer::SeekWrapper;
+
+      /**
+       * "funcs.zread_file" (ZREAD64) also appears in "minizip/zip.c",
+       * but is only needed by function "LoadCentralDirectoryRecord()"
+       * that is only used if appending new files to an already
+       * existing ZIP, which makes no sense for an output stream.
+       **/
+
+      pimpl_->file_ = zipOpen2_64(NULL /* no output path */, APPEND_STATUS_CREATE,
+                                  NULL /* global comment */, &funcs);
+
+      if (!pimpl_->file_)
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile,
+                               "Cannot create new ZIP archive into an output stream");
+      }
+    }
+    else if (path_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "Please call SetOutputPath() before creating the file");
+    }
+    else
+    {
+      hasFileInZip_ = false;
+
+      int mode = APPEND_STATUS_CREATE;
+      if (append_ && 
+          boost::filesystem::exists(path_))
+      {
+        mode = APPEND_STATUS_ADDINZIP;
+      }
+
+      if (isZip64_)
+      {
+        pimpl_->file_ = zipOpen64(path_.c_str(), mode);
+      }
+      else
+      {
+        pimpl_->file_ = zipOpen(path_.c_str(), mode);
+      }
+
+      if (!pimpl_->file_)
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile,
+                               "Cannot create new ZIP archive");  // we do not log the path anymore since it can contain PHI
+      }
+    }
+  }
+
+  void ZipWriter::SetOutputPath(const char* path)
+  {
+    Close();
+    path_ = path;
+  }
+
+  const std::string &ZipWriter::GetOutputPath() const
+  {
+    return path_;
+  }
+
+  void ZipWriter::SetZip64(bool isZip64)
+  {
+    if (outputStream_.get() == NULL)
+    {
+      Close();
+      isZip64_ = isZip64;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "SetZip64() must be given to AcquireOutputStream()");
+    }
+  }
+
+  void ZipWriter::SetCompressionLevel(uint8_t level)
+  {
+    if (level >= 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "ZIP compression level must be between 0 (no compression) "
+                             "and 9 (highest compression)");
+    }
+    else
+    {
+      compressionLevel_ = level;
+    }
+  }
+
+  uint8_t ZipWriter::GetCompressionLevel() const
+  {
+    return compressionLevel_;
+  }
+
+  void ZipWriter::OpenFile(const char* path)
+  {
+    Open();
+
+    zip_fileinfo zfi;
+    PrepareFileInfo(zfi);
+
+    int result;
+
+    if (isZip64_)
+    {
+      result = zipOpenNewFileInZip64(pimpl_->file_, path,
+                                     &zfi,
+                                     NULL,   0,
+                                     NULL,   0,
+                                     "",  // Comment
+                                     Z_DEFLATED,
+                                     compressionLevel_, 1);
+    }
+    else
+    {
+      result = zipOpenNewFileInZip(pimpl_->file_, path,
+                                   &zfi,
+                                   NULL,   0,
+                                   NULL,   0,
+                                   "",  // Comment
+                                   Z_DEFLATED,
+                                   compressionLevel_);
+    }
+
+    if (result != ZIP_OK)
+    {
+      throw OrthancException(ErrorCode_CannotWriteFile,
+                             "Cannot add new file inside ZIP archive - error code = " + boost::lexical_cast(result)); // we do not log the path anymore since it can contain PHI
+    }
+
+    hasFileInZip_ = true;
+  }
+
+
+  void ZipWriter::Write(const std::string& data)
+  {
+    if (data.size())
+    {
+      Write(&data[0], data.size());
+    }
+  }
+
+
+  void ZipWriter::Write(const void* data, size_t length)
+  {
+    if (!hasFileInZip_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Call first OpenFile()");
+    }
+
+    const size_t maxBytesInAStep = std::numeric_limits::max();
+
+    const char* p = reinterpret_cast(data);
+    
+    while (length > 0)
+    {
+      int bytes = static_cast(length <= maxBytesInAStep ? length : maxBytesInAStep);
+
+      int result = zipWriteInFileInZip(pimpl_->file_, p, bytes);
+      if (result != ZIP_OK)
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile,
+                               "Cannot write data to ZIP archive - error code =" + boost::lexical_cast(result));  // we do not log the path anymore since it can contain PHI
+      }
+      
+      p += bytes;
+      length -= bytes;
+    }
+  }
+
+
+  void ZipWriter::SetAppendToExisting(bool append)
+  {
+    Close();
+    append_ = append;
+  }
+
+  bool ZipWriter::IsAppendToExisting() const
+  {
+    return append_;
+  }
+
+  bool ZipWriter::IsZip64() const
+  {
+    return isZip64_;
+  }
+  
+
+  void ZipWriter::AcquireOutputStream(IOutputStream* stream,
+                                      bool isZip64)
+  {
+    std::unique_ptr protection(stream);
+    
+    if (stream == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      Close();
+      path_.clear();
+      isZip64_ = isZip64;
+      outputStream_.reset(protection.release());
+    }
+  }
+
+
+  void ZipWriter::SetMemoryOutput(std::string& target,
+                                  bool isZip64)
+  {
+    AcquireOutputStream(new MemoryStream(target), isZip64);
+  }
+
+
+  void ZipWriter::CancelStream()
+  {
+    if (outputStream_.get() == NULL ||
+        pimpl_->streamBuffer_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Only applicable after AcquireOutputStream() and Open()");
+    }
+    else
+    {
+      pimpl_->streamBuffer_->Cancel();
+    }
+  }
+
+
+  uint64_t ZipWriter::GetArchiveSize() const
+  {
+    if (outputStream_.get() != NULL)
+    {
+      return outputStream_->GetArchiveSize();
+    }
+    else if (path_.empty())
+    {
+      // This is the case after a call to "Close()"
+      return pimpl_->archiveSize_;
+    }
+    else
+    {
+      return SystemToolbox::GetFileSize(path_);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/Compression/ZipWriter.h b/OrthancFramework/Sources/Compression/ZipWriter.h
new file mode 100644
index 0000000..26e1933
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/ZipWriter.h
@@ -0,0 +1,185 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+
+#if !defined(ORTHANC_ENABLE_ZLIB)
+#  error The macro ORTHANC_ENABLE_ZLIB must be defined
+#endif
+
+#if ORTHANC_ENABLE_ZLIB != 1
+#  error ZLIB support must be enabled to include this file
+#endif
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include 
+#endif
+
+#include "../ChunkedBuffer.h"
+#include "../Compatibility.h"
+
+
+#include 
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ZipWriter : public boost::noncopyable
+  {
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+    FRIEND_TEST(ZipWriter, BufferWithSeek);
+#endif
+
+  public:
+    // New in Orthanc 1.9.4
+    class ORTHANC_PUBLIC IOutputStream : public boost::noncopyable
+    {
+    public:
+      virtual ~IOutputStream()
+      {
+      }
+
+      virtual void Write(const std::string& chunk) = 0;
+
+      virtual void Close() = 0;
+
+      virtual uint64_t GetArchiveSize() const = 0;
+    };
+
+
+    // The lifetime of the "target" buffer must be larger than that of ZipWriter
+    class ORTHANC_PUBLIC MemoryStream : public IOutputStream
+    {
+    private:
+      std::string&   target_;
+      ChunkedBuffer  chunked_;
+      uint64_t       archiveSize_;
+      
+    public:
+      explicit MemoryStream(std::string& target);
+      
+      virtual void Write(const std::string& chunk) ORTHANC_OVERRIDE;
+      
+      virtual void Close() ORTHANC_OVERRIDE;
+
+      virtual uint64_t GetArchiveSize() const ORTHANC_OVERRIDE;
+    };
+
+
+  private:
+    // This class is only public for unit tests
+    class ORTHANC_PUBLIC BufferWithSeek : public boost::noncopyable
+    {
+    private:
+      size_t         currentPosition_;
+      ChunkedBuffer  chunks_;
+      std::string    flattened_;
+
+      void CheckInvariants() const;
+  
+    public:
+      BufferWithSeek();
+
+      ~BufferWithSeek();
+
+      size_t GetPosition() const;
+  
+      size_t GetSize() const;
+
+      void Write(const void* data,
+                 size_t size);
+
+      void Write(const std::string& data);
+
+      void Seek(size_t position);
+
+      void Flush(std::string& target);
+    };
+
+    
+  private:
+    class StreamBuffer;
+    
+    struct PImpl;
+    boost::shared_ptr pimpl_;
+
+    bool isZip64_;
+    bool hasFileInZip_;
+    bool append_;
+    uint8_t compressionLevel_;
+    std::string path_;
+
+    std::unique_ptr outputStream_;
+
+  public:
+    ZipWriter();
+
+    ~ZipWriter();
+
+    void SetZip64(bool isZip64);
+
+    bool IsZip64() const;
+
+    void SetCompressionLevel(uint8_t level);
+
+    uint8_t GetCompressionLevel() const;
+
+    void SetAppendToExisting(bool append);
+    
+    bool IsAppendToExisting() const;
+    
+    void Open();
+
+    void Close();
+
+    bool IsOpen() const;
+
+    void SetOutputPath(const char* path);
+
+    const std::string& GetOutputPath() const;
+
+    void OpenFile(const char* path);
+
+    void Write(const void* data, size_t length);
+
+    void Write(const std::string& data);
+
+    void AcquireOutputStream(IOutputStream* stream, // transfers ownership
+                             bool isZip64);
+
+    // The lifetime of the "target" buffer must be larger than that of ZipWriter
+    void SetMemoryOutput(std::string& target,
+                         bool isZip64);
+
+    void CancelStream();
+
+    // WARNING: "GetArchiveSize()" only has its final value after
+    // "Close()" has been called
+    uint64_t GetArchiveSize() const;
+  };
+}
diff --git a/OrthancFramework/Sources/Compression/ZlibCompressor.cpp b/OrthancFramework/Sources/Compression/ZlibCompressor.cpp
new file mode 100644
index 0000000..e18f4cb
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.cpp
@@ -0,0 +1,164 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "ZlibCompressor.h"
+
+#include "../Endianness.h"
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  ZlibCompressor::ZlibCompressor()
+  {
+    SetPrefixWithUncompressedSize(true);
+  }
+
+  void ZlibCompressor::Compress(std::string& compressed,
+                                const void* uncompressed,
+                                size_t uncompressedSize)
+  {
+    if (uncompressedSize == 0)
+    {
+      compressed.clear();
+      return;
+    }
+
+    uLongf compressedSize = compressBound(static_cast(uncompressedSize))
+      + 1024 /* security margin */;
+    if (compressedSize == 0)
+    {
+      compressedSize = 1;
+    }
+
+    uint8_t* target;
+    if (HasPrefixWithUncompressedSize())
+    {
+      compressed.resize(compressedSize + sizeof(uint64_t));
+      target = reinterpret_cast(&compressed[0]) + sizeof(uint64_t);
+    }
+    else
+    {
+      compressed.resize(compressedSize);
+      target = reinterpret_cast(&compressed[0]);
+    }
+
+    int error = compress2(target,
+                          &compressedSize,
+                          const_cast(static_cast(uncompressed)), 
+                          static_cast(uncompressedSize),
+                          GetCompressionLevel());
+
+    if (error != Z_OK)
+    {
+      compressed.clear();
+
+      switch (error)
+      {
+      case Z_MEM_ERROR:
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+
+    // The compression was successful
+    if (HasPrefixWithUncompressedSize())
+    {
+      uint64_t s = static_cast(uncompressedSize);
+
+      // New in Orthanc 1.9.0: Explicitly use litte-endian encoding in size prefix
+      s = htole64(s);
+
+      memcpy(&compressed[0], &s, sizeof(uint64_t));
+      compressed.resize(compressedSize + sizeof(uint64_t));
+    }
+    else
+    {
+      compressed.resize(compressedSize);
+    }
+  }
+
+
+  void ZlibCompressor::Uncompress(std::string& uncompressed,
+                                  const void* compressed,
+                                  size_t compressedSize)
+  {
+    if (compressedSize == 0)
+    {
+      uncompressed.clear();
+      return;
+    }
+
+    if (!HasPrefixWithUncompressedSize())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot guess the uncompressed size of a zlib-encoded buffer");
+    }
+
+    uint64_t uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
+    
+    // New in Orthanc 1.9.0: Explicitly use litte-endian encoding in size prefix
+    uncompressedSize = le64toh(uncompressedSize);
+
+    try
+    {
+      uncompressed.resize(static_cast(uncompressedSize));
+    }
+    catch (...)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    uLongf tmp = static_cast(uncompressedSize);
+    int error = uncompress
+      (reinterpret_cast(&uncompressed[0]), 
+       &tmp,
+       reinterpret_cast(compressed) + sizeof(uint64_t),
+        static_cast(compressedSize - sizeof(uint64_t)));
+
+    if (error != Z_OK)
+    {
+      uncompressed.clear();
+
+      switch (error)
+      {
+      case Z_DATA_ERROR:
+        throw OrthancException(ErrorCode_CorruptedFile);
+
+      case Z_MEM_ERROR:
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/Compression/ZlibCompressor.h b/OrthancFramework/Sources/Compression/ZlibCompressor.h
new file mode 100644
index 0000000..182fd6d
--- /dev/null
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.h
@@ -0,0 +1,45 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DeflateBaseCompressor.h"
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ZlibCompressor : public DeflateBaseCompressor
+  {
+  public:
+    ZlibCompressor();
+
+    virtual void Compress(std::string& compressed,
+                          const void* uncompressed,
+                          size_t uncompressedSize) ORTHANC_OVERRIDE;
+
+    virtual void Uncompress(std::string& uncompressed,
+                            const void* compressed,
+                            size_t compressedSize) ORTHANC_OVERRIDE;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp
new file mode 100644
index 0000000..b5a5893
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp
@@ -0,0 +1,109 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomArray.h"
+
+#include "../OrthancException.h"
+
+#include 
+
+namespace Orthanc
+{
+  DicomArray::DicomArray(const DicomMap& map)
+  {
+    elements_.reserve(map.content_.size());
+    
+    for (DicomMap::Content::const_iterator it = 
+           map.content_.begin(); it != map.content_.end(); ++it)
+    {
+      elements_.push_back(new DicomElement(it->first, *it->second));
+    }
+  }
+
+
+  DicomArray::~DicomArray()
+  {
+    for (size_t i = 0; i < elements_.size(); i++)
+    {
+      delete elements_[i];
+    }
+  }
+
+
+  size_t DicomArray::GetSize() const
+  {
+    return elements_.size();
+  }
+
+
+  const DicomElement &DicomArray::GetElement(size_t i) const
+  {
+    if (i >= elements_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return *elements_[i];
+    }
+  }
+
+  void DicomArray::GetTags(std::set& tags) const
+  {
+    tags.clear();
+
+    for (size_t i = 0; i < elements_.size(); i++)
+    {
+      tags.insert(elements_[i]->GetTag());
+    }
+   
+  }
+
+  void DicomArray::Print(FILE* fp) const
+  {
+    for (size_t  i = 0; i < elements_.size(); i++)
+    {
+      DicomTag t = elements_[i]->GetTag();
+      const DicomValue& v = elements_[i]->GetValue();
+
+      std::string s;
+      if (v.IsNull())
+      {
+        s = "(null)";
+      }
+      else if (v.IsSequence())
+      {
+        //s = "(sequence)";
+        s = "(sequence) " + v.GetSequenceContent().toStyledString();
+      }
+      else
+      {
+        s = v.GetContent();
+      }
+
+      printf("0x%04x 0x%04x [%s]\n", t.GetGroup(), t.GetElement(), s.c_str());
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomArray.h b/OrthancFramework/Sources/DicomFormat/DicomArray.h
new file mode 100644
index 0000000..6413dda
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h
@@ -0,0 +1,54 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomElement.h"
+#include "DicomMap.h"
+
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomArray : public boost::noncopyable
+  {
+  private:
+    typedef std::vector  Elements;
+
+    Elements  elements_;
+
+  public:
+    explicit DicomArray(const DicomMap& map);
+
+    ~DicomArray();
+
+    size_t GetSize() const;
+
+    const DicomElement& GetElement(size_t i) const;
+
+    void GetTags(std::set& tags) const;
+
+    void Print(FILE* fp) const;  // For debugging only
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomElement.cpp b/OrthancFramework/Sources/DicomFormat/DicomElement.cpp
new file mode 100644
index 0000000..431951a
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomElement.cpp
@@ -0,0 +1,75 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomElement.h"
+
+
+namespace Orthanc
+{
+  DicomElement::DicomElement(uint16_t group,
+                             uint16_t element,
+                             const DicomValue &value) :
+    tag_(group, element),
+    value_(value.Clone())
+  {
+  }
+
+  DicomElement::DicomElement(const DicomTag &tag,
+                             const DicomValue &value) :
+    tag_(tag),
+    value_(value.Clone())
+  {
+  }
+
+  DicomElement::~DicomElement()
+  {
+    delete value_;
+  }
+
+  const DicomTag &DicomElement::GetTag() const
+  {
+    return tag_;
+  }
+
+  const DicomValue &DicomElement::GetValue() const
+  {
+    return *value_;
+  }
+
+  uint16_t DicomElement::GetTagGroup() const
+  {
+    return tag_.GetGroup();
+  }
+
+  uint16_t DicomElement::GetTagElement() const
+  {
+    return tag_.GetElement();
+  }
+
+  bool DicomElement::operator<(const DicomElement &other) const
+  {
+    return GetTag() < other.GetTag();
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomElement.h b/OrthancFramework/Sources/DicomFormat/DicomElement.h
new file mode 100644
index 0000000..3327d28
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomElement.h
@@ -0,0 +1,58 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomValue.h"
+#include "DicomTag.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomElement : public boost::noncopyable
+  {
+  private:
+    DicomTag tag_;
+    DicomValue* value_;
+
+  public:
+    DicomElement(uint16_t group,
+                 uint16_t element,
+                 const DicomValue& value);
+
+    DicomElement(const DicomTag& tag,
+                 const DicomValue& value);
+
+    ~DicomElement();
+
+    const DicomTag& GetTag() const;
+
+    const DicomValue& GetValue() const;
+
+    uint16_t GetTagGroup() const;
+
+    uint16_t GetTagElement() const;
+
+    bool operator< (const DicomElement& other) const;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp
new file mode 100644
index 0000000..956ce93
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp
@@ -0,0 +1,596 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "DicomImageInformation.h"
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "../Toolbox.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  DicomImageInformation::DicomImageInformation(const DicomMap& values)
+  {
+    std::string sopClassUid;
+    if (values.LookupStringValue(sopClassUid, DICOM_TAG_SOP_CLASS_UID, false))
+    {
+      sopClassUid = Toolbox::StripSpaces(sopClassUid);
+      if (sopClassUid == "1.2.840.10008.5.1.4.1.1.481.3" /* RT-STRUCT */)
+      {
+        LOG(WARNING) << "Orthanc::DicomImageInformation() should not be applied to SOP Class UID: " << sopClassUid;
+      }
+    }
+
+    uint32_t pixelRepresentation = 0;
+    uint32_t planarConfiguration = 0;
+
+    try
+    {
+      std::string p;
+      if (values.LookupStringValue(p, DICOM_TAG_PHOTOMETRIC_INTERPRETATION, false)) {
+        Toolbox::ToUpperCase(p);
+
+        if (p == "RGB")
+        {
+          photometric_ = PhotometricInterpretation_RGB;
+        }
+        else if (p == "MONOCHROME1")
+        {
+          photometric_ = PhotometricInterpretation_Monochrome1;
+        }
+        else if (p == "MONOCHROME2")
+        {
+          photometric_ = PhotometricInterpretation_Monochrome2;
+        }
+        else if (p == "PALETTE COLOR")
+        {
+          photometric_ = PhotometricInterpretation_Palette;
+        }
+        else if (p == "HSV")
+        {
+          photometric_ = PhotometricInterpretation_HSV;
+        }
+        else if (p == "ARGB")
+        {
+          photometric_ = PhotometricInterpretation_ARGB;
+        }
+        else if (p == "CMYK")
+        {
+          photometric_ = PhotometricInterpretation_CMYK;
+        }
+        else if (p == "YBR_FULL")
+        {
+          photometric_ = PhotometricInterpretation_YBRFull;
+        }
+        else if (p == "YBR_FULL_422")
+        {
+          photometric_ = PhotometricInterpretation_YBRFull422;
+        }
+        else if (p == "YBR_PARTIAL_420")
+        {
+          photometric_ = PhotometricInterpretation_YBRPartial420;
+        }
+        else if (p == "YBR_PARTIAL_422")
+        {
+          photometric_ = PhotometricInterpretation_YBRPartial422;
+        }
+        else if (p == "YBR_ICT")
+        {
+          photometric_ = PhotometricInterpretation_YBR_ICT;
+        }
+        else if (p == "YBR_RCT")
+        {
+          photometric_ = PhotometricInterpretation_YBR_RCT;
+        }
+        else
+        {
+          photometric_ = PhotometricInterpretation_Unknown;
+        }
+      }
+      else
+      {
+        photometric_ = PhotometricInterpretation_Unknown;
+      }
+
+      values.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(width_); // in some US images, we've seen tag values of "800\0"; that's why we parse the 'first' value
+      values.GetValue(DICOM_TAG_ROWS).ParseFirstUnsignedInteger(height_);
+
+      if (!values.ParseUnsignedInteger32(bitsAllocated_, DICOM_TAG_BITS_ALLOCATED))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      if (!values.ParseUnsignedInteger32(samplesPerPixel_, DICOM_TAG_SAMPLES_PER_PIXEL))
+      {
+        samplesPerPixel_ = 1;  // Assume 1 color channel
+      }
+
+      if (!values.ParseUnsignedInteger32(bitsStored_, DICOM_TAG_BITS_STORED))
+      {
+        bitsStored_ = bitsAllocated_;
+      }
+
+      if (bitsStored_ > bitsAllocated_)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      if (!values.ParseUnsignedInteger32(highBit_, DICOM_TAG_HIGH_BIT))
+      {
+        highBit_ = bitsStored_ - 1;
+      }
+
+      if (!values.ParseUnsignedInteger32(pixelRepresentation, DICOM_TAG_PIXEL_REPRESENTATION))
+      {
+        pixelRepresentation = 0;  // Assume unsigned pixels
+      }
+
+      if (samplesPerPixel_ > 1)
+      {
+        // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1
+        // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
+
+        if (!values.ParseUnsignedInteger32(planarConfiguration, DICOM_TAG_PLANAR_CONFIGURATION))
+        {
+          planarConfiguration = 0;  // Assume interleaved color channels
+        }
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+    catch (OrthancException&)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    
+    if (values.HasTag(DICOM_TAG_NUMBER_OF_FRAMES))
+    {
+      if (!values.ParseUnsignedInteger32(numberOfFrames_, DICOM_TAG_NUMBER_OF_FRAMES))
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+    else
+    {
+      numberOfFrames_ = 1;
+    }
+
+    if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
+        bitsAllocated_ != 24 && bitsAllocated_ != 32 &&
+        bitsAllocated_ != 1 /* new in Orthanc 1.10.0 */)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast(bitsAllocated_) + " bits allocated");
+    }
+    else if (numberOfFrames_ == 0)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)");
+    }
+    else if (planarConfiguration != 0 && planarConfiguration != 1)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast(planarConfiguration));
+    }
+
+    if (samplesPerPixel_ == 0)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
+    }
+
+    if (bitsStored_ == 1)
+    {
+      // This is the case of DICOM SEG, new in Orthanc 1.10.0
+      if (bitsAllocated_ != 1)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else if (width_ % 8 != 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Bad number of columns for a black-and-white image");
+      }
+      else
+      {
+        bytesPerValue_ = 0;  // Arbitrary initialization
+      }
+    }
+    else
+    {
+      bytesPerValue_ = bitsAllocated_ / 8;
+    }
+
+    isPlanar_ = (planarConfiguration != 0 ? true : false);
+    isSigned_ = (pixelRepresentation != 0 ? true : false);
+
+    // New in Orthanc 1.12.7
+    double d;
+
+    if (values.ParseDouble(d, DICOM_TAG_RESCALE_SLOPE))
+    {
+      rescaleSlope_ = d;
+    }
+    else
+    {
+      rescaleSlope_ = 1;
+    }
+
+    if (values.ParseDouble(d, DICOM_TAG_RESCALE_INTERCEPT))
+    {
+      rescaleIntercept_ = d;
+    }
+    else
+    {
+      rescaleIntercept_ = 0;
+    }
+
+    if (values.ParseDouble(d, DICOM_TAG_DOSE_GRID_SCALING))
+    {
+      rescaleSlope_ *= d;
+    }
+
+    const std::string centerTag = values.GetStringValue(DICOM_TAG_WINDOW_CENTER, "", false);
+    const std::string widthTag = values.GetStringValue(DICOM_TAG_WINDOW_WIDTH, "", false);
+    if (!centerTag.empty() &&
+        !widthTag.empty())
+    {
+      std::vector centers, widths;
+      Toolbox::TokenizeString(centers, centerTag, '\\');
+      Toolbox::TokenizeString(widths, widthTag, '\\');
+      if (centers.size() == widths.size())
+      {
+        for (size_t i = 0; i < centers.size(); i++)
+        {
+          double center, width;
+          if (SerializationToolbox::ParseDouble(center, centers[i]) &&
+              SerializationToolbox::ParseDouble(width, widths[i]))
+          {
+            windows_.push_back(Window(center, width));
+          }
+        }
+      }
+    }
+  }
+
+  DicomImageInformation* DicomImageInformation::Clone() const
+  {
+    std::unique_ptr target(new DicomImageInformation);
+    target->width_ = width_;
+    target->height_ = height_;
+    target->samplesPerPixel_ = samplesPerPixel_;
+    target->numberOfFrames_ = numberOfFrames_;
+    target->isPlanar_ = isPlanar_;
+    target->isSigned_ = isSigned_;
+    target->bytesPerValue_ = bytesPerValue_;
+    target->bitsAllocated_ = bitsAllocated_;
+    target->bitsStored_ = bitsStored_;
+    target->highBit_ = highBit_;
+    target->photometric_ = photometric_;
+    target->rescaleSlope_ = rescaleSlope_;
+    target->rescaleIntercept_ = rescaleIntercept_;
+    target->windows_ = windows_;
+
+    return target.release();
+  }
+
+  unsigned int DicomImageInformation::GetWidth() const
+  {
+    return width_;
+  }
+
+  unsigned int DicomImageInformation::GetHeight() const
+  {
+    return height_;
+  }
+
+  unsigned int DicomImageInformation::GetNumberOfFrames() const
+  {
+    return numberOfFrames_;
+  }
+
+  unsigned int DicomImageInformation::GetChannelCount() const
+  {
+    return samplesPerPixel_;
+  }
+
+  unsigned int DicomImageInformation::GetBitsStored() const
+  {
+    return bitsStored_;
+  }
+
+  size_t DicomImageInformation::GetBytesPerValue() const
+  {
+    if (bitsStored_ == 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "This call is incompatible with black-and-white images");
+    }
+    else
+    {
+      assert(bitsAllocated_ >= 8);
+      return bytesPerValue_;
+    }
+  }
+
+  bool DicomImageInformation::IsSigned() const
+  {
+    return isSigned_;
+  }
+
+  unsigned int DicomImageInformation::GetBitsAllocated() const
+  {
+    return bitsAllocated_;
+  }
+
+  unsigned int DicomImageInformation::GetHighBit() const
+  {
+    return highBit_;
+  }
+
+  bool DicomImageInformation::IsPlanar() const
+  {
+    return isPlanar_;
+  }
+
+  unsigned int DicomImageInformation::GetShift() const
+  {
+    return highBit_ + 1 - bitsStored_;
+  }
+
+  PhotometricInterpretation DicomImageInformation::GetPhotometricInterpretation() const
+  {
+    return photometric_;
+  }
+
+  bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format,
+                                                 bool ignorePhotometricInterpretation) const
+  {
+    if (photometric_ == PhotometricInterpretation_Palette)
+    {
+      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_RGB24;
+        return true;
+      }
+
+      if (GetBitsStored() == 16 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_RGB48;
+        return true;
+      }
+    }
+    
+    if (ignorePhotometricInterpretation ||
+        photometric_ == PhotometricInterpretation_Monochrome1 ||
+        photometric_ == PhotometricInterpretation_Monochrome2)
+    {
+      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale8;
+        return true;
+      }
+      
+      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale16;
+        return true;
+      }
+
+      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned())
+      {
+        format = PixelFormat_SignedGrayscale16;
+        return true;
+      }
+      
+      if (GetBitsAllocated() == 32 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale32;
+        return true;
+      }
+
+      if (GetBitsStored() == 1 && GetChannelCount() == 1 && !IsSigned())
+      {
+        // This is the case of DICOM SEG, new in Orthanc 1.10.0
+        format = PixelFormat_Grayscale8;
+        return true;
+      }
+    }
+
+    if (GetBitsStored() == 8 &&
+        GetChannelCount() == 3 &&
+        !IsSigned() &&
+        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
+    {
+      format = PixelFormat_RGB24;
+      return true;
+    }
+
+    if (GetBitsStored() == 16 &&
+        GetChannelCount() == 3 &&
+        !IsSigned() &&
+        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
+    {
+      format = PixelFormat_RGB48;
+      return true;
+    }
+
+    return false;
+  }
+
+
+  size_t DicomImageInformation::GetFrameSize() const
+  {
+    if (bitsStored_ == 1)
+    {
+      assert(GetWidth() % 8 == 0);
+      
+      if (GetChannelCount() == 1)
+      {
+        return GetHeight() * GetWidth() / 8;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_IncompatibleImageFormat,
+                               "Image not supported (multi-channel black-and-image image)");
+      }
+    }
+    else
+    {
+      return (GetHeight() *
+              GetWidth() *
+              GetBytesPerValue() *
+              GetChannelCount());
+    }
+  }
+
+
+  unsigned int DicomImageInformation::GetUsefulTagLength()
+  {
+    return 256;
+  }
+
+
+  ValueRepresentation DicomImageInformation::GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax,
+                                                                               unsigned int bitsAllocated)
+  {
+    /**
+     * This approach is validated in "Tests/GuessPixelDataVR.py":
+     * https://orthanc.uclouvain.be/hg/orthanc-tests/file/default/Tests/GuessPixelDataVR.py
+     **/
+
+    if (transferSyntax == DicomTransferSyntax_LittleEndianExplicit ||
+        transferSyntax == DicomTransferSyntax_BigEndianExplicit)
+    {
+      /**
+       * Same rules apply to Little Endian Explicit and Big Endian
+       * Explicit (now retired). The VR of the pixel data directly
+       * depends upon the "Bits Allocated (0028,0100)" tag:
+       * https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.2.html
+       * https://dicom.nema.org/medical/dicom/2016b/output/chtml/part05/sect_A.3.html
+       **/
+      if (bitsAllocated > 8)
+      {
+        return ValueRepresentation_OtherWord;
+      }
+      else
+      {
+        return ValueRepresentation_OtherByte;
+      }
+    }
+    else if (transferSyntax == DicomTransferSyntax_LittleEndianImplicit)
+    {
+      // Assume "OW" for DICOM Implicit VR Little Endian Transfer Syntax
+      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html#sect_A.1
+      return ValueRepresentation_OtherWord;
+    }
+    else
+    {
+      // Assume "OB" for all the compressed transfer syntaxes
+      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.html
+      return ValueRepresentation_OtherByte;
+    }
+  }
+
+
+  const Window& DicomImageInformation::GetWindow(size_t index) const
+  {
+    if (index < windows_.size())
+    {
+      return windows_[index];
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  double DicomImageInformation::ApplyRescale(double value) const
+  {
+    return rescaleSlope_ * value + rescaleIntercept_;
+  }
+
+
+  Window DicomImageInformation::GetDefaultWindow() const
+  {
+    if (windows_.empty())
+    {
+      const double width = static_cast(1 << GetBitsStored());
+      const double center = width / 2.0;
+      return Window(center, width);
+    }
+    else
+    {
+      return windows_[0];
+    }
+  }
+
+
+  void DicomImageInformation::ComputeRenderingTransform(double& offset,
+                                                        double& scaling,
+                                                        const Window& window) const
+  {
+    // Check out "../../../OrthancServer/Resources/ImplementationNotes/windowing.py"
+
+    double windowWidth = std::abs(window.GetWidth());
+
+    // Avoid divisions by zero
+    static const double MIN = 0.0001;
+    if (windowWidth <= MIN)
+    {
+      windowWidth = MIN;
+    }
+
+    if (GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1)
+    {
+      scaling = -255.0 * GetRescaleSlope() / windowWidth;
+      offset = 255.0 * (window.GetCenter() - GetRescaleIntercept()) / windowWidth + 127.5;
+    }
+    else
+    {
+      scaling = 255.0 * GetRescaleSlope() / windowWidth;
+      offset = 255.0 * (GetRescaleIntercept() - window.GetCenter()) / windowWidth + 127.5;
+    }
+  }
+
+
+  void DicomImageInformation::ComputeRenderingTransform(double& offset,
+                                                        double& scaling) const
+  {
+    ComputeRenderingTransform(offset, scaling, GetDefaultWindow());
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h
new file mode 100644
index 0000000..cef033c
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h
@@ -0,0 +1,154 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomMap.h"
+#include "Window.h"
+
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomImageInformation
+  {  
+  private:
+    unsigned int width_;
+    unsigned int height_;
+    unsigned int samplesPerPixel_;
+    uint32_t numberOfFrames_;
+
+    bool isPlanar_;
+    bool isSigned_;
+    size_t bytesPerValue_;
+
+    uint32_t bitsAllocated_;
+    uint32_t bitsStored_;
+    uint32_t highBit_;
+
+    PhotometricInterpretation  photometric_;
+
+    double rescaleSlope_;
+    double rescaleIntercept_;
+    std::vector  windows_;
+
+  protected:
+    explicit DicomImageInformation()
+    {
+    }
+
+  public:
+    explicit DicomImageInformation(const DicomMap& values);
+
+    DicomImageInformation* Clone() const;
+
+    unsigned int GetWidth() const;
+
+    unsigned int GetHeight() const;
+
+    unsigned int GetNumberOfFrames() const;
+
+    unsigned int GetChannelCount() const;
+
+    unsigned int GetBitsStored() const;
+
+    size_t GetBytesPerValue() const;
+
+    bool IsSigned() const;
+
+    unsigned int GetBitsAllocated() const;
+
+    unsigned int GetHighBit() const;
+
+    bool IsPlanar() const;
+
+    unsigned int GetShift() const;
+
+    PhotometricInterpretation GetPhotometricInterpretation() const;
+
+    bool ExtractPixelFormat(PixelFormat& format,
+                            bool ignorePhotometricInterpretation) const;
+
+    size_t GetFrameSize() const;
+
+    /**
+     * This constant gives a bound on the maximum tag length that is
+     * useful to class "DicomImageInformation", in order to avoid
+     * using too much memory when copying DICOM tags from "DcmDataset"
+     * to "DicomMap" using "ExtractDicomSummary()". It answers the
+     * value 256, which corresponds to ORTHANC_MAXIMUM_TAG_LENGTH that
+     * was implicitly used in Orthanc <= 1.7.2.
+     **/
+    static unsigned int GetUsefulTagLength();
+
+    static ValueRepresentation GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax,
+                                                                 unsigned int bitsAllocated);
+
+    double GetRescaleSlope() const
+    {
+      return rescaleSlope_;
+    }
+
+    double GetRescaleIntercept() const
+    {
+      return rescaleIntercept_;
+    }
+
+    bool HasWindows() const
+    {
+      return !windows_.empty();
+    }
+
+    size_t GetWindowsCount() const
+    {
+      return windows_.size();
+    }
+
+    const Window& GetWindow(size_t index) const;
+
+    double ApplyRescale(double value) const;
+
+    Window GetDefaultWindow() const;
+
+    /**
+     * Compute the linear transform "x * scaling + offset" that maps a
+     * window onto the [0,255] range of a grayscale image. The
+     * inversion due to MONOCHROME1 is taken into consideration. This
+     * information can be used in ImageProcessing::ShiftScale2().
+     **/
+    void ComputeRenderingTransform(double& offset,
+                                   double& scaling,
+                                   const Window& window) const;
+
+    void ComputeRenderingTransform(double& offset,
+                                   double& scaling,
+                                   size_t windowIndex) const
+    {
+      ComputeRenderingTransform(offset, scaling, GetWindow(windowIndex));
+    }
+
+    void ComputeRenderingTransform(double& offset,
+                                   double& scaling) const;  // Use the default windowing
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp
new file mode 100644
index 0000000..fab7f73
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp
@@ -0,0 +1,128 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomInstanceHasher.h"
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+namespace Orthanc
+{
+  void DicomInstanceHasher::Setup(const std::string& patientId,
+                                  const std::string& studyUid,
+                                  const std::string& seriesUid,
+                                  const std::string& instanceUid)
+  {
+    patientId_ = patientId;
+    studyUid_ = studyUid;
+    seriesUid_ = seriesUid;
+    instanceUid_ = instanceUid;
+
+    if (studyUid_.size() == 0 ||
+        seriesUid_.size() == 0 ||
+        instanceUid_.size() == 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
+    }
+  }
+
+  DicomInstanceHasher::DicomInstanceHasher(const DicomMap& instance)
+  {
+    const DicomValue* patientId = instance.TestAndGetValue(DICOM_TAG_PATIENT_ID);
+
+    Setup(patientId == NULL ? "" : patientId->GetContent(),
+          instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent(),
+          instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent(),
+          instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent());
+  }
+
+  DicomInstanceHasher::DicomInstanceHasher(const std::string &patientId,
+                                           const std::string &studyUid,
+                                           const std::string &seriesUid,
+                                           const std::string &instanceUid)
+  {
+    Setup(patientId, studyUid, seriesUid, instanceUid);
+  }
+
+  const std::string &DicomInstanceHasher::GetPatientId() const
+  {
+    return patientId_;
+  }
+
+  const std::string &DicomInstanceHasher::GetStudyUid() const
+  {
+    return studyUid_;
+  }
+
+  const std::string &DicomInstanceHasher::GetSeriesUid() const
+  {
+    return seriesUid_;
+  }
+
+  const std::string &DicomInstanceHasher::GetInstanceUid() const
+  {
+    return instanceUid_;
+  }
+
+  const std::string& DicomInstanceHasher::HashPatient() const
+  {
+    if (patientHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(patientHash_, patientId_);
+    }
+
+    return patientHash_;
+  }
+
+  const std::string& DicomInstanceHasher::HashStudy() const
+  {
+    if (studyHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(studyHash_, patientId_ + "|" + studyUid_);
+    }
+
+    return studyHash_;
+  }
+
+  const std::string& DicomInstanceHasher::HashSeries() const
+  {
+    if (seriesHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(seriesHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_);
+    }
+
+    return seriesHash_;
+  }
+
+  const std::string& DicomInstanceHasher::HashInstance() const
+  {
+    if (instanceHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(instanceHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_ + "|" + instanceUid_);
+    }
+
+    return instanceHash_;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h
new file mode 100644
index 0000000..15170bf
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h
@@ -0,0 +1,83 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomMap.h"
+
+namespace Orthanc
+{
+  /**
+   * This class implements the hashing mechanism that is used to
+   * convert DICOM unique identifiers to Orthanc identifiers. Any
+   * Orthanc identifier for a DICOM resource corresponds to the SHA-1
+   * hash of the DICOM identifiers. 
+
+   * \note SHA-1 hash is used because it is less sensitive to
+   * collision attacks than MD5. [Reference]
+   **/
+  class ORTHANC_PUBLIC DicomInstanceHasher
+  {
+  private:
+    std::string patientId_;
+    std::string studyUid_;
+    std::string seriesUid_;
+    std::string instanceUid_;
+
+    mutable std::string patientHash_;
+    mutable std::string studyHash_;
+    mutable std::string seriesHash_;
+    mutable std::string instanceHash_;
+
+    void Setup(const std::string& patientId,
+               const std::string& studyUid,
+               const std::string& seriesUid,
+               const std::string& instanceUid);
+
+  public:
+    explicit DicomInstanceHasher(const DicomMap& instance);
+
+    DicomInstanceHasher(const std::string& patientId,
+                        const std::string& studyUid,
+                        const std::string& seriesUid,
+                        const std::string& instanceUid);
+
+    const std::string& GetPatientId() const;
+
+    const std::string& GetStudyUid() const;
+
+    const std::string& GetSeriesUid() const;
+
+    const std::string& GetInstanceUid() const;
+
+    const std::string& HashPatient() const;
+
+    const std::string& HashStudy() const;
+
+    const std::string& HashSeries() const;
+
+    const std::string& HashInstance() const;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp
new file mode 100644
index 0000000..6e29289
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp
@@ -0,0 +1,237 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "DicomIntegerPixelAccessor.h"
+
+#include "../OrthancException.h"
+#include 
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  DicomIntegerPixelAccessor::DicomIntegerPixelAccessor(const DicomMap& values,
+                                                       const void* pixelData,
+                                                       size_t size) :
+    information_(values),
+    pixelData_(pixelData),
+    size_(size)
+  {
+    if (information_.GetBitsAllocated() > 32 ||
+        information_.GetBitsStored() >= 32)
+    {
+      // Not available, as the accessor internally uses int32_t values
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    frame_ = 0;
+    frameOffset_ = information_.GetFrameSize();
+
+    if (information_.GetNumberOfFrames() * frameOffset_ > size)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    if (information_.IsSigned())
+    {
+      // Pixels are signed
+      mask_ = (1 << (information_.GetBitsStored() - 1)) - 1;
+      signMask_ = (1 << (information_.GetBitsStored() - 1));
+    }
+    else
+    {
+      // Pixels are unsigned
+      mask_ = (1 << information_.GetBitsStored()) - 1;
+      signMask_ = 0;
+    }
+
+    if (information_.IsPlanar())
+    {
+      /**
+       * Each color plane shall be sent contiguously. For RGB images,
+       * this means the order of the pixel values sent is R1, R2, R3,
+       * ..., G1, G2, G3, ..., B1, B2, B3, etc.
+       **/
+      rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue();
+    }
+    else
+    {
+      /**
+       * The sample values for the first pixel are followed by the
+       * sample values for the second pixel, etc. For RGB images, this
+       * means the order of the pixel values sent shall be R1, G1, B1,
+       * R2, G2, B2, ..., etc.
+       **/
+      if (information_.GetBitsStored() == 1)
+      {
+        if (information_.GetChannelCount() == 1 &&
+            information_.GetBitsAllocated() == 1)
+        {
+          assert(information_.GetWidth() % 8 == 0);  // Tested by DicomImageInformation
+          rowOffset_ = information_.GetWidth() / 8;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_IncompatibleImageFormat,
+                                 "Image not supported (multi-channel black-and-image image)");
+        }
+      }
+      else
+      {
+        rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue() * information_.GetChannelCount();
+      }
+    }
+  }
+
+
+  void DicomIntegerPixelAccessor::GetExtremeValues(int32_t& min, 
+                                                   int32_t& max) const
+  {
+    if (information_.GetHeight() == 0 || information_.GetWidth() == 0)
+    {
+      min = max = 0;
+      return;
+    }
+
+    min = std::numeric_limits::max();
+    max = std::numeric_limits::min();
+
+    const unsigned int height = information_.GetHeight();
+    const unsigned int width = information_.GetWidth();
+    const unsigned int channels = information_.GetChannelCount();
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      for (unsigned int x = 0; x < width; x++)
+      {
+        for (unsigned int c = 0; c < channels; c++)
+        {
+          int32_t v = GetValue(x, y, c);
+          if (v < min)
+            min = v;
+          if (v > max)
+            max = v;
+        }
+      }
+    }
+  }
+
+
+  int32_t DicomIntegerPixelAccessor::GetValue(unsigned int x, 
+                                              unsigned int y,
+                                              unsigned int channel) const
+  {
+    assert(x < information_.GetWidth() && 
+           y < information_.GetHeight() && 
+           channel < information_.GetChannelCount());
+
+    const uint8_t* pixel = (reinterpret_cast(pixelData_) + 
+                            y * rowOffset_ + frame_ * frameOffset_);
+    
+    if (information_.GetBitsStored() == 1)
+    {
+      // New in Orthanc 1.10.0, notably for DICOM SEG
+      assert(information_.GetBitsAllocated() == 1 &&
+             information_.GetChannelCount() == 1 &&
+             !information_.IsPlanar());
+      
+      uint8_t b = pixel[x / 8];
+
+      if (b & (1 << (x % 8)))
+      {
+        return 255;
+      }
+      else
+      {
+        return 0;
+      }
+    }
+    else
+    {
+      // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
+      if (information_.IsPlanar())
+      {
+        /**
+         * Each color plane shall be sent contiguously. For RGB images,
+         * this means the order of the pixel values sent is R1, R2, R3,
+         * ..., G1, G2, G3, ..., B1, B2, B3, etc.
+         **/
+        assert(frameOffset_ % information_.GetChannelCount() == 0);
+        pixel += channel * frameOffset_ / information_.GetChannelCount() + x * information_.GetBytesPerValue();
+      }
+      else
+      {
+        /**
+         * The sample values for the first pixel are followed by the
+         * sample values for the second pixel, etc. For RGB images, this
+         * means the order of the pixel values sent shall be R1, G1, B1,
+         * R2, G2, B2, ..., etc.
+         **/
+        pixel += channel * information_.GetBytesPerValue() + x * information_.GetChannelCount() * information_.GetBytesPerValue();
+      }
+
+      uint32_t v;
+      v = pixel[0];
+      if (information_.GetBytesPerValue() >= 2)
+        v = v + (static_cast(pixel[1]) << 8);
+      if (information_.GetBytesPerValue() >= 3)
+        v = v + (static_cast(pixel[2]) << 16);
+      if (information_.GetBytesPerValue() >= 4)
+        v = v + (static_cast(pixel[3]) << 24);
+
+      v = v >> information_.GetShift();
+
+      if (v & signMask_)
+      {
+        // Signed value
+        // http://en.wikipedia.org/wiki/Two%27s_complement#Subtraction_from_2N
+        return -static_cast(mask_) + static_cast(v & mask_) - 1;
+      }
+      else
+      {
+        // Unsigned value
+        return static_cast(v & mask_);
+      }
+    }
+  }
+
+
+  void DicomIntegerPixelAccessor::SetCurrentFrame(unsigned int frame)
+  {
+    if (frame >= information_.GetNumberOfFrames())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    frame_ = frame;
+  }
+
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h
new file mode 100644
index 0000000..9448d9c
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h
@@ -0,0 +1,83 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomMap.h"
+
+#include "DicomImageInformation.h"
+
+#include 
+
+namespace Orthanc
+{
+  class DicomIntegerPixelAccessor
+  {
+  private:
+    DicomImageInformation information_;
+
+    uint32_t signMask_;
+    uint32_t mask_;
+
+    const void* pixelData_;
+    size_t size_;
+    unsigned int frame_;
+    size_t frameOffset_;
+    size_t rowOffset_;
+
+  public:
+    DicomIntegerPixelAccessor(const DicomMap& values,
+                              const void* pixelData,
+                              size_t size);
+
+    const DicomImageInformation GetInformation() const
+    {
+      return information_;
+    }
+
+    unsigned int GetCurrentFrame() const
+    {
+      return frame_;
+    }
+
+    void SetCurrentFrame(unsigned int frame);
+
+    void GetExtremeValues(int32_t& min, 
+                          int32_t& max) const;
+
+    int32_t GetValue(unsigned int x,
+                     unsigned int y,
+                     unsigned int channel) const;
+
+    const void* GetPixelData() const
+    {
+      return pixelData_;
+    }
+
+    size_t GetSize() const
+    {
+      return size_;
+    }
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp
new file mode 100644
index 0000000..d521528
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp
@@ -0,0 +1,1881 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomMap.h"
+
+#include 
+#include 
+#include 
+
+#include "../Compatibility.h"
+#include "../Endianness.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "DicomArray.h"
+#include "DicomImageInformation.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#include "../DicomParsing/FromDcmtkBridge.h"
+#endif
+
+#if !defined(__EMSCRIPTEN__)
+// Multithreading is not supported in WebAssembly
+#  include 
+#  include   // For boost::unique_lock<> and boost::shared_lock<>
+#endif
+
+namespace Orthanc
+{
+  // WARNING: the DEFAULT list of main dicom tags below are the list as they 
+  // were in Orthanc 1.10 before we introduced the dynamic main dicom tags.
+  // This list has not changed since Orthanc 1.4.2 and had a single change since
+  // Orthanc 0.9.5.
+  // These lists have a specific signature.  When a resource does not have
+  // the metadata "MainDicomTagsSignature", we'll assume that they were stored
+  // with an Orthanc prior to 1.11.  It is therefore very important that you never
+  // change these lists !  Update ResetDefaultMainDicomTags instead.
+
+  static const DicomTag DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS[] =
+  {
+    // { DicomTag(0x0010, 0x1010), "PatientAge" },
+    // { DicomTag(0x0010, 0x1040), "PatientAddress" },
+    DICOM_TAG_PATIENT_NAME,
+    DICOM_TAG_PATIENT_BIRTH_DATE,
+    DICOM_TAG_PATIENT_SEX,
+    DICOM_TAG_OTHER_PATIENT_IDS,
+    DICOM_TAG_PATIENT_ID
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
+  };
+  
+  static const DicomTag DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS[] =
+  {
+    // { DicomTag(0x0010, 0x1020), "PatientSize" },
+    // { DicomTag(0x0010, 0x1030), "PatientWeight" },
+    DICOM_TAG_STUDY_DATE,
+    DICOM_TAG_STUDY_TIME,
+    DICOM_TAG_STUDY_ID,
+    DICOM_TAG_STUDY_DESCRIPTION,
+    DICOM_TAG_ACCESSION_NUMBER,
+    DICOM_TAG_STUDY_INSTANCE_UID,
+    
+    // New in db v6 (Orthanc 0.9.5)
+    DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION,
+    DICOM_TAG_INSTITUTION_NAME,
+    DICOM_TAG_REQUESTING_PHYSICIAN,
+    DICOM_TAG_REFERRING_PHYSICIAN_NAME
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
+  };
+
+  static const DicomTag DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS[] =
+  {
+    // { DicomTag(0x0010, 0x1080), "MilitaryRank" },
+    DICOM_TAG_SERIES_DATE,
+    DICOM_TAG_SERIES_TIME,
+    DICOM_TAG_MODALITY,
+    DICOM_TAG_MANUFACTURER,
+    DICOM_TAG_STATION_NAME,
+    DICOM_TAG_SERIES_DESCRIPTION,
+    DICOM_TAG_BODY_PART_EXAMINED,
+    DICOM_TAG_SEQUENCE_NAME,
+    DICOM_TAG_PROTOCOL_NAME,
+    DICOM_TAG_SERIES_NUMBER,
+    DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES,
+    DICOM_TAG_IMAGES_IN_ACQUISITION,
+    DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS,
+    DICOM_TAG_NUMBER_OF_SLICES,
+    DICOM_TAG_NUMBER_OF_TIME_SLICES,
+    DICOM_TAG_SERIES_INSTANCE_UID,
+
+    // New in db v6 (Orthanc 0.9.5)
+    DICOM_TAG_IMAGE_ORIENTATION_PATIENT,
+    DICOM_TAG_SERIES_TYPE,
+    DICOM_TAG_OPERATOR_NAME,
+    DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION,
+    DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION,
+    DICOM_TAG_CONTRAST_BOLUS_AGENT
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
+  };
+
+  static const DicomTag DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS[] =
+  {
+    DICOM_TAG_INSTANCE_CREATION_DATE,
+    DICOM_TAG_INSTANCE_CREATION_TIME,
+    DICOM_TAG_ACQUISITION_NUMBER,
+    DICOM_TAG_IMAGE_INDEX,
+    DICOM_TAG_INSTANCE_NUMBER,
+    DICOM_TAG_NUMBER_OF_FRAMES,
+    DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER,
+    DICOM_TAG_SOP_INSTANCE_UID,
+
+    // New in db v6 (Orthanc 0.9.5)
+    DICOM_TAG_IMAGE_POSITION_PATIENT,
+    DICOM_TAG_IMAGE_COMMENTS,
+
+    /**
+     * Main DICOM tags that are not part of any release of the
+     * database schema yet, and that will be part of future db v7. In
+     * the meantime, the user must call "/tools/reconstruct" once to
+     * access these tags if the corresponding DICOM files where
+     * indexed in the database by an older version of Orthanc.
+     **/
+    DICOM_TAG_IMAGE_ORIENTATION_PATIENT  // New in Orthanc 1.4.2
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
+  };
+
+  class DicomMap::MainDicomTagsConfiguration : public boost::noncopyable
+  {
+  private:
+#if !defined(__EMSCRIPTEN__)
+    typedef boost::unique_lock WriterLock;
+    typedef boost::shared_lock ReaderLock;
+
+    boost::shared_mutex mutex_;
+#endif
+    
+    std::set patientsMainDicomTagsByLevel_;
+    std::set studiesMainDicomTagsByLevel_;
+    std::set seriesMainDicomTagsByLevel_;
+    std::set instancesMainDicomTagsByLevel_;
+
+    std::set allMainDicomTags_;
+
+    std::map signatures_;
+    std::map defaultSignatures_;
+
+    MainDicomTagsConfiguration()
+    {
+      ResetDefaultMainDicomTags();
+    }
+
+    std::string ComputeSignature(const std::set& tags)
+    {
+      // std::set are sorted by default (which is important for us !)
+      std::set tagsIds;
+      for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it)
+      {
+        tagsIds.insert(it->Format());
+      }
+
+      std::string signatureText = boost::algorithm::join(tagsIds, ";");
+
+      return signatureText;
+    }
+
+    void LoadDefaultMainDicomTags(ResourceType level)
+    {
+      const DicomTag* tags = NULL;
+      size_t size;
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          tags = DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Study:
+          tags = DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Series:
+          tags = DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Instance:
+          tags = DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      assert(tags != NULL &&
+            size != 0);
+
+      for (size_t i = 0; i < size; i++)
+      {
+        AddMainDicomTagInternal(tags[i], level);
+      }
+    }
+
+    std::set& GetMainDicomTagsByLevelInternal(ResourceType level)
+    {
+      switch (level)
+      {
+        case ResourceType_Patient:
+          return patientsMainDicomTagsByLevel_;
+
+        case ResourceType_Study:
+          return studiesMainDicomTagsByLevel_;
+
+        case ResourceType_Series:
+          return seriesMainDicomTagsByLevel_;
+
+        case ResourceType_Instance:
+          return instancesMainDicomTagsByLevel_;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    void AddMainDicomTagInternal(const DicomTag& tag,
+                                 ResourceType level)
+    {
+      std::set& existingLevelTags = GetMainDicomTagsByLevelInternal(level);
+      
+      if (existingLevelTags.find(tag) != existingLevelTags.end())
+      {
+        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined", false);
+      }
+
+      existingLevelTags.insert(tag);
+      allMainDicomTags_.insert(tag);
+
+      signatures_[level] = ComputeSignature(GetMainDicomTagsByLevelInternal(level));
+    }
+
+  public:
+    // Singleton pattern
+    static MainDicomTagsConfiguration& GetInstance()
+    {
+      static MainDicomTagsConfiguration parameters;
+      return parameters;
+    }
+
+    void ResetDefaultMainDicomTags()
+    {
+#if !defined(__EMSCRIPTEN__)
+      WriterLock lock(mutex_);
+#endif
+      
+      patientsMainDicomTagsByLevel_.clear();
+      studiesMainDicomTagsByLevel_.clear();
+      seriesMainDicomTagsByLevel_.clear();
+      instancesMainDicomTagsByLevel_.clear();
+
+      allMainDicomTags_.clear();
+
+      // by default, initialize with the previous static list (up to 1.10.0)
+      LoadDefaultMainDicomTags(ResourceType_Patient);
+      LoadDefaultMainDicomTags(ResourceType_Study);
+      LoadDefaultMainDicomTags(ResourceType_Series);
+      LoadDefaultMainDicomTags(ResourceType_Instance);
+
+      defaultSignatures_[ResourceType_Patient] = signatures_[ResourceType_Patient];
+      defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study];
+      defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series];
+      defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance];
+
+      // only add new tags here !
+      // introduced in v 1.12.5
+      AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Study);  // used in default QIDO-RS queries
+      
+      AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE, ResourceType_Series);  // used in default QIDO-RS queries
+
+      // TODO-FIND: remove it from metadata when adding it ! AddMainDicomTagInternal(DICOM_TAG_SOP_CLASS_UID, ResourceType_Instance);  // previously saved in a metadata; makes more sense to store it in a DICOM tag
+    }
+
+    void AddMainDicomTag(const DicomTag& tag,
+                         ResourceType level)
+    {
+#if !defined(__EMSCRIPTEN__)
+      WriterLock lock(mutex_);
+#endif
+      
+      AddMainDicomTagInternal(tag, level);
+    }
+
+    void GetAllMainDicomTags(std::set& target)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      target = allMainDicomTags_;
+    }
+
+    void GetMainDicomTagsByLevel(std::set& target,
+                                 ResourceType level)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      target = GetMainDicomTagsByLevelInternal(level);
+    }
+
+    std::string GetMainDicomTagsSignature(ResourceType level)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      assert(signatures_.find(level) != signatures_.end());
+      return signatures_[level];
+    }
+
+    std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      assert(defaultSignatures_.find(level) != defaultSignatures_.end());
+      return defaultSignatures_[level];
+    }
+
+    bool IsMainDicomTag(const DicomTag& tag)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      return allMainDicomTags_.find(tag) != allMainDicomTags_.end();
+    }
+
+    bool IsMainDicomTag(const DicomTag& tag,
+                        ResourceType level)
+    {
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock lock(mutex_);
+#endif
+      
+      const std::set& mainDicomTags = GetMainDicomTagsByLevelInternal(level);
+      return mainDicomTags.find(tag) != mainDicomTags.end();
+    }
+  };
+
+
+  void DicomMap::SetValueInternal(uint16_t group, 
+                                  uint16_t element, 
+                                  DicomValue* value)
+  {
+    DicomTag tag(group, element);
+    Content::iterator it = content_.find(tag);
+
+    if (it != content_.end())
+    {
+      delete it->second;
+      it->second = value;
+    }
+    else
+    {
+      content_.insert(std::make_pair(tag, value));
+    }
+  }
+
+
+  void DicomMap::Clear()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+
+    content_.clear();
+  }
+
+  void DicomMap::SetNullValue(uint16_t group, uint16_t element)
+  {
+    SetValueInternal(group, element, new DicomValue);
+  }
+
+  void DicomMap::SetNullValue(const DicomTag &tag)
+  {
+    SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue);
+  }
+
+  void DicomMap::SetValue(uint16_t group, uint16_t element, const DicomValue &value)
+  {
+    SetValueInternal(group, element, value.Clone());
+  }
+
+  void DicomMap::SetValue(const DicomTag &tag, const DicomValue &value)
+  {
+    SetValueInternal(tag.GetGroup(), tag.GetElement(), value.Clone());
+  }
+
+  void DicomMap::SetValue(const DicomTag &tag, const std::string &str, bool isBinary)
+  {
+    SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue(str, isBinary));
+  }
+
+  void DicomMap::SetValue(uint16_t group, uint16_t element, const std::string &str, bool isBinary)
+  {
+    SetValueInternal(group, element, new DicomValue(str, isBinary));
+  }
+
+  void DicomMap::SetSequenceValue(const DicomTag& tag, const Json::Value& value)
+  {
+    SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue(value));
+  }
+
+  bool DicomMap::HasTag(uint16_t group, uint16_t element) const
+  {
+    return HasTag(DicomTag(group, element));
+  }
+
+  bool DicomMap::HasTag(const DicomTag &tag) const
+  {
+    return content_.find(tag) != content_.end();
+  }
+
+  const DicomValue &DicomMap::GetValue(uint16_t group, uint16_t element) const
+  {
+    return GetValue(DicomTag(group, element));
+  }
+
+
+  static void ExtractTagsInternal(DicomMap& result,
+                                  const DicomMap::Content& source,
+                                  const std::set& mainDicomTags)
+  {
+    result.Clear();
+
+    for (std::set::const_iterator itmt = mainDicomTags.begin();
+         itmt != mainDicomTags.end(); ++itmt)
+    {
+      DicomMap::Content::const_iterator it = source.find(*itmt);
+      if (it != source.end())
+      {
+        result.SetValue(it->first, *it->second /* value will be cloned */);
+      }
+    }
+  }
+
+  void DicomMap::ExtractTags(DicomMap& result, const std::set& tags) const
+  {
+    result.Clear();
+
+    for (std::set::const_iterator itmt = tags.begin();
+         itmt != tags.end(); ++itmt)
+    {
+      DicomMap::Content::const_iterator it = content_.find(*itmt);
+      if (it != content_.end())
+      {
+        result.SetValue(it->first, *it->second /* value will be cloned */);
+      }
+    }
+  }
+
+  void DicomMap::ExtractResourceInformation(DicomMap& result, ResourceType level) const
+  {
+    std::set mainDicomTags;
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(mainDicomTags, level);
+    ExtractTagsInternal(result, content_, mainDicomTags);
+  }
+
+  void DicomMap::ExtractPatientInformation(DicomMap& result) const
+  {
+    ExtractResourceInformation(result, ResourceType_Patient);
+  }
+
+  void DicomMap::ExtractStudyInformation(DicomMap& result) const
+  {
+    ExtractResourceInformation(result, ResourceType_Study);
+  }
+
+  void DicomMap::ExtractSeriesInformation(DicomMap& result) const
+  {
+    ExtractResourceInformation(result, ResourceType_Series);
+  }
+
+  void DicomMap::ExtractInstanceInformation(DicomMap& result) const
+  {
+    ExtractResourceInformation(result, ResourceType_Instance);
+  }
+
+
+  DicomMap::~DicomMap()
+  {
+    Clear();
+  }
+
+  size_t DicomMap::GetSize() const
+  {
+    return content_.size();
+  }
+
+
+  DicomMap* DicomMap::Clone() const
+  {
+    std::unique_ptr result(new DicomMap);
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      result->content_.insert(std::make_pair(it->first, it->second->Clone()));
+    }
+
+    return result.release();
+  }
+
+
+  void DicomMap::Assign(const DicomMap& other)
+  {
+    Clear();
+
+    for (Content::const_iterator it = other.content_.begin(); it != other.content_.end(); ++it)
+    {
+      content_.insert(std::make_pair(it->first, it->second->Clone()));
+    }
+  }
+
+
+  const DicomValue& DicomMap::GetValue(const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value)
+    {
+      return *value;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentTag);
+    }
+  }
+
+  const DicomValue *DicomMap::TestAndGetValue(uint16_t group, uint16_t element) const
+  {
+    return TestAndGetValue(DicomTag(group, element));
+  }
+
+
+  const DicomValue* DicomMap::TestAndGetValue(const DicomTag& tag) const
+  {
+    Content::const_iterator it = content_.find(tag);
+
+    if (it == content_.end())
+    {
+      return NULL;
+    }
+    else
+    {
+      return it->second;
+    }
+  }
+
+
+  void DicomMap::Remove(const DicomTag& tag) 
+  {
+    Content::iterator it = content_.find(tag);
+    if (it != content_.end())
+    {
+      delete it->second;
+      content_.erase(it);
+    }
+  }
+
+  void DicomMap::RemoveTags(const std::set& tags) 
+  {
+    for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      Remove(*it);
+    }
+  }
+
+  void DicomMap::SetupFindPatientTemplate(DicomMap& result)
+  {
+    result.Clear();
+
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+
+    // Other tags in the "Patient" module
+    result.SetValue(DICOM_TAG_OTHER_PATIENT_IDS, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_BIRTH_DATE, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_NAME, "", false);
+    result.SetValue(DICOM_TAG_PATIENT_SEX, "", false);
+  }
+
+  void DicomMap::SetupFindStudyTemplate(DicomMap& result)
+  {
+    result.Clear();
+
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
+
+    // Other tags in the "General Study" module
+    result.SetValue(DICOM_TAG_REFERRING_PHYSICIAN_NAME, "", false);
+    result.SetValue(DICOM_TAG_STUDY_DATE, "", false);
+    result.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "", false);
+    result.SetValue(DICOM_TAG_STUDY_ID, "", false);
+    result.SetValue(DICOM_TAG_STUDY_TIME, "", false);
+  }
+
+  void DicomMap::SetupFindSeriesTemplate(DicomMap& result)
+  {
+    result.Clear();
+
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
+    result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "", false);
+
+    // Other tags in the "General Series" module
+    result.SetValue(DICOM_TAG_BODY_PART_EXAMINED, "", false);
+    result.SetValue(DICOM_TAG_MODALITY, "", false);
+    result.SetValue(DICOM_TAG_OPERATOR_NAME, "", false);
+    result.SetValue(DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, "", false);
+    result.SetValue(DICOM_TAG_PROTOCOL_NAME, "", false);
+    result.SetValue(DICOM_TAG_SERIES_DATE, "", false);
+    result.SetValue(DICOM_TAG_SERIES_DESCRIPTION, "", false);
+    result.SetValue(DICOM_TAG_SERIES_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_SERIES_TIME, "", false);
+  }
+
+  void DicomMap::SetupFindInstanceTemplate(DicomMap& result)
+  {
+    result.Clear();
+
+    // Identifying tags
+    result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
+    result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
+    result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "", false);
+    result.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "", false);
+
+    // Other tags in the "SOP Common" module
+    result.SetValue(DICOM_TAG_ACQUISITION_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_COMMENTS, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_INDEX, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "", false);
+    result.SetValue(DICOM_TAG_IMAGE_POSITION_PATIENT, "", false);
+    result.SetValue(DICOM_TAG_INSTANCE_CREATION_DATE, "", false);
+    result.SetValue(DICOM_TAG_INSTANCE_CREATION_TIME, "", false);
+    result.SetValue(DICOM_TAG_INSTANCE_NUMBER, "", false);
+    result.SetValue(DICOM_TAG_NUMBER_OF_FRAMES, "", false);
+    result.SetValue(DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, "", false);
+  }
+
+
+  bool DicomMap::CopyTagIfExists(const DicomMap& source,
+                                 const DicomTag& tag)
+  {
+    if (source.HasTag(tag))
+    {
+      SetValue(tag, source.GetValue(tag));
+      return true;
+    }
+
+    return false;
+  }
+
+
+  bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level)
+  {
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().IsMainDicomTag(tag, level);
+  }
+
+  bool DicomMap::IsMainDicomTag(const DicomTag& tag)
+  {
+    return (IsMainDicomTag(tag, ResourceType_Patient) ||
+            IsMainDicomTag(tag, ResourceType_Study) ||
+            IsMainDicomTag(tag, ResourceType_Series) ||
+            IsMainDicomTag(tag, ResourceType_Instance));
+  }
+
+  static bool IsGenericComputedTag(const DicomTag& tag)
+  {
+    return tag == DICOM_TAG_RETRIEVE_URL ||
+      tag == DICOM_TAG_RETRIEVE_AE_TITLE;
+  }
+
+  bool DicomMap::IsComputedTag(const DicomTag& tag)
+  {
+    return (IsComputedTag(tag, ResourceType_Patient) ||
+            IsComputedTag(tag, ResourceType_Study) ||
+            IsComputedTag(tag, ResourceType_Series) ||
+            IsComputedTag(tag, ResourceType_Instance) ||
+            IsGenericComputedTag(tag));
+  }
+
+  bool DicomMap::IsComputedTag(const DicomTag& tag, ResourceType level)
+  {
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return (
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES ||
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES ||
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES
+        );
+      case ResourceType_Study:
+        return (
+          tag == DICOM_TAG_MODALITIES_IN_STUDY ||
+          tag == DICOM_TAG_SOP_CLASSES_IN_STUDY ||
+          tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES ||
+          tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES
+        );
+      case ResourceType_Series:
+        return (
+          tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES
+        );
+      case ResourceType_Instance:
+        return (
+          tag == DICOM_TAG_INSTANCE_AVAILABILITY
+        );
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  bool DicomMap::HasOnlyComputedTags(const std::set& tags)
+  {
+    if (tags.size() == 0)
+    {
+      return false;
+    }
+
+    for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (!IsComputedTag(*it))
+      {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  bool DicomMap::HasComputedTags(const std::set& tags)
+  {
+    for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (IsComputedTag(*it))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool DicomMap::HasComputedTags(const std::set& tags, ResourceType level)
+  {
+    for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (IsComputedTag(*it, level))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  bool DicomMap::HasMetaInformationTags(const std::set& tags)
+  {
+    for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (it->GetGroup() == 0x0002)
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  void DicomMap::GetMainDicomTags(std::set& target,
+                                  ResourceType level)
+  {
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(target, level);
+  }
+
+  void DicomMap::GetAllMainDicomTags(std::set& target)
+  {
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags(target);
+  }
+
+  void DicomMap::AddMainDicomTag(const DicomTag& tag, ResourceType level)
+  {
+    DicomMap::MainDicomTagsConfiguration::GetInstance().AddMainDicomTag(tag, level);
+  }
+
+  void DicomMap::ResetDefaultMainDicomTags()
+  {
+    DicomMap::MainDicomTagsConfiguration::GetInstance().ResetDefaultMainDicomTags();
+  }
+
+  std::string DicomMap::GetMainDicomTagsSignature(ResourceType level)
+  {
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsSignature(level);
+  }
+
+  std::string DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level)
+  {
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignatureFrom1_11(level);
+  }
+
+  void DicomMap::GetTags(std::set& tags) const
+  {
+    tags.clear();
+
+    for (Content::const_iterator it = content_.begin();
+         it != content_.end(); ++it)
+    {
+      tags.insert(it->first);
+    }
+  }
+
+
+  static uint16_t ReadLittleEndianUint16(const char* dicom)
+  {
+    const uint8_t* p = reinterpret_cast(dicom);
+
+    return (static_cast(p[0]) |
+            (static_cast(p[1]) << 8));
+  }
+
+
+  static uint32_t ReadLittleEndianUint32(const char* dicom)
+  {
+    const uint8_t* p = reinterpret_cast(dicom);
+
+    return (static_cast(p[0]) |
+            (static_cast(p[1]) << 8) |
+            (static_cast(p[2]) << 16) |
+            (static_cast(p[3]) << 24));
+  }
+
+
+  static bool ValidateTag(const ValueRepresentation& vr,
+                          const std::string& value)
+  {
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:
+        return value.size() <= 16;
+
+      case ValueRepresentation_AgeString:
+        return (value.size() == 4 &&
+                isdigit(value[0]) &&
+                isdigit(value[1]) &&
+                isdigit(value[2]) &&
+                (value[3] == 'D' || value[3] == 'W' || value[3] == 'M' || value[3] == 'Y'));
+
+      case ValueRepresentation_AttributeTag:
+        return value.size() == 4;
+
+      case ValueRepresentation_CodeString:
+        return value.size() <= 16;
+
+      case ValueRepresentation_Date:
+        return value.size() <= 18;
+
+      case ValueRepresentation_DecimalString:
+        return value.size() <= 16;
+
+      case ValueRepresentation_DateTime:
+        return value.size() <= 54;
+
+      case ValueRepresentation_FloatingPointSingle:
+        return value.size() == 4;
+
+      case ValueRepresentation_FloatingPointDouble:
+        return value.size() == 8;
+
+      case ValueRepresentation_IntegerString:
+        return value.size() <= 12;
+
+      case ValueRepresentation_LongString:
+        return value.size() <= 64;
+
+      case ValueRepresentation_LongText:
+        return value.size() <= 10240;
+
+      case ValueRepresentation_OtherByte:
+        return true;
+      
+      case ValueRepresentation_OtherDouble:
+        return value.size() <= (static_cast(1) << 32) - 8;
+
+      case ValueRepresentation_OtherFloat:
+        return value.size() <= (static_cast(1) << 32) - 4;
+
+      case ValueRepresentation_OtherLong:
+        return true;
+
+      case ValueRepresentation_OtherWord:
+        return true;
+
+      case ValueRepresentation_PersonName:
+        return true;
+
+      case ValueRepresentation_ShortString:
+        return value.size() <= 16;
+
+      case ValueRepresentation_SignedLong:
+        return value.size() == 4;
+
+      case ValueRepresentation_Sequence:
+        return true;
+
+      case ValueRepresentation_SignedShort:
+        return value.size() == 2;
+
+      case ValueRepresentation_ShortText:
+        return value.size() <= 1024;
+
+      case ValueRepresentation_Time:
+        return value.size() <= 28;
+
+      case ValueRepresentation_UnlimitedCharacters:
+        return value.size() <= (static_cast(1) << 32) - 2;
+
+      case ValueRepresentation_UniqueIdentifier:
+        return value.size() <= 64;
+
+      case ValueRepresentation_UnsignedLong:
+        return value.size() == 4;
+
+      case ValueRepresentation_Unknown:
+        return true;
+
+      case ValueRepresentation_UniversalResource:
+        return value.size() <= (static_cast(1) << 32) - 2;
+
+      case ValueRepresentation_UnsignedShort:
+        return value.size() == 2;
+
+      case ValueRepresentation_UnlimitedText:
+        return value.size() <= (static_cast(1) << 32) - 2;
+
+      default:
+        // Assume unsupported tags are OK
+        return true;
+    }
+  }
+
+
+  static void RemoveTagPadding(std::string& value,
+                               const ValueRepresentation& vr)
+  {
+    /**
+     * Remove padding from character strings, if need be. For the time
+     * being, only the UI VR is supported.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+     **/
+
+    switch (vr)
+    {
+      case ValueRepresentation_UniqueIdentifier:
+      {
+        /**
+         * "Values with a VR of UI shall be padded with a single
+         * trailing NULL (00H) character when necessary to achieve even
+         * length."
+         **/
+
+        if (!value.empty() &&
+            value[value.size() - 1] == '\0')
+        {
+          value.resize(value.size() - 1);
+        }
+
+        break;
+      }
+
+      /**
+       * TODO implement other VR
+       **/
+
+      default:
+        // No padding is applicable to this VR
+        break;
+    }
+  }
+
+
+  static bool ReadNextTag(DicomTag& tag,
+                          ValueRepresentation& vr,
+                          std::string& value,
+                          const char* dicom,
+                          size_t size,
+                          size_t& position)
+  {
+    /**
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_7.html#sect_7.1.2
+     * This function reads a data element with Explicit VR encoded using Little-Endian.
+     **/
+
+    if (position + 6 > size)
+    {
+      return false;
+    }
+
+    tag = DicomTag(ReadLittleEndianUint16(dicom + position),
+                   ReadLittleEndianUint16(dicom + position + 2));
+
+    vr = StringToValueRepresentation(std::string(dicom + position + 4, 2), true);
+    if (vr == ValueRepresentation_NotSupported)
+    {
+      return false;
+    }
+
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_7.html#sect_7.1.2
+    if (vr == ValueRepresentation_ApplicationEntity   /* AE */ ||
+        vr == ValueRepresentation_AgeString           /* AS */ ||
+        vr == ValueRepresentation_AttributeTag        /* AT */ ||
+        vr == ValueRepresentation_CodeString          /* CS */ ||
+        vr == ValueRepresentation_Date                /* DA */ ||
+        vr == ValueRepresentation_DecimalString       /* DS */ ||
+        vr == ValueRepresentation_DateTime            /* DT */ ||
+        vr == ValueRepresentation_FloatingPointSingle /* FL */ ||
+        vr == ValueRepresentation_FloatingPointDouble /* FD */ ||
+        vr == ValueRepresentation_IntegerString       /* IS */ ||
+        vr == ValueRepresentation_LongString          /* LO */ ||
+        vr == ValueRepresentation_LongText            /* LT */ ||
+        vr == ValueRepresentation_PersonName          /* PN */ ||
+        vr == ValueRepresentation_ShortString         /* SH */ ||
+        vr == ValueRepresentation_SignedLong          /* SL */ ||
+        vr == ValueRepresentation_SignedShort         /* SS */ ||
+        vr == ValueRepresentation_ShortText           /* ST */ ||
+        vr == ValueRepresentation_Time                /* TM */ ||
+        vr == ValueRepresentation_UniqueIdentifier    /* UI */ ||
+        vr == ValueRepresentation_UnsignedLong        /* UL */ ||
+        vr == ValueRepresentation_UnsignedShort       /* US */)
+    {
+      /**
+       * This is Table 7.1-2. "Data Element with Explicit VR of AE,
+       * AS, AT, CS, DA, DS, DT, FL, FD, IS, LO, LT, PN, SH, SL, SS,
+       * ST, TM, UI, UL and US"
+       **/
+      if (position + 8 > size)
+      {
+        return false;
+      }
+
+      uint16_t length = ReadLittleEndianUint16(dicom + position + 6);
+      if (position + 8 + length > size)
+      {
+        return false;
+      }
+
+      value.assign(dicom + position + 8, length);
+      position += (8 + length);
+    }
+    else
+    {
+      /**
+       * This is Table 7.1-1. "Data Element with Explicit VR other
+       * than as shown in Table 7.1-2"
+       **/
+      if (position + 12 > size)
+      {
+        return false;
+      }
+      
+      uint16_t reserved = ReadLittleEndianUint16(dicom + position + 6);
+      if (reserved != 0)
+      {
+        return false;
+      }
+
+      uint32_t length = ReadLittleEndianUint32(dicom + position + 8);
+      if (position + 12 + length > size)
+      {
+        return false;
+      }
+
+      value.assign(dicom + position + 12, length);
+      position += (12 + length);
+    }
+
+    if (!ValidateTag(vr, value))
+    {
+      return false;
+    }
+
+    RemoveTagPadding(value, vr);
+
+    return true;
+  }
+
+
+  bool DicomMap::IsDicomFile(const void* dicom,
+                             size_t size)
+  {
+    /**
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part10/chapter_7.html
+     * According to Table 7.1-1, besides the "DICM" DICOM prefix, the
+     * file preamble (i.e. dicom[0..127]) should not be taken into
+     * account to determine whether the file is or is not a DICOM file.
+     **/
+
+    const uint8_t* p = reinterpret_cast(dicom);
+
+    return (size >= 132 &&
+            p[128] == 'D' &&
+            p[129] == 'I' &&
+            p[130] == 'C' &&
+            p[131] == 'M');
+  }
+    
+
+  bool DicomMap::ParseDicomMetaInformation(DicomMap& result,
+                                           const void* dicom,
+                                           size_t size)
+  {
+    if (!IsDicomFile(dicom, size))
+    {
+      return false;
+    }
+
+
+    /**
+     * The DICOM File Meta Information must be encoded using the
+     * Explicit VR Little Endian Transfer Syntax
+     * (UID=1.2.840.10008.1.2.1).
+     **/
+
+    result.Clear();
+
+    // First, we read the "File Meta Information Group Length" tag
+    // (0002,0000) to know where to stop reading the meta header
+    size_t position = 132;
+
+    DicomTag tag(0x0000, 0x0000);  // Dummy initialization
+    ValueRepresentation vr;
+    std::string value;
+    if (!ReadNextTag(tag, vr, value, reinterpret_cast(dicom), size, position) ||
+        tag.GetGroup() != 0x0002 ||
+        tag.GetElement() != 0x0000 ||
+        vr != ValueRepresentation_UnsignedLong ||
+        value.size() != 4)
+    {
+      return false;
+    }
+
+    size_t stopPosition = position + ReadLittleEndianUint32(value.c_str());
+    if (stopPosition > size)
+    {
+      return false;
+    }
+
+    while (position < stopPosition)
+    {
+      if (ReadNextTag(tag, vr, value, reinterpret_cast(dicom), size, position))
+      {
+        result.SetValue(tag, value, IsBinaryValueRepresentation(vr));
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  static std::string ValueAsString(const DicomMap& summary,
+                                   const DicomTag& tag)
+  {
+    const DicomValue& value = summary.GetValue(tag);
+    if (value.IsNull())
+    {
+      return "(null)";
+    }
+    else
+    {
+      return value.GetContent();
+    }
+  }
+
+
+  std::string DicomMap::FormatMissingTagsForStore() const
+  {
+    std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
+    
+    if (HasTag(DICOM_TAG_PATIENT_ID))
+    {
+      patientId = ValueAsString(*this, DICOM_TAG_PATIENT_ID);
+    }
+
+    if (HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      studyInstanceUid = ValueAsString(*this, DICOM_TAG_STUDY_INSTANCE_UID);
+    }
+
+    if (HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      seriesInstanceUid = ValueAsString(*this, DICOM_TAG_SERIES_INSTANCE_UID);
+    }
+
+    if (HasTag(DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      sopInstanceUid = ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID);
+    }
+
+    return FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+  }
+
+  
+  std::string DicomMap::FormatMissingTagsForStore(const std::string& patientId,
+                                                  const std::string& studyInstanceUid,
+                                                  const std::string& seriesInstanceUid,
+                                                  const std::string& sopInstanceUid)
+  {
+    std::string s, t;
+
+    if (!patientId.empty())
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "PatientID=" + patientId;
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "PatientID";
+    }
+
+    if (!studyInstanceUid.empty())
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "StudyInstanceUID=" + studyInstanceUid;
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "StudyInstanceUID";
+    }
+
+    if (!seriesInstanceUid.empty())
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "SeriesInstanceUID=" + seriesInstanceUid;
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "SeriesInstanceUID";
+    }
+
+    if (!sopInstanceUid.empty())
+    {
+      if (t.size() > 0)
+        t += ", ";
+      t += "SOPInstanceUID=" + sopInstanceUid;
+    }
+    else
+    {
+      if (s.size() > 0)
+        s += ", ";
+      s += "SOPInstanceUID";
+    }
+
+    if (t.size() == 0)
+    {
+      return "Store has failed because all the required tags (" + s + ") are missing (is it a DICOMDIR file?)";
+    }
+    else
+    {
+      return "Store has failed because required tags (" + s + ") are missing for the following instance: " + t;
+    }
+  }
+
+
+  bool DicomMap::LookupStringValue(std::string& result,
+                                   const DicomTag& tag,
+                                   bool allowBinary) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->CopyToString(result, allowBinary);
+    }
+  }
+
+  bool DicomMap::LookupStringValues(std::set& results,
+                                    const DicomTag& tag,
+                                    bool allowBinary) const
+  {
+    std::string tmp;
+    if (LookupStringValue(tmp, tag, allowBinary))
+    {
+      Toolbox::SplitString(results, tmp, '\\');
+      return true;
+    }
+
+    return false;
+  }
+
+
+  bool DicomMap::ParseInteger32(int32_t& result,
+                                const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseInteger32(result);
+    }
+  }
+
+  bool DicomMap::ParseInteger64(int64_t& result,
+                                const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseInteger64(result);
+    }
+  }
+
+  bool DicomMap::ParseUnsignedInteger32(uint32_t& result,
+                                        const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseUnsignedInteger32(result);
+    }
+  }
+
+  bool DicomMap::ParseUnsignedInteger64(uint64_t& result,
+                                        const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseUnsignedInteger64(result);
+    }
+  }
+
+  bool DicomMap::ParseFloat(float& result,
+                            const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseFloat(result);
+    }
+  }
+
+  bool DicomMap::ParseFirstFloat(float& result,
+                                 const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseFirstFloat(result);
+    }
+  }
+
+  bool DicomMap::ParseDouble(double& result,
+                             const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseDouble(result);
+    }
+  }
+
+  
+  void DicomMap::FromDicomAsJson(const Json::Value& dicomAsJson, bool append, bool parseSequences)
+  {
+    if (dicomAsJson.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    
+    if (!append)
+    {
+      Clear();
+    }
+    
+    Json::Value::Members tags = dicomAsJson.getMemberNames();
+    for (Json::Value::Members::const_iterator
+           it = tags.begin(); it != tags.end(); ++it)
+    {
+      DicomTag tag(0, 0);
+      if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+
+      const Json::Value& value = dicomAsJson[*it];
+
+      if (value.type() != Json::objectValue ||
+          !value.isMember("Type") ||
+          !value.isMember("Value") ||
+          value["Type"].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+
+      if (value["Type"] == "String")
+      {
+        if (value["Value"].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_CorruptedFile);
+        }
+        else
+        {
+          SetValue(tag, value["Value"].asString(), false /* not binary */);
+        }
+      }
+      else if (value["Type"] == "Sequence" && parseSequences)
+      {
+        if (value["Value"].type() != Json::arrayValue)
+        {
+          throw OrthancException(ErrorCode_CorruptedFile);
+        }
+        else
+        {
+          SetSequenceValue(tag, value["Value"]);
+        }
+      }
+    }
+  }
+
+
+  void DicomMap::Merge(const DicomMap& other)
+  {
+    for (Content::const_iterator it = other.content_.begin();
+         it != other.content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (content_.find(it->first) == content_.end())
+      {
+        content_[it->first] = it->second->Clone();
+      }
+    }
+  }
+
+
+  void DicomMap::MergeMainDicomTags(const DicomMap& other,
+                                    ResourceType level)
+  {
+    std::set mainDicomTags;
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(mainDicomTags, level);
+
+    for (std::set::const_iterator itmt = mainDicomTags.begin();
+         itmt != mainDicomTags.end(); ++itmt)
+    {
+      Content::const_iterator found = other.content_.find(*itmt);
+
+      if (found != other.content_.end() &&
+          content_.find(*itmt) == content_.end())
+      {
+        assert(found->second != NULL);
+        content_[*itmt] = found->second->Clone();
+      }
+    }
+  }
+    
+
+  void DicomMap::ExtractMainDicomTags(const DicomMap& other)
+  {
+    Clear();
+    MergeMainDicomTags(other, ResourceType_Patient);
+    MergeMainDicomTags(other, ResourceType_Study);
+    MergeMainDicomTags(other, ResourceType_Series);
+    MergeMainDicomTags(other, ResourceType_Instance);
+  }    
+
+
+  bool DicomMap::HasOnlyMainDicomTags() const
+  {
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      if (!DicomMap::MainDicomTagsConfiguration::GetInstance().IsMainDicomTag(it->first))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  void DicomMap::ExtractSequences(DicomMap& result) const
+  {
+    result.Clear();
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      if (it->second->IsSequence())
+      {
+        result.SetSequenceValue(it->first, it->second->GetSequenceContent());
+      }
+    }
+  }
+
+  void DicomMap::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      
+      std::string tag = it->first.Format();
+
+      Json::Value value;
+      it->second->Serialize(value);
+
+      target[tag] = value;
+    }
+  }
+  
+
+  void DicomMap::Unserialize(const Json::Value& source)
+  {
+    Clear();
+
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members tags = source.getMemberNames();
+
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag(0, 0);
+      
+      if (!DicomTag::ParseHexadecimal(tag, tags[i].c_str()) ||
+          content_.find(tag) != content_.end())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      std::unique_ptr value(new DicomValue);
+      value->Unserialize(source[tags[i]]);
+
+      content_[tag] = value.release();
+    }
+  }
+
+
+  void DicomMap::FromDicomWeb(const Json::Value& source)
+  {
+    static const char* const ALPHABETIC = "Alphabetic";
+    static const char* const IDEOGRAPHIC = "Ideographic";
+    static const char* const INLINE_BINARY = "InlineBinary";
+    static const char* const PHONETIC = "Phonetic";
+    static const char* const VALUE = "Value";
+    static const char* const VR = "vr";
+  
+    Clear();
+
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  
+    Json::Value::Members tags = source.getMemberNames();
+
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      const Json::Value& item = source[tags[i]];
+      DicomTag tag(0, 0);
+
+      if (item.type() != Json::objectValue ||
+          !item.isMember(VR) ||
+          item[VR].type() != Json::stringValue ||
+          !DicomTag::ParseHexadecimal(tag, tags[i].c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      ValueRepresentation vr = StringToValueRepresentation(item[VR].asString(), false);
+
+      if (item.isMember(INLINE_BINARY))
+      {
+        const Json::Value& value = item[INLINE_BINARY];
+
+        if (value.type() == Json::stringValue)
+        {
+          std::string decoded;
+          Toolbox::DecodeBase64(decoded, value.asString());
+          SetValue(tag, decoded, true /* binary data */);
+        }
+      }
+      else if (!item.isMember(VALUE))
+      {
+        // Tag is present, but it has a null value
+        SetValue(tag, "", false /* not binary */);
+      }
+      else
+      {
+        const Json::Value& value = item[VALUE];
+
+        if (value.type() == Json::arrayValue)
+        {
+          bool supported = true;
+          
+          std::string s;
+          for (Json::Value::ArrayIndex j = 0; j < value.size() && supported; j++)
+          {
+            if (!s.empty())
+            {
+              s += '\\';
+            }
+
+            switch (value[j].type())
+            {
+              case Json::objectValue:
+                if (vr == ValueRepresentation_PersonName &&
+                    value[j].type() == Json::objectValue)
+                {
+                  if (value[j].isMember(ALPHABETIC) &&
+                      value[j][ALPHABETIC].type() == Json::stringValue)
+                  {
+                    s += value[j][ALPHABETIC].asString();
+                  }
+
+                  bool hasIdeographic = false;
+                  
+                  if (value[j].isMember(IDEOGRAPHIC) &&
+                      value[j][IDEOGRAPHIC].type() == Json::stringValue)
+                  {
+                    s += '=' + value[j][IDEOGRAPHIC].asString();
+                    hasIdeographic = true;
+                  }
+                  
+                  if (value[j].isMember(PHONETIC) &&
+                      value[j][PHONETIC].type() == Json::stringValue)
+                  {
+                    if (!hasIdeographic)
+                    {
+                      s += '=';
+                    }
+                      
+                    s += '=' + value[j][PHONETIC].asString();
+                  }
+                }
+                else
+                {
+                  // This is the case of sequences
+                  supported = false;
+                }
+
+                break;
+            
+              case Json::stringValue:
+                s += value[j].asString();
+                break;
+              
+              case Json::intValue:
+                s += boost::lexical_cast(value[j].asInt64());
+                break;
+              
+              case Json::uintValue:
+                s += boost::lexical_cast(value[j].asUInt64());
+                break;
+              
+              case Json::realValue:
+                s += boost::lexical_cast(value[j].asDouble());
+                break;
+              
+              default:
+                break;
+            }
+          }
+
+          if (supported)
+          {
+            SetValue(tag, s, false /* not binary */);
+          }
+        }
+      }
+    }
+  }
+
+
+  std::string DicomMap::GetStringValue(const DicomTag& tag,
+                                       const std::string& defaultValue,
+                                       bool allowBinary) const
+  {
+    std::string s;
+    if (LookupStringValue(s, tag, allowBinary))
+    {
+      return s;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  void DicomMap::RemoveBinaryTags()
+  {
+    Content kept;
+
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!it->second->IsBinary() &&
+          !it->second->IsNull())
+      {
+        kept[it->first] = it->second;
+      }
+      else
+      {
+        delete it->second;
+      }
+    }
+
+    content_ = kept;
+  }
+
+
+  void DicomMap::RemoveSequences()
+  {
+    Content kept;
+
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!it->second->IsSequence())
+      {
+        kept[it->first] = it->second;
+      }
+      else
+      {
+        delete it->second;
+      }
+    }
+
+    content_ = kept;
+  }
+
+  void DicomMap::DumpMainDicomTags(Json::Value& target,
+                                   ResourceType level) const
+  {
+    std::set mainDicomTags;
+    DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(mainDicomTags, level);
+    
+    target = Json::objectValue;
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      
+      if (!it->second->IsBinary() &&
+          !it->second->IsNull())
+      {
+        std::set::const_iterator found = mainDicomTags.find(it->first);
+
+        if (found != mainDicomTags.end())
+        {
+#if ORTHANC_ENABLE_DCMTK == 1
+          target[FromDcmtkBridge::GetTagName(*found, "")] = it->second->GetContent();
+#else
+          target[found->Format()] = it->second->GetContent();
+#endif
+        }
+      }
+    }    
+  }
+  
+
+  ValueRepresentation DicomMap::GuessPixelDataValueRepresentation(DicomTransferSyntax transferSyntax) const
+  {
+    const DicomValue* value = TestAndGetValue(DICOM_TAG_BITS_ALLOCATED);
+
+    uint32_t bitsAllocated;
+    if (value == NULL ||
+        !value->ParseUnsignedInteger32(bitsAllocated))
+    {
+      bitsAllocated = 8;
+    }
+
+    return DicomImageInformation::GuessPixelDataValueRepresentation(transferSyntax, bitsAllocated);
+  }
+  
+
+  void DicomMap::Print(FILE* fp) const
+  {
+    DicomArray a(*this);
+    a.Print(fp);
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomMap.h b/OrthancFramework/Sources/DicomFormat/DicomMap.h
new file mode 100644
index 0000000..f2dae88
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h
@@ -0,0 +1,244 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomTag.h"
+#include "DicomValue.h"
+#include "../Enumerations.h"
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomMap : public boost::noncopyable
+  {
+  public:
+    typedef std::map  Content;
+
+  private:
+    class MainDicomTagsConfiguration;
+    friend class DicomArray;
+    friend class FromDcmtkBridge;
+    friend class ParsedDicomFile;
+
+    Content content_;
+
+    // Warning: This takes the ownership of "value"
+    void SetValueInternal(uint16_t group, 
+                          uint16_t element, 
+                          DicomValue* value);
+
+  public:
+    ~DicomMap();
+
+    static void ResetDefaultMainDicomTags();
+
+    size_t GetSize() const;
+    
+    DicomMap* Clone() const;
+
+    void Assign(const DicomMap& other);
+
+    void Clear();
+
+    void SetNullValue(uint16_t group,
+                      uint16_t element);
+    
+    void SetNullValue(const DicomTag& tag);
+    
+    void SetValue(uint16_t group,
+                  uint16_t element,
+                  const DicomValue& value);
+
+    void SetValue(const DicomTag& tag,
+                  const DicomValue& value);
+
+    void SetValue(const DicomTag& tag,
+                  const std::string& str,
+                  bool isBinary);
+
+    void SetValue(uint16_t group,
+                  uint16_t element,
+                  const std::string& str,
+                  bool isBinary);
+
+    void SetSequenceValue(const DicomTag& tag,
+                          const Json::Value& value);
+
+    bool HasTag(uint16_t group, uint16_t element) const;
+
+    bool HasTag(const DicomTag& tag) const;
+
+    const DicomValue& GetValue(uint16_t group, uint16_t element) const;
+
+    const DicomValue& GetValue(const DicomTag& tag) const;
+
+    // DO NOT delete the returned value!
+    const DicomValue* TestAndGetValue(uint16_t group, uint16_t element) const;
+
+    // DO NOT delete the returned value!
+    const DicomValue* TestAndGetValue(const DicomTag& tag) const;
+
+    void Remove(const DicomTag& tag);
+
+    void RemoveTags(const std::set& tags);
+
+    void ExtractPatientInformation(DicomMap& result) const;
+
+    void ExtractStudyInformation(DicomMap& result) const;
+
+    void ExtractSeriesInformation(DicomMap& result) const;
+
+    void ExtractInstanceInformation(DicomMap& result) const;
+
+    void ExtractResourceInformation(DicomMap& result, ResourceType level) const;
+
+    void ExtractTags(DicomMap& result, const std::set& tags) const;
+
+    void ExtractSequences(DicomMap& result) const;
+
+    static void SetupFindPatientTemplate(DicomMap& result);
+
+    static void SetupFindStudyTemplate(DicomMap& result);
+
+    static void SetupFindSeriesTemplate(DicomMap& result);
+
+    static void SetupFindInstanceTemplate(DicomMap& result);
+
+    bool CopyTagIfExists(const DicomMap& source,
+                         const DicomTag& tag);
+
+    static bool IsMainDicomTag(const DicomTag& tag, ResourceType level);
+
+    static bool IsMainDicomTag(const DicomTag& tag);
+
+    static bool IsComputedTag(const DicomTag& tag, ResourceType level);
+
+    static bool IsComputedTag(const DicomTag& tag);
+
+    static bool HasOnlyComputedTags(const std::set& tags);
+
+    static bool HasComputedTags(const std::set& tags, ResourceType level);
+
+    static bool HasComputedTags(const std::set& tags);
+
+    static bool HasMetaInformationTags(const std::set& tags);
+
+    static void GetMainDicomTags(std::set& target,
+                                 ResourceType level);
+
+    // returns a string uniquely identifying the list of main dicom tags for a level
+    static std::string GetMainDicomTagsSignature(ResourceType level);
+
+    static std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level);
+
+    static void GetAllMainDicomTags(std::set& target);
+
+    // adds a main dicom tag to the definition of main dicom tags for each level.
+    // this should be done once at startup before you use MainDicomTags methods
+    static void AddMainDicomTag(const DicomTag& tag, ResourceType level);
+
+    void GetTags(std::set& tags) const;
+
+    static bool IsDicomFile(const void* dicom,
+                            size_t size);
+    
+    static bool ParseDicomMetaInformation(DicomMap& result,
+                                          const void* dicom,
+                                          size_t size);
+
+    std::string FormatMissingTagsForStore() const;
+
+    static std::string FormatMissingTagsForStore(const std::string& patientId,
+                                                 const std::string& studyInstanceUid,
+                                                 const std::string& seriesInstanceUid,
+                                                 const std::string& sopInstanceUid);
+
+    bool LookupStringValue(std::string& result,
+                           const DicomTag& tag,
+                           bool allowBinary) const;
+
+    bool LookupStringValues(std::set& results,
+                           const DicomTag& tag,
+                           bool allowBinary) const;
+
+    bool ParseInteger32(int32_t& result,
+                        const DicomTag& tag) const;
+
+    bool ParseInteger64(int64_t& result,
+                        const DicomTag& tag) const;                                
+
+    bool ParseUnsignedInteger32(uint32_t& result,
+                                const DicomTag& tag) const;
+
+    bool ParseUnsignedInteger64(uint64_t& result,
+                                const DicomTag& tag) const;
+
+    bool ParseFloat(float& result,
+                    const DicomTag& tag) const;
+
+    bool ParseFirstFloat(float& result,
+                         const DicomTag& tag) const;
+
+    bool ParseDouble(double& result,
+                     const DicomTag& tag) const;
+
+    void FromDicomAsJson(const Json::Value& dicomAsJson, 
+                         bool append = false,
+                         bool parseSequences = false);
+
+    void Merge(const DicomMap& other);
+
+    void MergeMainDicomTags(const DicomMap& other,
+                            ResourceType level);
+
+    void ExtractMainDicomTags(const DicomMap& other);
+
+    bool HasOnlyMainDicomTags() const;
+    
+    void Serialize(Json::Value& target) const;
+
+    void Unserialize(const Json::Value& source);
+
+    void FromDicomWeb(const Json::Value& source);
+
+    std::string GetStringValue(const DicomTag& tag,
+                               const std::string& defaultValue,
+                               bool allowBinary) const;
+
+    void RemoveBinaryTags();
+
+    void RemoveSequences();
+
+    void DumpMainDicomTags(Json::Value& target,
+                           ResourceType level) const;
+
+    ValueRepresentation GuessPixelDataValueRepresentation(DicomTransferSyntax transferSyntax) const;
+
+    void Print(FILE* fp) const;  // For debugging only
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomPath.cpp b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp
new file mode 100644
index 0000000..78a21e3
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.cpp
@@ -0,0 +1,415 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomPath.h"
+
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error Macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "../DicomParsing/FromDcmtkBridge.h"
+#endif
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  DicomPath::PrefixItem::PrefixItem(DicomTag tag,
+                                    bool isUniversal,
+                                    size_t index) :
+    tag_(tag),
+    isUniversal_(isUniversal),
+    index_(index)
+  {
+  }
+      
+
+  size_t DicomPath::PrefixItem::GetIndex() const
+  {
+    if (isUniversal_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return index_;
+    }
+  }
+
+  
+  void DicomPath::PrefixItem::SetIndex(size_t index)
+  {
+    isUniversal_ = false;
+    index_ = index;
+  }
+
+
+  DicomTag DicomPath::ParseTag(const std::string& token)
+  {
+    DicomTag tag(0,0);
+            
+    if (token[0] == '(' &&
+        token[token.size() - 1] == ')')
+    {
+      std::string hex = token.substr(1, token.size() - 2);
+      if (!DicomTag::ParseHexadecimal(tag, hex.c_str()))
+      {
+        throw OrthancException(ErrorCode_UnknownDicomTag, "Cannot parse tag: " + token);
+      }
+    }
+    else
+    {
+#if ORTHANC_ENABLE_DCMTK == 1
+      tag = FromDcmtkBridge::ParseTag(token);
+#else
+      if (!DicomTag::ParseHexadecimal(tag, token.c_str()))
+      {
+        throw OrthancException(ErrorCode_UnknownDicomTag, "Cannot parse tag without DCMTK: " + token);
+      }
+#endif
+    }
+
+    return tag;
+  }
+
+
+  const DicomPath::PrefixItem& DicomPath::GetLevel(size_t i) const
+  {
+    if (i >= prefix_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return prefix_[i];
+    }
+  }
+
+
+  DicomPath::DicomPath(const Orthanc::DicomTag& tag) :
+    finalTag_(tag)
+  {
+  }
+
+
+  DicomPath::DicomPath(const Orthanc::DicomTag& sequence,
+                       size_t index,
+                       const Orthanc::DicomTag& tag) :
+    finalTag_(tag)
+  {
+    AddIndexedTagToPrefix(sequence, index);
+  }
+
+  
+  DicomPath::DicomPath(const Orthanc::DicomTag& sequence1,
+                       size_t index1,
+                       const Orthanc::DicomTag& sequence2,
+                       size_t index2,
+                       const Orthanc::DicomTag& tag) :
+    finalTag_(tag)
+  {
+    AddIndexedTagToPrefix(sequence1, index1);
+    AddIndexedTagToPrefix(sequence2, index2);
+  }
+
+
+  DicomPath::DicomPath(const Orthanc::DicomTag& sequence1,
+                       size_t index1,
+                       const Orthanc::DicomTag& sequence2,
+                       size_t index2,
+                       const Orthanc::DicomTag& sequence3,
+                       size_t index3,
+                       const Orthanc::DicomTag& tag) :
+    finalTag_(tag)
+  {
+    AddIndexedTagToPrefix(sequence1, index1);
+    AddIndexedTagToPrefix(sequence2, index2);
+    AddIndexedTagToPrefix(sequence3, index3);
+  }
+
+
+  DicomPath::DicomPath(const std::vector& parentTags,
+                       const std::vector& parentIndexes,
+                       const Orthanc::DicomTag& finalTag) :
+    finalTag_(finalTag)
+  {
+    if (parentTags.size() != parentIndexes.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      prefix_.reserve(parentTags.size());
+
+      for (size_t i = 0; i < parentTags.size(); i++)
+      {
+        prefix_.push_back(PrefixItem::CreateIndexed(parentTags[i], parentIndexes[i]));        
+      }
+    }
+  }
+
+
+  void DicomPath::AddIndexedTagToPrefix(const Orthanc::DicomTag& tag,
+                                        size_t index)
+  {
+    prefix_.push_back(PrefixItem::CreateIndexed(tag, index));
+  }
+
+
+  void DicomPath::AddUniversalTagToPrefix(const Orthanc::DicomTag& tag)
+  {
+    prefix_.push_back(PrefixItem::CreateUniversal(tag));
+  }
+  
+
+  size_t DicomPath::GetPrefixLength() const
+  {
+    return prefix_.size();
+  }
+  
+
+  const Orthanc::DicomTag& DicomPath::GetFinalTag() const
+  {
+    return finalTag_;
+  }
+
+  
+  const Orthanc::DicomTag& DicomPath::GetPrefixTag(size_t level) const
+  {
+    return GetLevel(level).GetTag();
+  }
+
+  
+  bool DicomPath::IsPrefixUniversal(size_t level) const
+  {
+    return GetLevel(level).IsUniversal();
+  }
+  
+
+  size_t DicomPath::GetPrefixIndex(size_t level) const
+  {
+    return GetLevel(level).GetIndex();
+  }
+
+
+  bool DicomPath::HasUniversal() const
+  {
+    for (size_t i = 0; i < prefix_.size(); i++)
+    {
+      if (prefix_[i].IsUniversal())
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void DicomPath::SetPrefixIndex(size_t level,
+                                 size_t index)
+  {
+    if (level >= prefix_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      prefix_[level].SetIndex(index);
+    }
+  }
+
+
+  std::string DicomPath::Format() const
+  {
+    std::string s;
+
+    for (size_t i = 0; i < prefix_.size(); i++)
+    {
+      s += "(" + prefix_[i].GetTag().Format() + ")";
+
+      if (prefix_[i].IsUniversal())
+      {
+        s += "[*].";
+      }
+      else
+      {
+        s += "[" + boost::lexical_cast(prefix_[i].GetIndex()) + "].";
+      }
+    }
+
+    return s + "(" + finalTag_.Format() + ")";
+  }
+
+  
+  DicomPath DicomPath::Parse(const std::string& s)
+  {
+    std::vector tokens;
+    Toolbox::TokenizeString(tokens, s, '.');
+
+    if (tokens.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Empty path to DICOM tags");
+    }
+
+    const DicomTag finalTag = ParseTag(Toolbox::StripSpaces(tokens[tokens.size() - 1]));
+
+    DicomPath path(finalTag);
+
+    for (size_t i = 0; i < tokens.size() - 1; i++)
+    {
+      size_t pos = tokens[i].find('[');
+      if (pos == std::string::npos)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "Parent path doesn't contain an index");
+      }
+      else
+      {
+        const std::string left = Orthanc::Toolbox::StripSpaces(tokens[i].substr(0, pos));
+        const std::string right = Orthanc::Toolbox::StripSpaces(tokens[i].substr(pos + 1));
+
+        if (left.empty())
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange, "Parent path doesn't contain a tag");
+        }            
+        else if (right.empty() ||
+                 right[right.size() - 1] != ']')
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange, "Parent path doesn't contain the end of the index");
+        }
+        else
+        {
+          DicomTag tag = ParseTag(left);
+
+          try
+          {
+            std::string t = Toolbox::StripSpaces(right.substr(0, right.size() - 1));
+            if (t == "*")
+            {
+              path.AddUniversalTagToPrefix(tag);
+            }
+            else
+            {
+              int index = boost::lexical_cast(t);
+              if (index < 0)
+              {
+                throw OrthancException(ErrorCode_ParameterOutOfRange, "Negative index in parent path: " + t);
+              }
+              else
+              {
+                path.AddIndexedTagToPrefix(tag, static_cast(index));
+              }
+            }
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid index in parent path: [" + right);
+          }
+        }
+      }
+    }
+
+    return path;
+  }
+
+
+  bool DicomPath::IsMatch(const DicomPath& pattern,
+                          const DicomPath& path)
+  {
+    if (path.HasUniversal())
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else if (path.GetPrefixLength() < pattern.GetPrefixLength())
+    {
+      return false;
+    }
+    else
+    {
+      for (size_t i = 0; i < pattern.GetPrefixLength(); i++)
+      {
+        if (path.GetPrefixTag(i) != pattern.GetPrefixTag(i) ||
+            (!pattern.IsPrefixUniversal(i) &&
+             path.GetPrefixIndex(i) != pattern.GetPrefixIndex(i)))
+        {
+          return false;
+        }
+      }
+
+      if (path.GetPrefixLength() == pattern.GetPrefixLength())
+      {
+        return (path.GetFinalTag() == pattern.GetFinalTag());
+      }
+      else
+      {
+        return (path.GetPrefixTag(pattern.GetPrefixLength()) == pattern.GetFinalTag());
+      }
+    }
+  }
+
+
+  bool DicomPath::IsMatch(const DicomPath& pattern,
+                          const std::vector& prefixTags,
+                          const std::vector& prefixIndexes,
+                          const DicomTag& finalTag)
+  {
+    if (prefixTags.size() != prefixIndexes.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    if (prefixTags.size() < pattern.GetPrefixLength())
+    {
+      return false;
+    }
+    else
+    {
+      for (size_t i = 0; i < pattern.GetPrefixLength(); i++)
+      {
+        if (prefixTags[i] != pattern.GetPrefixTag(i) ||
+            (!pattern.IsPrefixUniversal(i) &&
+             prefixIndexes[i] != pattern.GetPrefixIndex(i)))
+        {
+          return false;
+        }
+      }
+
+      if (prefixTags.size() == pattern.GetPrefixLength())
+      {
+        return (finalTag == pattern.GetFinalTag());
+      }
+      else
+      {
+        return (prefixTags[pattern.GetPrefixLength()] == pattern.GetFinalTag());
+      }
+    }
+  }    
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomPath.h b/OrthancFramework/Sources/DicomFormat/DicomPath.h
new file mode 100644
index 0000000..d8fb7d1
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomPath.h
@@ -0,0 +1,141 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+#include "../DicomFormat/DicomTag.h"
+
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomPath
+  {
+  private:
+    class PrefixItem
+    {
+    private:
+      DicomTag  tag_;
+      bool      isUniversal_;  // Matches any index
+      size_t    index_;
+
+      PrefixItem(DicomTag tag,
+                 bool isUniversal,
+                 size_t index);
+      
+    public:
+      static PrefixItem CreateUniversal(const DicomTag& tag)
+      {
+        return PrefixItem(tag, true, 0 /* dummy value */);
+      }
+
+      static PrefixItem CreateIndexed(const DicomTag& tag,
+                                      size_t index)
+      {
+        return PrefixItem(tag, false, index);
+      }
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      bool IsUniversal() const
+      {
+        return isUniversal_;
+      }
+
+      size_t GetIndex() const;
+
+      void SetIndex(size_t index);
+    };
+
+    std::vector  prefix_;
+    Orthanc::DicomTag        finalTag_;
+
+    static DicomTag ParseTag(const std::string& token);
+    
+    const PrefixItem& GetLevel(size_t i) const;
+    
+  public:
+    explicit DicomPath(const Orthanc::DicomTag& tag);
+
+    DicomPath(const Orthanc::DicomTag& sequence,
+              size_t index,
+              const Orthanc::DicomTag& tag);
+
+    DicomPath(const Orthanc::DicomTag& sequence1,
+              size_t index1,
+              const Orthanc::DicomTag& sequence2,
+              size_t index2,
+              const Orthanc::DicomTag& tag);
+
+    DicomPath(const Orthanc::DicomTag& sequence1,
+              size_t index1,
+              const Orthanc::DicomTag& sequence2,
+              size_t index2,
+              const Orthanc::DicomTag& sequence3,
+              size_t index3,
+              const Orthanc::DicomTag& tag);
+
+    DicomPath(const std::vector& parentTags,
+              const std::vector& parentIndexes,
+              const Orthanc::DicomTag& finalTag);
+
+    void AddIndexedTagToPrefix(const Orthanc::DicomTag& tag,
+                               size_t index);
+
+    void AddUniversalTagToPrefix(const Orthanc::DicomTag& tag);
+
+    size_t GetPrefixLength() const;
+
+    const Orthanc::DicomTag& GetFinalTag() const;
+
+    const Orthanc::DicomTag& GetPrefixTag(size_t level) const;
+
+    bool IsPrefixUniversal(size_t level) const;
+
+    size_t GetPrefixIndex(size_t level) const;
+
+    bool HasUniversal() const;
+
+    // This method is used for an optimization in Stone
+    // (cf. "DicomStructureSet.cpp")
+    void SetPrefixIndex(size_t level,
+                        size_t index);
+
+    std::string Format() const;
+
+    static DicomPath Parse(const std::string& s);
+
+    static bool IsMatch(const DicomPath& pattern,
+                        const DicomPath& path);
+
+    static bool IsMatch(const DicomPath& pattern,
+                        const std::vector& prefixTags,
+                        const std::vector& prefixIndexes,
+                        const DicomTag& finalTag);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp
new file mode 100644
index 0000000..9d2daeb
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp
@@ -0,0 +1,743 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomStreamReader.h"
+
+#include "../OrthancException.h"
+
+#include 
+#include 
+#include 
+#include 
+
+
+#include 
+
+namespace Orthanc
+{
+  static bool IsNormalizationNeeded(const std::string& source,
+                                    ValueRepresentation vr)
+  {
+    return (!source.empty() &&
+            (source[source.size() - 1] == ' ' ||
+             source[source.size() - 1] == '\0') &&
+            // Normalization only applies to string-based VR
+            (vr == ValueRepresentation_ApplicationEntity ||
+             vr == ValueRepresentation_AgeString ||
+             vr == ValueRepresentation_CodeString ||
+             vr == ValueRepresentation_DecimalString ||
+             vr == ValueRepresentation_IntegerString ||
+             vr == ValueRepresentation_LongString ||
+             vr == ValueRepresentation_LongText ||
+             vr == ValueRepresentation_PersonName ||
+             vr == ValueRepresentation_ShortString ||
+             vr == ValueRepresentation_ShortText ||
+             vr == ValueRepresentation_UniqueIdentifier ||
+             vr == ValueRepresentation_UnlimitedText));
+  }
+
+  
+  static void NormalizeValue(std::string& inplace,
+                             ValueRepresentation vr)
+  {
+    if (IsNormalizationNeeded(inplace, vr))
+    {
+      assert(!inplace.empty());
+      inplace.resize(inplace.size() - 1);
+    }
+  }
+
+    
+  static uint16_t ReadUnsignedInteger16(const char* dicom,
+                                        bool littleEndian)
+  {
+    const uint8_t* p = reinterpret_cast(dicom);
+
+    if (littleEndian)
+    {
+      return (static_cast(p[0]) |
+              (static_cast(p[1]) << 8));
+    }
+    else
+    {
+      return (static_cast(p[1]) |
+              (static_cast(p[0]) << 8));
+    }
+  }
+
+
+  static uint32_t ReadUnsignedInteger32(const char* dicom,
+                                        bool littleEndian)
+  {
+    const uint8_t* p = reinterpret_cast(dicom);
+
+    if (littleEndian)
+    {
+      return (static_cast(p[0]) |
+              (static_cast(p[1]) << 8) |
+              (static_cast(p[2]) << 16) |
+              (static_cast(p[3]) << 24));
+    }
+    else
+    {
+      return (static_cast(p[3]) |
+              (static_cast(p[2]) << 8) |
+              (static_cast(p[1]) << 16) |
+              (static_cast(p[0]) << 24));
+    }
+  }
+
+
+  static DicomTag ReadTag(const char* dicom,
+                          bool littleEndian)
+  {
+    return DicomTag(ReadUnsignedInteger16(dicom, littleEndian),
+                    ReadUnsignedInteger16(dicom + 2, littleEndian));
+  }
+
+
+  static bool IsShortExplicitTag(ValueRepresentation vr)
+  {
+    /**
+     * Are we in the case of Table 7.1-2? "Data Element with
+     * Explicit VR of AE, AS, AT, CS, DA, DS, DT, FL, FD, IS, LO,
+     * LT, PN, SH, SL, SS, ST, TM, UI, UL and US"
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_7.html#sect_7.1.2
+     **/
+    return (vr == ValueRepresentation_ApplicationEntity   /* AE */ ||
+            vr == ValueRepresentation_AgeString           /* AS */ ||
+            vr == ValueRepresentation_AttributeTag        /* AT */ ||
+            vr == ValueRepresentation_CodeString          /* CS */ ||
+            vr == ValueRepresentation_Date                /* DA */ ||
+            vr == ValueRepresentation_DecimalString       /* DS */ ||
+            vr == ValueRepresentation_DateTime            /* DT */ ||
+            vr == ValueRepresentation_FloatingPointSingle /* FL */ ||
+            vr == ValueRepresentation_FloatingPointDouble /* FD */ ||
+            vr == ValueRepresentation_IntegerString       /* IS */ ||
+            vr == ValueRepresentation_LongString          /* LO */ ||
+            vr == ValueRepresentation_LongText            /* LT */ ||
+            vr == ValueRepresentation_PersonName          /* PN */ ||
+            vr == ValueRepresentation_ShortString         /* SH */ ||
+            vr == ValueRepresentation_SignedLong          /* SL */ ||
+            vr == ValueRepresentation_SignedShort         /* SS */ ||
+            vr == ValueRepresentation_ShortText           /* ST */ ||
+            vr == ValueRepresentation_Time                /* TM */ ||
+            vr == ValueRepresentation_UniqueIdentifier    /* UI */ ||
+            vr == ValueRepresentation_UnsignedLong        /* UL */ ||
+            vr == ValueRepresentation_UnsignedShort       /* US */);
+  }
+
+
+  bool DicomStreamReader::IsLittleEndian() const
+  {
+    return (transferSyntax_ != DicomTransferSyntax_BigEndianExplicit);
+  }
+
+
+  void DicomStreamReader::HandlePreamble(IVisitor& visitor,
+                                         const std::string& block)
+  {
+    assert(block.size() == 144u);
+    assert(reader_.GetProcessedBytes() == 144u);
+
+    /**
+     * The "DICOM file meta information" is always encoded using
+     * "Explicit VR Little Endian Transfer Syntax"
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part10/chapter_7.html
+     **/
+    if (block[128] != 'D' ||
+        block[129] != 'I' ||
+        block[130] != 'C' ||
+        block[131] != 'M' ||
+        ReadTag(block.c_str() + 132, true) != DicomTag(0x0002, 0x0000) ||
+        block[136] != 'U' ||
+        block[137] != 'L' ||
+        ReadUnsignedInteger16(block.c_str() + 138, true) != 4)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    uint32_t length = ReadUnsignedInteger32(block.c_str() + 140, true);
+
+    reader_.Schedule(length);
+    state_ = State_MetaHeader;
+  }
+
+
+  void DicomStreamReader::HandleMetaHeader(IVisitor& visitor,
+                                           const std::string& block)
+  {
+    size_t pos = 0;
+    const char* p = block.c_str();
+
+    bool hasTransferSyntax = false;
+
+    while (pos + 8 <= block.size())
+    {
+      DicomTag tag = ReadTag(p + pos, true);
+        
+      ValueRepresentation vr = StringToValueRepresentation(std::string(p + pos + 4, 2), true);
+
+      if (IsShortExplicitTag(vr))
+      {
+        uint16_t length = ReadUnsignedInteger16(p + pos + 6, true);
+
+        std::string value;
+        value.assign(p + pos + 8, length);
+        NormalizeValue(value, vr);
+
+        if (tag.GetGroup() == 0x0002)
+        {
+          visitor.VisitMetaHeaderTag(tag, vr, value);
+        }                  
+
+        if (tag == DICOM_TAG_TRANSFER_SYNTAX_UID)
+        {
+          if (LookupTransferSyntax(transferSyntax_, value))
+          {
+            hasTransferSyntax = true;
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_NotImplemented, "Unsupported transfer syntax: " + value);
+          }
+        }
+          
+        pos += length + 8;
+      }
+      else if (pos + 12 <= block.size())
+      {
+        uint16_t reserved = ReadUnsignedInteger16(p + pos + 6, true);
+        if (reserved != 0)
+        {
+          break;
+        }
+          
+        uint32_t length = ReadUnsignedInteger32(p + pos + 8, true);
+
+        if (tag.GetGroup() == 0x0002)
+        {
+          std::string value;
+          value.assign(p + pos + 12, length);
+          NormalizeValue(value, vr);
+          visitor.VisitMetaHeaderTag(tag, vr, value);
+        }                  
+          
+        pos += length + 12;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Invalid DICOM File: Unable to parse Meta Header");
+      }
+    }
+
+    if (pos != block.size())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    if (!hasTransferSyntax)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "DICOM file meta-header without transfer syntax UID");
+    }
+
+    visitor.VisitTransferSyntax(transferSyntax_);
+
+    reader_.Schedule(8);
+    state_ = State_DatasetTag;
+  }
+    
+
+  void DicomStreamReader::HandleDatasetTag(const std::string& block,
+                                           const DicomTag& untilTag)
+  {
+    static const DicomTag DICOM_TAG_SEQUENCE_ITEM(0xfffe, 0xe000);
+    static const DicomTag DICOM_TAG_SEQUENCE_DELIMITATION_ITEM(0xfffe, 0xe00d);
+    static const DicomTag DICOM_TAG_SEQUENCE_DELIMITATION_SEQUENCE(0xfffe, 0xe0dd);
+
+    assert(block.size() == 8u);
+
+    const bool littleEndian = IsLittleEndian();
+    DicomTag tag = ReadTag(block.c_str(), littleEndian);
+
+    if (sequenceDepth_ == 0 &&
+        tag >= untilTag)
+    {
+      state_ = State_Done;
+      return;
+    }
+      
+    if (tag == DICOM_TAG_SEQUENCE_ITEM ||
+        tag == DICOM_TAG_SEQUENCE_DELIMITATION_ITEM ||
+        tag == DICOM_TAG_SEQUENCE_DELIMITATION_SEQUENCE)
+    {
+      // The special sequence items are encoded like "Implicit VR"
+      uint32_t length = ReadUnsignedInteger32(block.c_str() + 4, littleEndian);
+
+      if (tag == DICOM_TAG_SEQUENCE_ITEM)
+      {
+        if (length == 0xffffffffu)
+        {
+          // Undefined length: Need to loop over the tags of the nested dataset
+          reader_.Schedule(8);
+          state_ = State_DatasetTag;
+        }
+        else
+        {
+          // Explicit length: Can skip the full sequence at once
+          reader_.Schedule(length);
+          state_ = State_DatasetValue;
+        }
+      }
+      else if (tag == DICOM_TAG_SEQUENCE_DELIMITATION_ITEM ||
+               tag == DICOM_TAG_SEQUENCE_DELIMITATION_SEQUENCE)
+      {
+        if (length != 0 ||
+            sequenceDepth_ == 0)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+
+        if (tag == DICOM_TAG_SEQUENCE_DELIMITATION_SEQUENCE)
+        {
+          sequenceDepth_ --;
+        }
+
+        reader_.Schedule(8);
+        state_ = State_DatasetTag;          
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      assert(reader_.GetProcessedBytes() >= block.size());
+      const uint64_t tagOffset = reader_.GetProcessedBytes() - block.size();
+        
+      ValueRepresentation vr = ValueRepresentation_Unknown;
+        
+      if (transferSyntax_ == DicomTransferSyntax_LittleEndianImplicit)
+      {
+        if (sequenceDepth_ == 0)
+        {
+          danglingTag_ = tag;
+          danglingVR_ = vr;
+          danglingOffset_ = tagOffset;
+        }
+
+        uint32_t length = ReadUnsignedInteger32(block.c_str() + 4, true /* little endian */);
+        HandleDatasetExplicitLength(length);
+      }
+      else
+      {
+        // This in an explicit transfer syntax
+
+        vr = StringToValueRepresentation(
+          std::string(block.c_str() + 4, 2), false /* ignore unknown VR */);
+
+        if (vr == ValueRepresentation_Sequence)
+        {
+          sequenceDepth_ ++;
+          reader_.Schedule(4);
+          state_ = State_SequenceExplicitLength;
+        }
+        else if (IsShortExplicitTag(vr))
+        {
+          uint16_t length = ReadUnsignedInteger16(block.c_str() + 6, littleEndian);
+
+          reader_.Schedule(length);
+          state_ = State_DatasetValue;
+        }
+        else
+        {
+          uint16_t reserved = ReadUnsignedInteger16(block.c_str() + 6, littleEndian);
+          if (reserved != 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+
+          reader_.Schedule(4);
+          state_ = State_DatasetExplicitLength;
+        }
+
+        if (sequenceDepth_ == 0)
+        {
+          danglingTag_ = tag;
+          danglingVR_ = vr;
+          danglingOffset_ = tagOffset;
+        }
+      }
+    }
+  }
+
+
+  void DicomStreamReader::HandleDatasetExplicitLength(uint32_t length)
+  {
+    if (length == 0xffffffffu)
+    {
+      /**
+       * This is the case of pixel data with compressed transfer
+       * syntaxes. Schedule the reading of the first tag of the
+       * nested dataset.
+       * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.5.html
+       **/
+      state_ = State_DatasetTag;
+      reader_.Schedule(8);
+      sequenceDepth_ ++;
+    }
+    else
+    {
+      reader_.Schedule(length);
+      state_ = State_DatasetValue;
+    }
+  }    
+
+    
+  void DicomStreamReader::HandleDatasetExplicitLength(IVisitor& visitor,
+                                                      const std::string& block)
+  {
+    assert(block.size() == 4);
+
+    uint32_t length = ReadUnsignedInteger32(block.c_str(), IsLittleEndian());
+    HandleDatasetExplicitLength(length);
+
+    std::string empty;
+    if (!visitor.VisitDatasetTag(danglingTag_, danglingVR_, empty, IsLittleEndian(), danglingOffset_))
+    {
+      state_ = State_Done;
+    }
+  }
+    
+
+  void DicomStreamReader::HandleSequenceExplicitLength(const std::string& block)
+  {
+    assert(block.size() == 4);
+
+    uint32_t length = ReadUnsignedInteger32(block.c_str(), IsLittleEndian());
+    if (length == 0xffffffffu)
+    {
+      state_ = State_DatasetTag;
+      reader_.Schedule(8);
+    }
+    else
+    {
+      reader_.Schedule(length);
+      state_ = State_SequenceExplicitValue;
+    }
+  }
+
+    
+  void DicomStreamReader::HandleSequenceExplicitValue()
+  {
+    if (sequenceDepth_ == 0)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    sequenceDepth_ --;
+
+    state_ = State_DatasetTag;
+    reader_.Schedule(8);
+  }
+
+
+  void DicomStreamReader::HandleDatasetValue(IVisitor& visitor,
+                                             const std::string& block)
+  {
+    if (sequenceDepth_ == 0)
+    {
+      bool c;
+
+      if (IsNormalizationNeeded(block, danglingVR_))
+      {
+        std::string s(block.begin(), block.end() - 1);
+        c = visitor.VisitDatasetTag(danglingTag_, danglingVR_, s, IsLittleEndian(), danglingOffset_);
+      }
+      else
+      {
+        c = visitor.VisitDatasetTag(danglingTag_, danglingVR_, block, IsLittleEndian(), danglingOffset_);
+      }
+      
+      if (!c)
+      {
+        state_ = State_Done;
+        return;
+      }
+    }
+
+    reader_.Schedule(8);
+    state_ = State_DatasetTag;
+  }
+    
+    
+  DicomStreamReader::DicomStreamReader(std::istream& stream) :
+    reader_(stream),
+    state_(State_Preamble),
+    transferSyntax_(DicomTransferSyntax_LittleEndianImplicit),  // Dummy
+    danglingTag_(0x0000, 0x0000),  // Dummy
+    danglingVR_(ValueRepresentation_Unknown),  // Dummy
+    danglingOffset_(0),  // Dummy
+    sequenceDepth_(0)
+  {
+    reader_.Schedule(128 /* empty header */ +
+                     4 /* "DICM" magic value */ +
+                     4 /* (0x0002, 0x0000) tag */ +
+                     2 /* value representation of (0x0002, 0x0000) == "UL" */ +
+                     2 /* length of "UL" value == 4 */ +
+                     4 /* actual length of the meta-header */);
+  }
+
+  
+  void DicomStreamReader::Consume(IVisitor& visitor,
+                                  const DicomTag& untilTag)
+  {
+    while (state_ != State_Done)
+    {
+      std::string block;
+      if (reader_.Read(block))
+      {
+        switch (state_)
+        {
+          case State_Preamble:
+            HandlePreamble(visitor, block);
+            break;
+
+          case State_MetaHeader:
+            HandleMetaHeader(visitor, block);
+            break;
+
+          case State_DatasetTag:
+            HandleDatasetTag(block, untilTag);
+            break;
+
+          case State_DatasetExplicitLength:
+            HandleDatasetExplicitLength(visitor, block);
+            break;
+
+          case State_SequenceExplicitLength:
+            HandleSequenceExplicitLength(block);
+            break;
+
+          case State_SequenceExplicitValue:
+            HandleSequenceExplicitValue();
+            break;
+
+          case State_DatasetValue:
+            HandleDatasetValue(visitor, block);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+      else
+      {
+        return;  // No more data in the stream
+      }
+    }
+  }
+
+
+  void DicomStreamReader::Consume(IVisitor& visitor)
+  {
+    DicomTag untilTag(0xffff, 0xffff);
+    Consume(visitor, untilTag);
+  }
+
+
+  bool DicomStreamReader::IsDone() const
+  {
+    return (state_ == State_Done);
+  }
+
+  
+  uint64_t DicomStreamReader::GetProcessedBytes() const
+  {
+    return reader_.GetProcessedBytes();
+  }
+
+
+  class DicomStreamReader::PixelDataVisitor : public DicomStreamReader::IVisitor
+  {
+  private:
+    bool                 hasPixelData_;
+    uint64_t             pixelDataOffset_;
+    ValueRepresentation  pixelDataVR_;
+    DicomTransferSyntax  transferSyntax_;
+    
+  public:
+    PixelDataVisitor() :
+      hasPixelData_(false),
+      pixelDataOffset_(0),
+      pixelDataVR_(ValueRepresentation_Unknown),
+      transferSyntax_(DicomTransferSyntax_LittleEndianImplicit) // Default DICOM transfer syntax
+    {
+    }
+    
+    virtual void VisitMetaHeaderTag(const DicomTag& tag,
+                                    const ValueRepresentation& vr,
+                                    const std::string& value) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void VisitTransferSyntax(DicomTransferSyntax transferSyntax) ORTHANC_OVERRIDE
+    {
+      transferSyntax_ = transferSyntax;
+    }
+    
+    virtual bool VisitDatasetTag(const DicomTag& tag,
+                                 const ValueRepresentation& vr,
+                                 const std::string& value,
+                                 bool isLittleEndian,
+                                 uint64_t fileOffset) ORTHANC_OVERRIDE
+    {
+      if (tag == DICOM_TAG_PIXEL_DATA)
+      {
+        hasPixelData_ = true;
+        pixelDataOffset_ = fileOffset;
+
+        if (transferSyntax_ == DicomTransferSyntax_LittleEndianImplicit)
+        {
+          // Implicit Little Endian has always "OW" VR for pixel data
+          // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html
+          pixelDataVR_ = ValueRepresentation_OtherWord;
+        }
+        else if (transferSyntax_ == DicomTransferSyntax_LittleEndianExplicit ||
+                 transferSyntax_ == DicomTransferSyntax_BigEndianExplicit)
+        {
+          pixelDataVR_ = vr;
+        }
+        else
+        {
+          // Compressed transfer syntaxes must always be OB
+          pixelDataVR_ = ValueRepresentation_OtherByte;
+        }
+      }
+
+      // Stop processing once pixel data has been passed
+      return (tag < DICOM_TAG_PIXEL_DATA);
+    }
+
+    bool HasPixelData() const
+    {
+      return hasPixelData_;
+    }
+
+    uint64_t GetPixelDataOffset() const
+    {
+      return pixelDataOffset_;
+    }
+
+    ValueRepresentation GetPixelDataVR() const
+    {
+      return pixelDataVR_;
+    }
+
+    static bool LookupPixelDataOffset(uint64_t& offset,
+                                      ValueRepresentation& vr,
+                                      std::istream& stream)
+    {
+      PixelDataVisitor visitor;
+      bool isLittleEndian;
+
+      {
+        DicomStreamReader reader(stream);
+
+        try
+        {
+          reader.Consume(visitor);
+          isLittleEndian = reader.IsLittleEndian();
+        }
+        catch (OrthancException&)
+        {
+          // Invalid DICOM file
+          return false;
+        }
+      }
+
+      if (visitor.HasPixelData())
+      {
+        // Sanity check if we face an unsupported DICOM file: Make
+        // sure that we can read DICOM_TAG_PIXEL_DATA at the reported
+        // position in the stream
+        stream.seekg(visitor.GetPixelDataOffset(), stream.beg);
+        
+        std::string s;
+        s.resize(4);
+        stream.read(&s[0], s.size());
+
+        if (!isLittleEndian)
+        {
+          // Byte swapping if reading a file whose transfer syntax is
+          // 1.2.840.10008.1.2.2 (big endian explicit)
+          std::swap(s[0], s[1]);
+          std::swap(s[2], s[3]);          
+        }
+        
+        if (stream.gcount() == static_cast(s.size()) &&
+            s[0] == char(0xe0) &&
+            s[1] == char(0x7f) &&
+            s[2] == char(0x10) &&
+            s[3] == char(0x00))
+        {
+          offset = visitor.GetPixelDataOffset();
+          vr = visitor.GetPixelDataVR();
+          return true;
+        }
+        else
+        {
+          return false;
+        }
+      }
+      else
+      {
+        return false;
+      }
+    }
+  };
+
+  
+  bool DicomStreamReader::LookupPixelDataOffset(uint64_t& offset,
+                                                ValueRepresentation& vr,
+                                                const std::string& dicom)
+  {
+    std::stringstream stream(dicom);
+    return PixelDataVisitor::LookupPixelDataOffset(offset, vr, stream);
+  }
+  
+
+  bool DicomStreamReader::LookupPixelDataOffset(uint64_t& offset,
+                                                ValueRepresentation& vr,
+                                                const void* buffer,
+                                                size_t size)
+  {
+    boost::iostreams::array_source source(reinterpret_cast(buffer), size);
+    boost::iostreams::stream stream(source);
+    return PixelDataVisitor::LookupPixelDataOffset(offset, vr, stream);
+  }
+}
+
diff --git a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h
new file mode 100644
index 0000000..1af2d65
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h
@@ -0,0 +1,141 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomTag.h"
+#include "StreamBlockReader.h"
+
+namespace Orthanc
+{
+  /**
+   * This class parses a stream containing a DICOM instance, using a
+   * state machine.
+   *
+   * It does *not* support the visit of sequences (it only works at
+   * the first level of the hierarchy), and as a consequence, it
+   * doesn't give access to the pixel data of compressed transfer
+   * syntaxes.
+   **/
+  class ORTHANC_PUBLIC DicomStreamReader : public boost::noncopyable
+  {
+  public:
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      // The data from this function will always be Little Endian (as
+      // specified by the DICOM standard)
+      virtual void VisitMetaHeaderTag(const DicomTag& tag,
+                                      const ValueRepresentation& vr,
+                                      const std::string& value) = 0;
+
+      virtual void VisitTransferSyntax(DicomTransferSyntax transferSyntax) = 0;
+
+      // Return "false" to stop processing
+      virtual bool VisitDatasetTag(const DicomTag& tag,
+                                   const ValueRepresentation& vr,
+                                   const std::string& value,
+                                   bool isLittleEndian,
+                                   uint64_t fileOffset) = 0;
+    };
+    
+  private:
+    class PixelDataVisitor;
+    
+    enum State
+    {
+      State_Preamble,
+      State_MetaHeader,
+      State_DatasetTag,
+      State_SequenceExplicitLength,
+      State_SequenceExplicitValue,
+      State_DatasetExplicitLength,
+      State_DatasetValue,
+      State_Done
+    };
+
+    StreamBlockReader    reader_;
+    State                state_;
+    DicomTransferSyntax  transferSyntax_;
+    DicomTag             danglingTag_;  // Current root-level tag
+    ValueRepresentation  danglingVR_;
+    uint64_t             danglingOffset_;
+    unsigned int         sequenceDepth_;
+    
+    bool IsLittleEndian() const;
+    
+    void HandlePreamble(IVisitor& visitor,
+                        const std::string& block);
+    
+    void HandleMetaHeader(IVisitor& visitor,
+                          const std::string& block);
+
+    void HandleDatasetTag(const std::string& block,
+                          const DicomTag& untilTag);
+
+    void HandleDatasetExplicitLength(uint32_t length);
+    
+    void HandleDatasetExplicitLength(IVisitor& visitor,
+                                     const std::string& block);
+
+    void HandleSequenceExplicitLength(const std::string& block);
+
+    void HandleSequenceExplicitValue();
+    
+    void HandleDatasetValue(IVisitor& visitor,
+                            const std::string& block);
+    
+  public:
+    explicit DicomStreamReader(std::istream& stream);
+
+    /**
+     * Consume all the available bytes from the input stream, until
+     * end-of-stream is reached or the current tag is ">= untilTag".
+     * This method can be invoked several times, as more bytes are
+     * available from the input stream. To check if the DICOM stream
+     * is fully parsed until the goal tag, call "IsDone()".
+     **/
+    void Consume(IVisitor& visitor,
+                 const DicomTag& untilTag);
+
+    void Consume(IVisitor& visitor);
+
+    bool IsDone() const;
+
+    uint64_t GetProcessedBytes() const;
+
+    static bool LookupPixelDataOffset(uint64_t& offset /* out */,
+                                      ValueRepresentation& vr /* out */,
+                                      const std::string& dicom);
+
+    static bool LookupPixelDataOffset(uint64_t& offset /* out */,
+                                      ValueRepresentation& vr /* out */,
+                                      const void* buffer,
+                                      size_t size);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomTag.cpp b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp
new file mode 100644
index 0000000..33e8351
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp
@@ -0,0 +1,322 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomTag.h"
+
+#include "../OrthancException.h"
+
+#include 
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{ 
+  static inline uint16_t GetCharValue(char c)
+  {
+    if (c >= '0' && c <= '9')
+      return c - '0';
+    else if (c >= 'a' && c <= 'f')
+      return c - 'a' + 10;
+    else if (c >= 'A' && c <= 'F')
+      return c - 'A' + 10;
+    else
+      return 0;
+  }
+
+
+  static inline uint16_t GetTagValue(const char* c)
+  {
+    return ((GetCharValue(c[0]) << 12) + 
+            (GetCharValue(c[1]) << 8) + 
+            (GetCharValue(c[2]) << 4) + 
+            GetCharValue(c[3]));
+  }
+
+
+  DicomTag::DicomTag(uint16_t group, uint16_t element) :
+    group_(group),
+    element_(element)
+  {
+  }
+
+  uint16_t DicomTag::GetGroup() const
+  {
+    return group_;
+  }
+
+  uint16_t DicomTag::GetElement() const
+  {
+    return element_;
+  }
+
+  bool DicomTag::IsPrivate() const
+  {
+    return group_ % 2 == 1;
+  }
+
+
+  bool DicomTag::operator< (const DicomTag& other) const
+  {
+    if (group_ < other.group_)
+      return true;
+
+    if (group_ > other.group_)
+      return false;
+
+    return element_ < other.element_;
+  }
+
+
+  bool DicomTag::operator<= (const DicomTag& other) const
+  {
+    if (group_ < other.group_)
+      return true;
+
+    if (group_ > other.group_)
+      return false;
+
+    return element_ <= other.element_;
+  }
+
+  bool DicomTag::operator>(const DicomTag &other) const
+  {
+    return !(*this <= other);
+  }
+
+  bool DicomTag::operator>=(const DicomTag &other) const
+  {
+    return !(*this < other);
+  }
+
+  bool DicomTag::operator==(const DicomTag &other) const
+  {
+    return group_ == other.group_ && element_ == other.element_;
+  }
+
+  bool DicomTag::operator!=(const DicomTag &other) const
+  {
+    return !(*this == other);
+  }
+
+
+  std::ostream& DicomTag::FormatStream(std::ostream& o) const
+  {
+    using namespace std;
+    ios_base::fmtflags state = o.flags();
+    o.flags(ios::right | ios::hex);
+    o << "(" << setfill('0') << setw(4) << GetGroup()
+      << "," << setw(4) << GetElement() << ")";
+    o.flags(state);
+    return o;
+  }
+
+
+  std::string DicomTag::Format() const
+  {
+    char b[16];
+    sprintf(b, "%04x,%04x", group_, element_);
+    return std::string(b);
+  }
+
+
+  bool DicomTag::ParseHexadecimal(DicomTag& tag,
+                                  const char* value)
+  {
+    size_t length = strlen(value);
+
+    if (length == 9 &&
+        isxdigit(value[0]) &&
+        isxdigit(value[1]) &&
+        isxdigit(value[2]) &&
+        isxdigit(value[3]) &&
+        (value[4] == '-' || value[4] == ',') &&
+        isxdigit(value[5]) &&
+        isxdigit(value[6]) &&
+        isxdigit(value[7]) &&
+        isxdigit(value[8]))        
+    {
+      uint16_t group = GetTagValue(value);
+      uint16_t element = GetTagValue(value + 5);
+      tag = DicomTag(group, element);
+      return true;
+    }
+    else if (length == 8 &&
+             isxdigit(value[0]) &&
+             isxdigit(value[1]) &&
+             isxdigit(value[2]) &&
+             isxdigit(value[3]) &&
+             isxdigit(value[4]) &&
+             isxdigit(value[5]) &&
+             isxdigit(value[6]) &&
+             isxdigit(value[7])) 
+    {
+      uint16_t group = GetTagValue(value);
+      uint16_t element = GetTagValue(value + 4);
+      tag = DicomTag(group, element);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void DicomTag::AddTagsForModule(std::set& target,
+                                  DicomModule module)
+  {
+    // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions
+
+    switch (module)
+    {
+      case DicomModule_Patient:
+        // This is Table C.7-1 "Patient Module Attributes" (p. 373)
+        target.insert(DicomTag(0x0010, 0x0010));   // Patient's name
+        target.insert(DicomTag(0x0010, 0x0020));   // Patient ID
+        target.insert(DicomTag(0x0010, 0x0030));   // Patient's birth date
+        target.insert(DicomTag(0x0010, 0x0040));   // Patient's sex
+        target.insert(DicomTag(0x0008, 0x1120));   // Referenced patient sequence
+        target.insert(DicomTag(0x0010, 0x0032));   // Patient's birth time
+        target.insert(DicomTag(0x0010, 0x1000));   // Other patient IDs
+        target.insert(DicomTag(0x0010, 0x1002));   // Other patient IDs sequence
+        target.insert(DicomTag(0x0010, 0x1001));   // Other patient names
+        target.insert(DicomTag(0x0010, 0x2160));   // Ethnic group
+        target.insert(DicomTag(0x0010, 0x4000));   // Patient comments
+        target.insert(DicomTag(0x0010, 0x2201));   // Patient species description
+        target.insert(DicomTag(0x0010, 0x2202));   // Patient species code sequence
+        target.insert(DicomTag(0x0010, 0x2292));   // Patient breed description
+        target.insert(DicomTag(0x0010, 0x2293));   // Patient breed code sequence
+        target.insert(DicomTag(0x0010, 0x2294));   // Breed registration sequence
+        target.insert(DicomTag(0x0010, 0x2297));   // Responsible person
+        target.insert(DicomTag(0x0010, 0x2298));   // Responsible person role
+        target.insert(DicomTag(0x0010, 0x2299));   // Responsible organization
+        target.insert(DicomTag(0x0012, 0x0062));   // Patient identity removed
+        target.insert(DicomTag(0x0012, 0x0063));   // De-identification method
+        target.insert(DicomTag(0x0012, 0x0064));   // De-identification method code sequence
+
+        // Table 10-18 ISSUER OF PATIENT ID MACRO (p. 112)
+        target.insert(DicomTag(0x0010, 0x0021));   // Issuer of Patient ID
+        target.insert(DicomTag(0x0010, 0x0024));   // Issuer of Patient ID qualifiers sequence
+        break;
+
+      case DicomModule_Study:
+        // This is Table C.7-3 "General Study Module Attributes" (p. 378)
+        target.insert(DicomTag(0x0020, 0x000d));   // Study instance UID
+        target.insert(DicomTag(0x0008, 0x0020));   // Study date
+        target.insert(DicomTag(0x0008, 0x0030));   // Study time
+        target.insert(DicomTag(0x0008, 0x0090));   // Referring physician's name
+        target.insert(DicomTag(0x0008, 0x0096));   // Referring physician identification sequence
+        target.insert(DicomTag(0x0020, 0x0010));   // Study ID
+        target.insert(DicomTag(0x0008, 0x0050));   // Accession number
+        target.insert(DicomTag(0x0008, 0x0051));   // Issuer of accession number sequence
+        target.insert(DicomTag(0x0008, 0x1030));   // Study description
+        target.insert(DicomTag(0x0008, 0x1048));   // Physician(s) of record
+        target.insert(DicomTag(0x0008, 0x1049));   // Physician(s) of record identification sequence
+        target.insert(DicomTag(0x0008, 0x1060));   // Name of physician(s) reading study
+        target.insert(DicomTag(0x0008, 0x1062));   // Physician(s) reading study identification sequence
+        target.insert(DicomTag(0x0032, 0x1034));   // Requesting service code sequence
+        target.insert(DicomTag(0x0008, 0x1110));   // Referenced study sequence
+        target.insert(DicomTag(0x0008, 0x1032));   // Procedure code sequence
+        target.insert(DicomTag(0x0040, 0x1012));   // Reason for performed procedure code sequence
+        break;
+
+      case DicomModule_Series:
+        // This is Table C.7-5 "General Series Module Attributes" (p. 385)
+        target.insert(DicomTag(0x0008, 0x0060));   // Modality 
+        target.insert(DicomTag(0x0020, 0x000e));   // Series Instance UID 
+        target.insert(DicomTag(0x0020, 0x0011));   // Series Number 
+        target.insert(DicomTag(0x0020, 0x0060));   // Laterality 
+        target.insert(DicomTag(0x0008, 0x0021));   // Series Date 
+        target.insert(DicomTag(0x0008, 0x0031));   // Series Time 
+        target.insert(DicomTag(0x0008, 0x1050));   // Performing Physicians’ Name 
+        target.insert(DicomTag(0x0008, 0x1052));   // Performing Physician Identification Sequence 
+        target.insert(DicomTag(0x0018, 0x1030));   // Protocol Name
+        target.insert(DicomTag(0x0008, 0x103e));   // Series Description 
+        target.insert(DicomTag(0x0008, 0x103f));   // Series Description Code Sequence 
+        target.insert(DicomTag(0x0008, 0x1070));   // Operators' Name 
+        target.insert(DicomTag(0x0008, 0x1072));   // Operator Identification Sequence 
+        target.insert(DicomTag(0x0008, 0x1111));   // Referenced Performed Procedure Step Sequence
+        target.insert(DicomTag(0x0008, 0x1250));   // Related Series Sequence
+        target.insert(DicomTag(0x0018, 0x0015));   // Body Part Examined
+        target.insert(DicomTag(0x0018, 0x5100));   // Patient Position
+        target.insert(DicomTag(0x0028, 0x0108));   // Smallest Pixel Value in Series 
+        target.insert(DicomTag(0x0029, 0x0109));   // Largest Pixel Value in Series 
+        target.insert(DicomTag(0x0040, 0x0275));   // Request Attributes Sequence 
+        target.insert(DicomTag(0x0010, 0x2210));   // Anatomical Orientation Type
+
+        // Table 10-16 PERFORMED PROCEDURE STEP SUMMARY MACRO ATTRIBUTES
+        target.insert(DicomTag(0x0040, 0x0253));   // Performed Procedure Step ID 
+        target.insert(DicomTag(0x0040, 0x0244));   // Performed Procedure Step Start Date 
+        target.insert(DicomTag(0x0040, 0x0245));   // Performed Procedure Step Start Time 
+        target.insert(DicomTag(0x0040, 0x0254));   // Performed Procedure Step Description 
+        target.insert(DicomTag(0x0040, 0x0260));   // Performed Protocol Code Sequence 
+        target.insert(DicomTag(0x0040, 0x0280));   // Comments on the Performed Procedure Step
+        break;
+
+      case DicomModule_Instance:
+        // This is Table C.12-1 "SOP Common Module Attributes" (p. 1207)
+        target.insert(DicomTag(0x0008, 0x0016));   // SOP Class UID
+        target.insert(DicomTag(0x0008, 0x0018));   // SOP Instance UID 
+        target.insert(DicomTag(0x0008, 0x0005));   // Specific Character Set 
+        target.insert(DicomTag(0x0008, 0x0012));   // Instance Creation Date 
+        target.insert(DicomTag(0x0008, 0x0013));   // Instance Creation Time 
+        target.insert(DicomTag(0x0008, 0x0014));   // Instance Creator UID 
+        target.insert(DicomTag(0x0008, 0x001a));   // Related General SOP Class UID 
+        target.insert(DicomTag(0x0008, 0x001b));   // Original Specialized SOP Class UID 
+        target.insert(DicomTag(0x0008, 0x0110));   // Coding Scheme Identification Sequence 
+        target.insert(DicomTag(0x0008, 0x0201));   // Timezone Offset From UTC 
+        target.insert(DicomTag(0x0018, 0xa001));   // Contributing Equipment Sequence
+        target.insert(DicomTag(0x0020, 0x0013));   // Instance Number 
+        target.insert(DicomTag(0x0100, 0x0410));   // SOP Instance Status 
+        target.insert(DicomTag(0x0100, 0x0420));   // SOP Authorization DateTime 
+        target.insert(DicomTag(0x0100, 0x0424));   // SOP Authorization Comment 
+        target.insert(DicomTag(0x0100, 0x0426));   // Authorization Equipment Certification Number
+        target.insert(DicomTag(0x0400, 0x0500));   // Encrypted Attributes Sequence
+        target.insert(DicomTag(0x0400, 0x0561));   // Original Attributes Sequence 
+        target.insert(DicomTag(0x0040, 0xa390));   // HL7 Structured Document Reference Sequence
+        target.insert(DicomTag(0x0028, 0x0303));   // Longitudinal Temporal Information Modified 
+
+        // Table C.12-6 "DIGITAL SIGNATURES MACRO ATTRIBUTES" (p. 1216)
+        target.insert(DicomTag(0x4ffe, 0x0001));   // MAC Parameters sequence
+        target.insert(DicomTag(0xfffa, 0xfffa));   // Digital signatures sequence
+        break;
+
+        // TODO IMAGE MODULE?
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+  std::ostream& operator<< (std::ostream& o, const DicomTag& tag)
+  {
+    tag.FormatStream(o);
+    return o;
+  }
+#endif
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomTag.h b/OrthancFramework/Sources/DicomFormat/DicomTag.h
new file mode 100644
index 0000000..f900673
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h
@@ -0,0 +1,243 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include "../Compatibility.h"
+#include "../Enumerations.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomTag
+  {
+    // This must stay a POD (plain old data structure) 
+
+  private:
+    uint16_t group_;
+    uint16_t element_;
+
+  public:
+    DicomTag(uint16_t group,
+             uint16_t element);
+
+    uint16_t GetGroup() const;
+
+    uint16_t GetElement() const;
+
+    bool IsPrivate() const;
+
+    bool operator< (const DicomTag& other) const;
+
+    bool operator<= (const DicomTag& other) const;
+
+    bool operator> (const DicomTag& other) const;
+
+    bool operator>= (const DicomTag& other) const;
+
+    bool operator== (const DicomTag& other) const;
+
+    bool operator!= (const DicomTag& other) const;
+
+    std::string Format() const;
+
+    std::ostream& FormatStream(std::ostream& o) const;
+
+    static bool ParseHexadecimal(DicomTag& tag,
+                                 const char* value);
+
+    static void AddTagsForModule(std::set& target,
+                                 DicomModule module);
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+    ORTHANC_PUBLIC ORTHANC_DEPRECATED(friend std::ostream& operator<<(std::ostream& o, const DicomTag& tag));
+#endif
+  };
+
+  // Aliases for the most useful tags
+  static const DicomTag DICOM_TAG_ACCESSION_NUMBER(0x0008, 0x0050);
+  static const DicomTag DICOM_TAG_SOP_INSTANCE_UID(0x0008, 0x0018);
+  static const DicomTag DICOM_TAG_PATIENT_ID(0x0010, 0x0020);
+  static const DicomTag DICOM_TAG_SERIES_INSTANCE_UID(0x0020, 0x000e);
+  static const DicomTag DICOM_TAG_STUDY_INSTANCE_UID(0x0020, 0x000d);
+  static const DicomTag DICOM_TAG_PIXEL_DATA(0x7fe0, 0x0010);
+  static const DicomTag DICOM_TAG_TRANSFER_SYNTAX_UID(0x0002, 0x0010);
+
+  static const DicomTag DICOM_TAG_IMAGE_INDEX(0x0054, 0x1330);
+  static const DicomTag DICOM_TAG_INSTANCE_NUMBER(0x0020, 0x0013);
+
+  static const DicomTag DICOM_TAG_NUMBER_OF_SLICES(0x0054, 0x0081);
+  static const DicomTag DICOM_TAG_NUMBER_OF_TIME_SLICES(0x0054, 0x0101);
+  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_ENCAPSULATED_DOCUMENT(0x0042, 0x0011);
+
+  static const DicomTag DICOM_TAG_STUDY_DESCRIPTION(0x0008, 0x1030);
+  static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e);
+  static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060);
+
+  // The following is used for "modify/anonymize" operations
+  static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
+  static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002);
+  static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID(0x0002, 0x0003);
+  static const DicomTag DICOM_TAG_PATIENT_IDENTITY_REMOVED(0x0012, 0x0062);
+  static const DicomTag DICOM_TAG_DEIDENTIFICATION_METHOD(0x0012, 0x0063);
+  
+  // DICOM tags used for fMRI (thanks to Will Ryder)
+  static const DicomTag DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS(0x0020, 0x0105);
+  static const DicomTag DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER(0x0020, 0x0100);
+
+  // Tags for C-FIND and C-MOVE
+  static const DicomTag DICOM_TAG_MESSAGE_ID(0x0000, 0x0110);
+  static const DicomTag DICOM_TAG_SPECIFIC_CHARACTER_SET(0x0008, 0x0005);
+  static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052);
+  static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061);
+  static const DicomTag DICOM_TAG_RETRIEVE_AE_TITLE(0x0008, 0x0054);
+  static const DicomTag DICOM_TAG_INSTANCE_AVAILABILITY(0x0008, 0x0056);
+
+  // Tags for images
+  static const DicomTag DICOM_TAG_COLUMNS(0x0028, 0x0011);
+  static const DicomTag DICOM_TAG_ROWS(0x0028, 0x0010);
+  static const DicomTag DICOM_TAG_SAMPLES_PER_PIXEL(0x0028, 0x0002);
+  static const DicomTag DICOM_TAG_BITS_ALLOCATED(0x0028, 0x0100);
+  static const DicomTag DICOM_TAG_BITS_STORED(0x0028, 0x0101);
+  static const DicomTag DICOM_TAG_HIGH_BIT(0x0028, 0x0102);
+  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);
+  static const DicomTag DICOM_TAG_IMAGE_ORIENTATION_PATIENT(0x0020, 0x0037);
+  static const DicomTag DICOM_TAG_IMAGE_POSITION_PATIENT(0x0020, 0x0032);
+  static const DicomTag DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE(0x0028, 0x0107);
+  static const DicomTag DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE(0x0028, 0x0106);
+
+  // 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);
+  static const DicomTag DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC(0x0008, 0x0201);
+
+  // Various tags
+  static const DicomTag DICOM_TAG_SERIES_TYPE(0x0054, 0x1000);
+  static const DicomTag DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION(0x0032, 0x1060);
+  static const DicomTag DICOM_TAG_INSTITUTION_NAME(0x0008, 0x0080);
+  static const DicomTag DICOM_TAG_REQUESTING_PHYSICIAN(0x0032, 0x1032);
+  static const DicomTag DICOM_TAG_REFERRING_PHYSICIAN_NAME(0x0008, 0x0090);
+  static const DicomTag DICOM_TAG_OPERATOR_NAME(0x0008, 0x1070);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE(0x0040, 0x0244);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME(0x0040, 0x0245);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254);
+  static const DicomTag DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE(0x0040, 0x0275);
+  static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000);
+  static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400);
+  static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_CODE(0x0018, 0x1401);
+  static const DicomTag DICOM_TAG_CASSETTE_ORIENTATION(0x0018, 0x1402);
+  static const DicomTag DICOM_TAG_CASSETTE_SIZE(0x0018, 0x1403);
+  static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010);
+  static const DicomTag DICOM_TAG_STUDY_ID(0x0020, 0x0010);
+  static const DicomTag DICOM_TAG_SERIES_NUMBER(0x0020, 0x0011);
+  static const DicomTag DICOM_TAG_PATIENT_SEX(0x0010, 0x0040);
+  static const DicomTag DICOM_TAG_LATERALITY(0x0020, 0x0060);
+  static const DicomTag DICOM_TAG_BODY_PART_EXAMINED(0x0018, 0x0015);
+  static const DicomTag DICOM_TAG_SEQUENCE_NAME(0x0018, 0x0024);
+  static const DicomTag DICOM_TAG_PROTOCOL_NAME(0x0018, 0x1030);
+  static const DicomTag DICOM_TAG_VIEW_POSITION(0x0018, 0x5101);
+  static const DicomTag DICOM_TAG_MANUFACTURER(0x0008, 0x0070);
+  static const DicomTag DICOM_TAG_STATION_NAME(0x0008, 0x1010);
+  static const DicomTag DICOM_TAG_PATIENT_ORIENTATION(0x0020, 0x0020);
+  static const DicomTag DICOM_TAG_PATIENT_COMMENTS(0x0010, 0x4000);
+  static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201);
+  static const DicomTag DICOM_TAG_STUDY_COMMENTS(0x0032, 0x4000);
+  static const DicomTag DICOM_TAG_OTHER_PATIENT_IDS(0x0010, 0x1000);
+  static const DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUP_SEQUENCE(0x5200, 0x9230);
+  static const DicomTag DICOM_TAG_PIXEL_VALUE_TRANSFORMATION_SEQUENCE(0x0028, 0x9145);
+  static const DicomTag DICOM_TAG_FRAME_VOI_LUT_SEQUENCE(0x0028, 0x9132);
+  static const DicomTag DICOM_TAG_ACQUISITION_NUMBER(0x0020, 0x0012);
+
+  // Tags used within the Stone of Orthanc
+  static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009);
+  static const DicomTag DICOM_TAG_GRID_FRAME_OFFSET_VECTOR(0x3004, 0x000c);
+  static const DicomTag DICOM_TAG_PIXEL_SPACING(0x0028, 0x0030);
+  static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052);
+  static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053);
+  static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050);
+  static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050);
+  static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051);
+  static const DicomTag DICOM_TAG_DOSE_GRID_SCALING(0x3004, 0x000e);
+  static const DicomTag DICOM_TAG_RED_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1201);
+  static const DicomTag DICOM_TAG_GREEN_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1202);
+  static const DicomTag DICOM_TAG_BLUE_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1203);
+  static const DicomTag DICOM_TAG_RED_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1101);
+  static const DicomTag DICOM_TAG_GREEN_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1102);
+  static const DicomTag DICOM_TAG_BLUE_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1103);
+  static const DicomTag DICOM_TAG_CONTOUR_DATA(0x3006, 0x0050);
+  static const DicomTag DICOM_TAG_CINE_RATE(0x0018, 0x0040);
+                             
+  // Counting patients, studies and series
+  // https://www.medicalconnections.co.uk/kb/Counting_Studies_Series_and_Instances
+  static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES(0x0020, 0x1200);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES(0x0020, 0x1202);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES(0x0020, 0x1204);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES(0x0020, 0x1206);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES(0x0020, 0x1208);  
+  static const DicomTag DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES(0x0020, 0x1209);  
+  static const DicomTag DICOM_TAG_SOP_CLASSES_IN_STUDY(0x0008, 0x0062);  
+
+  // Tags to preserve relationships during anonymization
+  static const DicomTag DICOM_TAG_REFERENCED_IMAGE_SEQUENCE(0x0008, 0x1140);
+  static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID(0x0008, 0x1155);
+  static const DicomTag DICOM_TAG_SOURCE_IMAGE_SEQUENCE(0x0008, 0x2112);
+  static const DicomTag DICOM_TAG_FRAME_OF_REFERENCE_UID(0x0020, 0x0052);
+  static const DicomTag DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_UID(0x3006, 0x0024);
+  static const DicomTag DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID(0x3006, 0x00c2);
+  static const DicomTag DICOM_TAG_CURRENT_REQUESTED_PROCEDURE_EVIDENCE_SEQUENCE(0x0040, 0xa375);
+  static const DicomTag DICOM_TAG_REFERENCED_SERIES_SEQUENCE(0x0008, 0x1115);
+  static const DicomTag DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE(0x3006, 0x0010);
+  static const DicomTag DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE(0x3006, 0x0012);
+  static const DicomTag DICOM_TAG_RT_REFERENCED_SERIES_SEQUENCE(0x3006, 0x0014);
+
+  // Tags for DICOMDIR
+  static const DicomTag DICOM_TAG_DIRECTORY_RECORD_TYPE(0x0004, 0x1430);
+  static const DicomTag DICOM_TAG_OFFSET_OF_THE_NEXT_DIRECTORY_RECORD(0x0004, 0x1400);
+  static const DicomTag DICOM_TAG_OFFSET_OF_REFERENCED_LOWER_LEVEL_DIRECTORY_ENTITY(0x0004, 0x1420);
+  static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE(0x0004, 0x1511);
+  static const DicomTag DICOM_TAG_REFERENCED_FILE_ID(0x0004, 0x1500);
+
+  // Tags for DicomWeb
+  static const Orthanc::DicomTag DICOM_TAG_RETRIEVE_URL(0x0008, 0x1190);
+
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp
new file mode 100644
index 0000000..a1875a5
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp
@@ -0,0 +1,335 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomValue.h"
+
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "../Toolbox.h"
+
+#include 
+
+namespace Orthanc
+{
+  DicomValue::DicomValue() :
+    type_(Type_Null)
+  {
+  }
+
+
+  DicomValue::DicomValue(const DicomValue& other) :
+    type_(other.type_),
+    content_(other.content_),
+    sequenceJson_(other.sequenceJson_)
+  {
+  }
+
+
+  DicomValue::DicomValue(const std::string& content,
+                         bool isBinary) :
+    type_(isBinary ? Type_Binary : Type_String),
+    content_(content)
+  {
+  }
+  
+  
+  DicomValue::DicomValue(const char* data,
+                         size_t size,
+                         bool isBinary) :
+    type_(isBinary ? Type_Binary : Type_String)
+  {
+    content_.assign(data, size);
+  }
+    
+  DicomValue::DicomValue(const Json::Value& value) :
+    type_(Type_SequenceAsJson),
+    sequenceJson_(value)
+  {
+    if (value.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+  }
+  
+  const std::string& DicomValue::GetContent() const
+  {
+    if (type_ == Type_Null || type_ == Type_SequenceAsJson)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return content_;
+    }
+  }
+
+  const Json::Value& DicomValue::GetSequenceContent() const
+  {
+    if (type_ != Type_SequenceAsJson)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return sequenceJson_;
+    }
+  }
+
+
+  bool DicomValue::IsNull() const
+  {
+    return type_ == Type_Null;
+  }
+
+  bool DicomValue::IsBinary() const
+  {
+    return type_ == Type_Binary;
+  }
+
+  bool DicomValue::IsString() const
+  {
+    return type_ == Type_String;
+  }
+
+  bool DicomValue::IsSequence() const
+  {
+    return type_ == Type_SequenceAsJson;
+  }
+
+  DicomValue* DicomValue::Clone() const
+  {
+    return new DicomValue(*this);
+  }
+
+  
+#if ORTHANC_ENABLE_BASE64 == 1
+  void DicomValue::FormatDataUriScheme(std::string& target,
+                                       const std::string& mime) const
+  {
+    Toolbox::EncodeBase64(target, GetContent());
+    target.insert(0, "data:" + mime + ";base64,");
+  }
+
+  void DicomValue::FormatDataUriScheme(std::string& target) const
+  {
+    FormatDataUriScheme(target, MIME_BINARY);
+  }
+#endif
+
+  bool DicomValue::ParseInteger32(int32_t& result) const
+  {
+    if (!IsString())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseInteger32(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseInteger64(int64_t& result) const
+  {
+    if (!IsString())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseInteger64(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const
+  {
+    if (!IsString())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseUnsignedInteger32(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const
+  {
+    if (!IsString())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseUnsignedInteger64(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseFloat(float& result) const
+  {
+    if (!IsString())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseFloat(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseDouble(double& result) const
+  {
+    if (!IsString())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseDouble(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseFirstFloat(float& result) const
+  {
+    if (!IsString())
+    {
+      return false;
+    }
+    else
+    {
+      return SerializationToolbox::ParseFirstFloat(result, GetContent());
+    }
+  }
+
+  bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const
+  {
+    uint64_t value;
+
+    if (!IsString())
+    {
+      return false;
+    }
+    else if (SerializationToolbox::ParseFirstUnsignedInteger64(value, GetContent()))
+    {
+      result = static_cast(value);
+      return (static_cast(result) == value);   // Check no overflow
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool DicomValue::CopyToString(std::string& result,
+                                bool allowBinary) const
+  {
+    if (IsNull())
+    {
+      return false;
+    }
+    else if (IsSequence())
+    {
+      return false;
+    }
+    else if (IsBinary() && !allowBinary)
+    {
+      return false;
+    }
+    else
+    {
+      result.assign(content_);
+      return true;
+    }
+  }    
+
+
+  static const char* KEY_TYPE = "Type";
+  static const char* KEY_CONTENT = "Content";
+  
+  void DicomValue::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    switch (type_)
+    {
+      case Type_Null:
+        target[KEY_TYPE] = "Null";
+        break;
+
+      case Type_String:
+        target[KEY_TYPE] = "String";
+        target[KEY_CONTENT] = content_;
+        break;
+
+      case Type_Binary:
+      {
+        target[KEY_TYPE] = "Binary";
+
+        std::string base64;
+        Toolbox::EncodeBase64(base64, content_);
+        target[KEY_CONTENT] = base64;
+        break;
+      }
+
+      case Type_SequenceAsJson:
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  void DicomValue::Unserialize(const Json::Value& source)
+  {
+    std::string type = SerializationToolbox::ReadString(source, KEY_TYPE);
+
+    if (type == "Null")
+    {
+      type_ = Type_Null;
+      content_.clear();
+    }
+    else if (type == "String")
+    {
+      type_ = Type_String;
+      content_ = SerializationToolbox::ReadString(source, KEY_CONTENT);
+    }
+    else if (type == "Binary")
+    {
+      type_ = Type_Binary;
+
+      const std::string base64 =SerializationToolbox::ReadString(source, KEY_CONTENT);
+      Toolbox::DecodeBase64(content_, base64);
+    }
+    else if (type == "Sequence")
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/DicomValue.h b/OrthancFramework/Sources/DicomFormat/DicomValue.h
new file mode 100644
index 0000000..47e8946
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.h
@@ -0,0 +1,113 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Enumerations.h"
+
+#include 
+#include 
+#include 
+
+#if !defined(ORTHANC_ENABLE_BASE64)
+#  error The macro ORTHANC_ENABLE_BASE64 must be defined
+#endif
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomValue : public boost::noncopyable
+  {
+  private:
+    enum Type
+    {
+      Type_Null,
+      Type_String,
+      Type_Binary,
+      Type_SequenceAsJson
+    };
+
+    Type         type_;
+    std::string  content_;
+    Json::Value  sequenceJson_;
+
+    DicomValue(const DicomValue& other);
+
+  public:
+    DicomValue();
+    
+    DicomValue(const std::string& content,
+               bool isBinary);
+    
+    DicomValue(const char* data,
+               size_t size,
+               bool isBinary);
+    
+    explicit DicomValue(const Json::Value& value);
+    
+    const std::string& GetContent() const;
+
+    const Json::Value& GetSequenceContent() const;
+
+    bool IsNull() const;
+
+    bool IsBinary() const;
+
+    bool IsString() const;
+
+    bool IsSequence() const;
+    
+    DicomValue* Clone() const;
+
+#if ORTHANC_ENABLE_BASE64 == 1
+    void FormatDataUriScheme(std::string& target,
+                             const std::string& mime) const;
+
+    void FormatDataUriScheme(std::string& target) const;
+#endif
+
+    bool CopyToString(std::string& result,
+                      bool allowBinary) const;
+    
+    bool ParseInteger32(int32_t& result) const;
+
+    bool ParseInteger64(int64_t& result) const;                                
+
+    bool ParseUnsignedInteger32(uint32_t& result) const;
+
+    bool ParseUnsignedInteger64(uint64_t& result) const;                                
+
+    bool ParseFloat(float& result) const;                                
+
+    bool ParseDouble(double& result) const;
+
+    bool ParseFirstFloat(float& result) const;
+
+    bool ParseFirstUnsignedInteger(unsigned int& result) const;
+
+    void Serialize(Json::Value& target) const;
+
+    void Unserialize(const Json::Value& source);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/StreamBlockReader.cpp b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.cpp
new file mode 100644
index 0000000..988e040
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.cpp
@@ -0,0 +1,103 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "StreamBlockReader.h"
+
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  StreamBlockReader::StreamBlockReader(std::istream& stream) :
+    stream_(stream),
+    blockPos_(0),
+    processedBytes_(0)
+  {
+  }
+
+
+  void StreamBlockReader::Schedule(size_t blockSize)
+  {
+    if (!block_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      block_.resize(blockSize);
+      blockPos_ = 0;
+    }
+  }
+
+
+  bool StreamBlockReader::Read(std::string& block)
+  {
+    if (block_.empty())
+    {
+      if (blockPos_ != 0)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+        
+      block.clear();
+      return true;
+    }
+    else
+    {
+      while (blockPos_ < block_.size())
+      {
+        /**
+         * WARNING: Do NOT use "stream_.readsome()", as it does not
+         * work properly on non-buffered stream (which is the case in
+         * "DicomStreamReader::LookupPixelDataOffset()" for buffers)
+         **/
+        
+        size_t remainingBytes = block_.size() - blockPos_;
+        stream_.read(&block_[blockPos_], remainingBytes);
+        
+        std::streamsize r = stream_.gcount();
+        if (r == 0)
+        {
+          return false;
+        }
+        else
+        {
+          blockPos_ += r;
+        }
+      }
+
+      processedBytes_ += block_.size();
+
+      block.swap(block_);
+      block_.clear();
+      return true;
+    }
+  }
+
+  uint64_t StreamBlockReader::GetProcessedBytes() const
+  {
+    return processedBytes_;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/StreamBlockReader.h b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.h
new file mode 100644
index 0000000..9a35f61
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/StreamBlockReader.h
@@ -0,0 +1,68 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"  // For ORTHANC_PUBLIC
+
+#include 
+#include 
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  /**
+   * This class is used to extract blocks of given size from a
+   * stream. Bytes from the stream are buffered until the requested
+   * size is available, and the full block can be returned.
+   **/
+  class ORTHANC_PUBLIC StreamBlockReader : public boost::noncopyable
+  {
+  private:
+    std::istream&  stream_;
+    std::string    block_;
+    size_t         blockPos_;
+    uint64_t       processedBytes_;
+
+  public:
+    explicit StreamBlockReader(std::istream& stream);
+
+    /**
+     * Schedule the size of the next block to be extracted from the
+     * stream.
+     **/
+    void Schedule(size_t blockSize);
+
+    /**
+     * Extract the block whose size was configured by the previous
+     * call to "Schedule()". Returns "false" iff not enough bytes are
+     * available from the stream yet: In this case, try again later.
+     **/
+    bool Read(std::string& block);
+    
+    uint64_t GetProcessedBytes() const;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomFormat/Window.cpp b/OrthancFramework/Sources/DicomFormat/Window.cpp
new file mode 100644
index 0000000..b038189
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/Window.cpp
@@ -0,0 +1,54 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "Window.h"
+
+
+#include 
+
+namespace Orthanc
+{
+  Window::Window(double center,
+                 double width) :
+    center_(center)
+  {
+    width_ = std::abs(width);
+  }
+
+
+  void Window::GetBounds(double& low,
+                         double& high) const
+  {
+    low = center_ - width_ / 2.0;
+    high = center_ + width_ / 2.0;
+  }
+
+
+  Window Window::FromBounds(double low,
+                            double high)
+  {
+    return Window((low + high) / 2.0, std::abs(high - low));
+  }
+}
diff --git a/OrthancFramework/Sources/DicomFormat/Window.h b/OrthancFramework/Sources/DicomFormat/Window.h
new file mode 100644
index 0000000..7ccf625
--- /dev/null
+++ b/OrthancFramework/Sources/DicomFormat/Window.h
@@ -0,0 +1,59 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Compatibility.h"
+#include "../OrthancFramework.h"
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC Window
+  {
+  private:
+    double center_;
+    double width_;
+
+  public:
+    Window(double center,
+           double width);
+
+    double GetCenter() const
+    {
+      return center_;
+    }
+
+    double GetWidth() const
+    {
+      return width_;
+    }
+
+    void GetBounds(double& low,
+                   double& high) const;
+
+    static Window FromBounds(double low,
+                             double high);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp
new file mode 100644
index 0000000..c08d216
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp
@@ -0,0 +1,1005 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomAssociation.h"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "NetworkingCompatibility.h"
+
+#ifdef _WIN32
+#  include 
+#endif
+
+#include   // For dcmConnectionTimeout()
+#include 
+
+namespace Orthanc
+{
+  static void FillSopSequence(DcmDataset& dataset,
+                              const DcmTagKey& tag,
+                              const std::vector& sopClassUids,
+                              const std::vector& sopInstanceUids,
+                              const std::vector& failureReasons,
+                              bool hasFailureReasons)
+  {
+    assert(sopClassUids.size() == sopInstanceUids.size() &&
+           (hasFailureReasons ?
+            failureReasons.size() == sopClassUids.size() :
+            failureReasons.empty()));
+
+    if (sopInstanceUids.empty())
+    {
+      // Add an empty sequence
+      if (!dataset.insertEmptyElement(tag).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      for (size_t i = 0; i < sopClassUids.size(); i++)
+      {
+        std::unique_ptr item(new DcmItem);
+        if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() ||
+            !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() ||
+            (hasFailureReasons &&
+             !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) ||
+            !dataset.insertSequenceItem(tag, item.release()).good())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+  }                              
+
+
+  void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters,
+                                         const OFCondition& cond)
+  {
+    try
+    {
+      if (cond.bad() &&
+          cond == DUL_ASSOCIATIONREJECTED)
+      {
+        T_ASC_RejectParameters rej;
+        ASC_getRejectParameters(params_, &rej);
+
+        OFString str;
+        CLOG(TRACE, DICOM) << "Association Rejected:" << std::endl
+                           << ASC_printRejectParameters(str, &rej);
+      }
+      
+      CheckCondition(cond, parameters, "connecting");
+    }
+    catch (OrthancException&)
+    {
+      CloseInternal();
+      throw;
+    }
+  }
+
+    
+  void DicomAssociation::CloseInternal()
+  {
+    CLOG(INFO, DICOM) << "Closing DICOM association";
+
+#if ORTHANC_ENABLE_SSL == 1
+    tls_.reset(NULL);  // Transport layer must be destroyed before the association itself
+#endif
+    
+    if (assoc_ != NULL)
+    {
+      ASC_releaseAssociation(assoc_);
+      ASC_destroyAssociation(&assoc_);
+      assoc_ = NULL;
+      params_ = NULL;
+    }
+    else
+    {
+      if (params_ != NULL)
+      {
+        ASC_destroyAssociationParameters(¶ms_);
+        params_ = NULL;
+      }
+    }
+
+    if (net_ != NULL)
+    {
+      ASC_dropNetwork(&net_);
+      net_ = NULL;
+    }
+
+    accepted_.clear();
+    isOpen_ = false;
+  }
+
+    
+  void DicomAssociation::AddAccepted(const std::string& abstractSyntax,
+                                     DicomTransferSyntax syntax,
+                                     uint8_t presentationContextId)
+  {
+    AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax);
+
+    if (found == accepted_.end())
+    {
+      std::map syntaxes;
+      syntaxes[syntax] = presentationContextId;
+      accepted_[abstractSyntax] = syntaxes;
+    }      
+    else
+    {
+      if (found->second.find(syntax) != found->second.end())
+      {
+        CLOG(WARNING, DICOM) << "The same transfer syntax ("
+                             << GetTransferSyntaxUid(syntax)
+                             << ") was accepted twice for the same abstract syntax UID ("
+                             << abstractSyntax << ")";
+      }
+      else
+      {
+        found->second[syntax] = presentationContextId;
+      }
+    }
+  }
+
+
+  DicomAssociation::DicomAssociation()
+  {
+    isOpen_ = false;
+    net_ = NULL; 
+    params_ = NULL;
+    assoc_ = NULL;
+
+    // Must be after "isOpen_ = false"
+    ClearPresentationContexts();
+  }
+  
+
+  DicomAssociation::~DicomAssociation()
+  {
+    try
+    {
+      Close();
+    }
+    catch (OrthancException& e)
+    {
+      // Don't throw exception in destructors
+      CLOG(ERROR, DICOM) << "Error while destroying a DICOM association: " << e.What();
+    }
+  }
+
+
+  void DicomAssociation::ClearPresentationContexts()
+  {
+    Close();
+    proposed_.clear();
+    proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
+  }
+
+
+  static T_ASC_SC_ROLE GetDcmtkRole(DicomAssociationRole role)
+  {
+    switch (role)
+    {
+      case DicomAssociationRole_Default:
+        return ASC_SC_ROLE_DEFAULT;
+
+      case DicomAssociationRole_Scu:
+        return ASC_SC_ROLE_SCU;
+
+      case DicomAssociationRole_Scp:
+        return ASC_SC_ROLE_SCP;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void DicomAssociation::Open(const DicomAssociationParameters& parameters)
+  {
+    if (isOpen_)
+    {
+      return;  // Already open
+    }
+
+    // Timeout used during association negociation and ASC_releaseAssociation()
+    uint32_t acseTimeout = parameters.GetTimeout();
+    if (acseTimeout == 0)
+    {
+      /**
+       * Timeout is disabled. Global timeout (seconds) for
+       * connecting to remote hosts.  Default value is -1 which
+       * selects infinite timeout, i.e. blocking connect().
+       **/
+      dcmConnectionTimeout.set(-1);
+      acseTimeout = 10;
+    }
+    else
+    {
+      dcmConnectionTimeout.set(acseTimeout);
+    }
+      
+
+    assert(net_ == NULL &&
+           params_ == NULL &&
+           assoc_ == NULL);
+
+#if ORTHANC_ENABLE_SSL == 1
+    assert(tls_.get() == NULL);
+#endif
+
+    if (proposed_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No presentation context was proposed");
+    }
+
+    std::string localAet = parameters.GetLocalApplicationEntityTitle();
+    if (parameters.GetRemoteModality().HasLocalAet())
+    {
+      localAet = parameters.GetRemoteModality().GetLocalAet();
+    }
+
+    CLOG(INFO, DICOM) << "Opening a DICOM SCU connection "
+                      << (parameters.GetRemoteModality().IsDicomTlsEnabled() ? "using DICOM TLS" : "without DICOM TLS")
+                      << " from AET \"" << localAet
+                      << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle()
+                      << "\" on host " << parameters.GetRemoteModality().GetHost()
+                      << ":" << parameters.GetRemoteModality().GetPortNumber() 
+                      << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer())
+                      << ", " << (parameters.HasTimeout() ?
+                                  "timeout: " + boost::lexical_cast(parameters.GetTimeout()) + "s" :
+                                  "no timeout") << ")";
+
+    CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
+#if DCMTK_VERSION_NUMBER >= 368
+    CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength(), acseTimeout));
+#else
+    // from 3.6.8, this version is obsolete
+    CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength()));
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+    if (parameters.GetRemoteModality().IsDicomTlsEnabled())
+    {
+      try
+      {
+        assert(net_ != NULL &&
+               params_ != NULL);
+        tls_.reset(Internals::InitializeDicomTls(net_, NET_REQUESTOR, parameters.GetOwnPrivateKeyPath(),
+                                                 parameters.GetOwnCertificatePath(),
+                                                 parameters.GetTrustedCertificatesPath(),
+                                                 parameters.IsRemoteCertificateRequired(),
+                                                 parameters.GetMinimumTlsVersion(),
+                                                 parameters.GetAcceptedCiphers()));
+      }
+      catch (OrthancException&)
+      {
+        CloseInternal();
+        throw;
+      }
+    }
+#endif
+
+    // Set this application's title and the called application's title in the params
+    CheckConnecting(parameters, ASC_setAPTitles(
+                      params_, localAet.c_str(),
+                      parameters.GetRemoteModality().GetApplicationEntityTitle().c_str(), NULL));
+
+    // Set the network addresses of the local and remote entities
+    char localHost[HOST_NAME_MAX];
+    gethostname(localHost, HOST_NAME_MAX - 1);
+
+    char remoteHostAndPort[HOST_NAME_MAX];
+
+#ifdef _MSC_VER
+    _snprintf
+#else
+      snprintf
+#endif
+      (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d",
+       parameters.GetRemoteModality().GetHost().c_str(),
+       parameters.GetRemoteModality().GetPortNumber());
+
+    CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort));
+
+    // Set various options
+#if ORTHANC_ENABLE_SSL == 1
+    CheckConnecting(parameters, ASC_setTransportLayerType(params_, (tls_.get() != NULL) /*opt_secureConnection*/));
+#else
+    CheckConnecting(parameters, ASC_setTransportLayerType(params_, false /*opt_secureConnection*/));
+#endif
+
+    // Setup the list of proposed presentation contexts
+    unsigned int presentationContextId = 1;
+    for (size_t i = 0; i < proposed_.size(); i++)
+    {
+      assert(presentationContextId <= 255);
+      const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
+
+      const std::list& source = proposed_[i].transferSyntaxes_;
+          
+      std::vector transferSyntaxes;
+      transferSyntaxes.reserve(source.size());
+          
+      for (std::list::const_iterator
+             it = source.begin(); it != source.end(); ++it)
+      {
+        transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
+      }
+
+      assert(!transferSyntaxes.empty());
+      CheckConnecting(parameters, ASC_addPresentationContext(
+                        params_, presentationContextId, abstractSyntax,
+                        &transferSyntaxes[0], transferSyntaxes.size(), GetDcmtkRole(proposed_[i].role_)));
+
+      presentationContextId += 2;
+    }
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Request Parameters:" << std::endl
+                         << ASC_dumpParameters(str, params_, ASC_ASSOC_RQ);
+    }
+
+    // Do the association
+    CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_));
+    isOpen_ = true;
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Connection Parameters: "
+                         << ASC_dumpConnectionParameters(str, assoc_);
+      CLOG(TRACE, DICOM) << "Association Parameters Negotiated:" << std::endl
+                         << ASC_dumpParameters(str, params_, ASC_ASSOC_AC);
+    }
+
+
+    // Inspect the accepted transfer syntaxes
+    LST_HEAD **l = ¶ms_->DULparams.acceptedPresentationContext;
+    if (*l != NULL)
+    {
+      DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
+      LST_Position(l, (LST_NODE*)pc);
+      while (pc)
+      {
+        if (pc->result == ASC_P_ACCEPTANCE && strlen(pc->abstractSyntax) > 0)
+        {
+          CLOG(TRACE, DICOM) << "DicomAssociation::Open, adding SOPClassUID " << pc->abstractSyntax << " - TS " << pc->acceptedTransferSyntax << " - PC ID " << boost::lexical_cast(static_cast(pc->presentationContextID));
+
+          DicomTransferSyntax transferSyntax;
+          if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
+          {
+            AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
+          }
+          else
+          {
+            CLOG(WARNING, DICOM) << "Unknown transfer syntax received from AET \""
+                                 << parameters.GetRemoteModality().GetApplicationEntityTitle()
+                                 << "\": " << pc->acceptedTransferSyntax;
+          }
+        }
+            
+        pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
+      }
+    }
+
+    if (accepted_.empty())
+    {
+      throw OrthancException(ErrorCode_NoPresentationContext,
+                             "Unable to negotiate a presentation context with AET \"" +
+                             parameters.GetRemoteModality().GetApplicationEntityTitle() + "\"");
+    }
+  }
+
+  void DicomAssociation::Close()
+  {
+    if (isOpen_)
+    {
+      CloseInternal();
+    }
+  }
+
+    
+  bool DicomAssociation::LookupAcceptedPresentationContext(std::map& target,
+                                                           const std::string& abstractSyntax) const
+  {
+    if (!IsOpen())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened");
+    }
+      
+    AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax);
+
+    if (found == accepted_.end())
+    {
+      return false;
+    }
+    else
+    {
+      target = found->second;
+      return true;
+    }
+  }
+
+  void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                                           DicomAssociationRole role)
+  {
+    std::list ts;
+    ts.push_back(DicomTransferSyntax_LittleEndianExplicit); // the most standard one first !
+    ts.push_back(DicomTransferSyntax_LittleEndianImplicit);
+    ts.push_back(DicomTransferSyntax_BigEndianExplicit);  // Retired but was historicaly proposed by Orthanc
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
+    
+  void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax)
+  {
+    ProposeGenericPresentationContext(abstractSyntax, DicomAssociationRole_Default);
+  }
+
+    
+  void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
+                                                    DicomTransferSyntax transferSyntax)
+  {
+    ProposePresentationContext(abstractSyntax, transferSyntax, DicomAssociationRole_Default);
+  }
+
+
+  void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
+                                                    DicomTransferSyntax transferSyntax,
+                                                    DicomAssociationRole role)
+  {
+    std::list ts;
+    ts.push_back(transferSyntax);
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
+
+  size_t DicomAssociation::GetRemainingPropositions() const
+  {
+    assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
+    return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
+  }
+    
+  void DicomAssociation::ProposePresentationContext(
+    const std::string& abstractSyntax,
+    const std::list& transferSyntaxes)
+  {
+    ProposePresentationContext(abstractSyntax, transferSyntaxes, DicomAssociationRole_Default);
+  }
+
+
+  void DicomAssociation::ProposePresentationContext(
+    const std::string& abstractSyntax,
+    const std::list& transferSyntaxes,
+    DicomAssociationRole role)
+  {
+    if (transferSyntaxes.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "No transfer syntax provided");
+    }
+      
+    if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Too many proposed presentation contexts");
+    }
+      
+    if (IsOpen())
+    {
+      Close();
+    }
+
+    ProposedPresentationContext context;
+    context.abstractSyntax_ = abstractSyntax;
+    context.transferSyntaxes_ = transferSyntaxes;
+    context.role_ = role;
+
+    proposed_.push_back(context);
+  }
+
+    
+  T_ASC_Association& DicomAssociation::GetDcmtkAssociation() const
+  {
+    if (isOpen_)
+    {
+      assert(assoc_ != NULL);
+      return *assoc_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "The connection is not open");
+    }
+  }
+
+  bool DicomAssociation::GetAssociationParameters(std::string& remoteAet,
+                                                  std::string& remoteIp,
+                                                  std::string& calledAet) const
+  {
+    T_ASC_Association& dcmtkAssoc = GetDcmtkAssociation();
+
+    DIC_AE remoteAet_C;
+    DIC_AE calledAet_C;
+    DIC_AE remoteIp_C;
+    DIC_AE calledIP_C;
+
+    if (
+#if DCMTK_VERSION_NUMBER >= 364
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).good()
+#else
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, calledAet_C, NULL).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, calledIP_C).good()
+#endif
+      )
+    {
+      remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+      remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
+      calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      return true;
+    }
+
+    return false;
+  }
+    
+  T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const
+  {
+    if (isOpen_)
+    {
+      assert(net_ != NULL);
+      return *net_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "The connection is not open");
+    }
+  }
+
+    
+  void DicomAssociation::CheckCondition(const OFCondition& cond,
+                                        const DicomAssociationParameters& parameters,
+                                        const std::string& command)
+  {
+    if (cond.bad())
+    {
+      // Reformat the error message from DCMTK by turning multiline
+      // errors into a single line
+      
+      std::string s(cond.text());
+      std::string info;
+      info.reserve(s.size());
+
+      bool isMultiline = false;
+      for (size_t i = 0; i < s.size(); i++)
+      {
+        if (s[i] == '\r')
+        {
+          // Ignore
+        }
+        else if (s[i] == '\n')
+        {
+          if (isMultiline)
+          {
+            info += "; ";
+          }
+          else
+          {
+            info += " (";
+            isMultiline = true;
+          }
+        }
+        else
+        {
+          info.push_back(s[i]);
+        }
+      }
+
+      if (isMultiline)
+      {
+        info += ")";
+      }
+
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "DicomAssociation - " + command + " to AET \"" +
+                             parameters.GetRemoteModality().GetApplicationEntityTitle() +
+                             "\": " + info);
+    }
+  }
+    
+
+  void DicomAssociation::ReportStorageCommitment(
+    const DicomAssociationParameters& parameters,
+    const std::string& transactionUid,
+    const std::vector& sopClassUids,
+    const std::vector& sopInstanceUids,
+    const std::vector& failureReasons)
+  {
+    if (sopClassUids.size() != sopInstanceUids.size() ||
+        sopClassUids.size() != failureReasons.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+
+    std::vector successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids;
+    std::vector failedReasons;
+
+    successSopClassUids.reserve(sopClassUids.size());
+    successSopInstanceUids.reserve(sopClassUids.size());
+    failedSopClassUids.reserve(sopClassUids.size());
+    failedSopInstanceUids.reserve(sopClassUids.size());
+    failedReasons.reserve(sopClassUids.size());
+
+    for (size_t i = 0; i < sopClassUids.size(); i++)
+    {
+      switch (failureReasons[i])
+      {
+        case StorageCommitmentFailureReason_Success:
+          successSopClassUids.push_back(sopClassUids[i]);
+          successSopInstanceUids.push_back(sopInstanceUids[i]);
+          break;
+
+        case StorageCommitmentFailureReason_ProcessingFailure:
+        case StorageCommitmentFailureReason_NoSuchObjectInstance:
+        case StorageCommitmentFailureReason_ResourceLimitation:
+        case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
+        case StorageCommitmentFailureReason_ClassInstanceConflict:
+        case StorageCommitmentFailureReason_DuplicateTransactionUID:
+          failedSopClassUids.push_back(sopClassUids[i]);
+          failedSopInstanceUids.push_back(sopInstanceUids[i]);
+          failedReasons.push_back(failureReasons[i]);
+          break;
+
+        default:
+        {
+          char buf[16];
+          sprintf(buf, "%04xH", failureReasons[i]);
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Unsupported failure reason for storage commitment: " + std::string(buf));
+        }
+      }
+    }
+    
+    DicomAssociation association;
+
+    {
+      std::list transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
+
+      association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
+                                             transferSyntaxes, DicomAssociationRole_Scp);
+    }
+      
+    association.Open(parameters);
+
+    /**
+     * N-EVENT-REPORT
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
+     *
+     * Status code:
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
+     **/
+
+    /**
+     * Send the "EVENT_REPORT_RQ" request
+     **/
+
+    CLOG(INFO, DICOM) << "Reporting modality \""
+                      << parameters.GetRemoteModality().GetApplicationEntityTitle()
+                      << "\" about storage commitment transaction: " << transactionUid
+                      << " (" << successSopClassUids.size() << " successes, " 
+                      << failedSopClassUids.size() << " failures)";
+    const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
+      
+    {
+      T_DIMSE_Message message;
+      memset(&message, 0, sizeof(message));
+      message.CommandField = DIMSE_N_EVENT_REPORT_RQ;
+
+      T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ;
+      content.MessageID = messageId;
+      strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+      strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+      content.DataSetType = DIMSE_DATASET_PRESENT;
+
+      DcmDataset dataset;
+      if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      {
+        std::vector empty;
+        FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids,
+                        successSopInstanceUids, empty, false);
+      }
+
+      // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+      if (failedSopClassUids.empty())
+      {
+        content.EventTypeID = 1;  // "Storage Commitment Request Successful"
+      }
+      else
+      {
+        content.EventTypeID = 2;  // "Storage Commitment Request Complete - Failures Exist"
+
+        // Failure reason
+        // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+        FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids,
+                        failedSopInstanceUids, failedReasons, true);
+      }
+
+      int presID = ASC_findAcceptedPresentationContextID(
+        &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
+      if (presID == 0)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to send N-EVENT-REPORT request to AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      {
+        std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+        dataset.print(s);
+
+        OFString str;
+        CLOG(TRACE, DICOM) << "Sending Storage Commitment Report:" << std::endl
+                           << DIMSE_dumpMessage(str, message, DIMSE_OUTGOING) << std::endl
+                           << s.str();
+      }
+
+      if (!DIMSE_sendMessageUsingMemoryData(
+            &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
+            &dataset, NULL /* callback */, NULL /* callback context */,
+            NULL /* commandSet */).good())
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol);
+      }
+    }
+
+    /**
+     * Read the "EVENT_REPORT_RSP" response
+     **/
+
+    {
+      T_ASC_PresentationContextID presID = 0;
+      T_DIMSE_Message message;
+
+      if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
+                                (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                parameters.GetTimeout(), &presID, &message,
+                                NULL /* no statusDetail */).good() ||
+          message.CommandField != DIMSE_N_EVENT_REPORT_RSP)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to read N-EVENT-REPORT response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      {
+        OFString str;
+        CLOG(TRACE, DICOM) << "Received Storage Commitment Report Response:" << std::endl
+                           << DIMSE_dumpMessage(str, message, DIMSE_INCOMING, NULL, presID);
+      }
+      
+      const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP;
+      if (content.MessageIDBeingRespondedTo != messageId ||
+          !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) ||
+          !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) ||
+          //(content.opts & O_NEVENTREPORT_EVENTTYPEID) ||  // Pedantic test - The "content.EventTypeID" is not used by Orthanc
+          std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
+          content.DataSetType != DIMSE_DATASET_NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Badly formatted N-EVENT-REPORT response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      if (content.DimseStatus != 0 /* success */)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "The request cannot be handled by remote AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+    }
+
+    association.Close();
+  }
+
+    
+  void DicomAssociation::RequestStorageCommitment(
+    const DicomAssociationParameters& parameters,
+    const std::string& transactionUid,
+    const std::vector& sopClassUids,
+    const std::vector& sopInstanceUids)
+  {
+    if (sopClassUids.size() != sopInstanceUids.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    for (size_t i = 0; i < sopClassUids.size(); i++)
+    {
+      if (sopClassUids[i].empty() ||
+          sopInstanceUids[i].empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "The SOP class/instance UIDs cannot be empty, found: \"" +
+                               sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\"");
+      }
+    }
+
+    if (transactionUid.size() < 5 ||
+        transactionUid.substr(0, 5) != "2.25.")
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    DicomAssociation association;
+
+    {
+      std::list transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
+      
+      // association.SetRole(DicomAssociationRole_Default);
+      association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
+                                             transferSyntaxes, DicomAssociationRole_Default);
+    }
+      
+    association.Open(parameters);
+      
+    /**
+     * N-ACTION
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
+     *
+     * Status code:
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
+     **/
+
+    /**
+     * Send the "N_ACTION_RQ" request
+     **/
+
+    CLOG(INFO, DICOM) << "Request to modality \""
+                      << parameters.GetRemoteModality().GetApplicationEntityTitle()
+                      << "\" about storage commitment for " << sopClassUids.size()
+                      << " instances, with transaction UID: " << transactionUid;
+    const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
+      
+    {
+      T_DIMSE_Message message;
+      memset(&message, 0, sizeof(message));
+      message.CommandField = DIMSE_N_ACTION_RQ;
+
+      T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ;
+      content.MessageID = messageId;
+      strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+      strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+      content.ActionTypeID = 1;  // "Request Storage Commitment"
+      content.DataSetType = DIMSE_DATASET_PRESENT;
+
+      DcmDataset dataset;
+      if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      {
+        std::vector empty;
+        FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false);
+      }
+          
+      int presID = ASC_findAcceptedPresentationContextID(
+        &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
+      if (presID == 0)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to send N-ACTION request to AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      {
+        std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+        dataset.print(s);
+
+        OFString str;
+        CLOG(TRACE, DICOM) << "Sending Storage Commitment Request:" << std::endl
+                           << DIMSE_dumpMessage(str, message, DIMSE_OUTGOING) << std::endl
+                           << s.str();
+      }
+
+      if (!DIMSE_sendMessageUsingMemoryData(
+            &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
+            &dataset, NULL /* callback */, NULL /* callback context */,
+            NULL /* commandSet */).good())
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol);
+      }
+    }
+
+    /**
+     * Read the "N_ACTION_RSP" response
+     **/
+
+    {
+      T_ASC_PresentationContextID presID = 0;
+      T_DIMSE_Message message;
+        
+      if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
+                                (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                parameters.GetTimeout(), &presID, &message,
+                                NULL /* no statusDetail */).good() ||
+          message.CommandField != DIMSE_N_ACTION_RSP)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to read N-ACTION response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP;
+      if (content.MessageIDBeingRespondedTo != messageId ||
+          !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) ||
+          !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) ||
+          //(content.opts & O_NACTION_ACTIONTYPEID) ||  // Pedantic test - The "content.ActionTypeID" is not used by Orthanc
+          std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
+          content.DataSetType != DIMSE_DATASET_NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Badly formatted N-ACTION response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      {
+        OFString str;
+        CLOG(TRACE, DICOM) << "Received Storage Commitment Request Response:" << std::endl
+                           << DIMSE_dumpMessage(str, message, DIMSE_INCOMING, NULL, presID);
+      }
+
+      if (content.DimseStatus != 0 /* success */)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "The request cannot be handled by remote AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+    }
+
+    association.Close();
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h
new file mode 100644
index 0000000..8fccfd0
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h
@@ -0,0 +1,157 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+#  include "Internals/DicomTls.h"
+#endif
+
+#include "../Compatibility.h"  // For std::unique_ptr<>
+#include "DicomAssociationParameters.h"
+
+#include 
+
+#include    // For uint8_t
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class DicomAssociation : public boost::noncopyable
+  {
+  private:
+    // This is the maximum number of presentation context IDs (the
+    // number of odd integers between 1 and 255)
+    // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html
+    static const size_t MAX_PROPOSED_PRESENTATIONS = 128;
+    
+    struct ProposedPresentationContext
+    {
+      std::string                    abstractSyntax_;
+      std::list transferSyntaxes_;
+      DicomAssociationRole           role_;
+    };
+
+    typedef std::map >
+    AcceptedPresentationContexts;
+
+    bool                                      isOpen_;
+    std::vector  proposed_;
+    AcceptedPresentationContexts              accepted_;
+    T_ASC_Network*                            net_;
+    T_ASC_Parameters*                         params_;
+    T_ASC_Association*                        assoc_;
+
+#if ORTHANC_ENABLE_SSL == 1
+    std::unique_ptr     tls_;
+#endif
+
+    void CheckConnecting(const DicomAssociationParameters& parameters,
+                         const OFCondition& cond);
+    
+    void CloseInternal();
+
+    void AddAccepted(const std::string& abstractSyntax,
+                     DicomTransferSyntax syntax,
+                     uint8_t presentationContextId);
+
+  public:
+    DicomAssociation();
+
+    ~DicomAssociation();
+
+    bool IsOpen() const
+    {
+      return isOpen_;
+    }
+
+    void ClearPresentationContexts();
+
+    void Open(const DicomAssociationParameters& parameters);
+    
+    void Close();
+
+    bool LookupAcceptedPresentationContext(
+      std::map& target,
+      const std::string& abstractSyntax) const;
+
+    void ProposeGenericPresentationContext(const std::string& abstractSyntax);
+
+    void ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                           DicomAssociationRole role);
+
+    void ProposePresentationContext(const std::string& abstractSyntax,
+                                    DicomTransferSyntax transferSyntax,
+                                    DicomAssociationRole role);
+
+    void ProposePresentationContext(const std::string& abstractSyntax,
+                                    DicomTransferSyntax transferSyntax);
+
+    size_t GetRemainingPropositions() const;
+
+    void ProposePresentationContext(
+      const std::string& abstractSyntax,
+      const std::list& transferSyntaxes);
+
+    void ProposePresentationContext(
+      const std::string& abstractSyntax,
+      const std::list& transferSyntaxes,
+      DicomAssociationRole role);
+
+    T_ASC_Association& GetDcmtkAssociation() const;
+
+    T_ASC_Network& GetDcmtkNetwork() const;
+
+    bool GetAssociationParameters(std::string& remoteAet,
+                                  std::string& remoteIp,
+                                  std::string& calledAet) const;
+
+    static void CheckCondition(const OFCondition& cond,
+                               const DicomAssociationParameters& parameters,
+                               const std::string& command);
+
+    static void ReportStorageCommitment(
+      const DicomAssociationParameters& parameters,
+      const std::string& transactionUid,
+      const std::vector& sopClassUids,
+      const std::vector& sopInstanceUids,
+      const std::vector& failureReasons);
+    
+    static void RequestStorageCommitment(
+      const DicomAssociationParameters& parameters,
+      const std::string& transactionUid,
+      const std::vector& sopClassUids,
+      const std::vector& sopInstanceUids);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp
new file mode 100644
index 0000000..5d196f8
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp
@@ -0,0 +1,522 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomAssociationParameters.h"
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "../SystemToolbox.h"
+#include "NetworkingCompatibility.h"
+
+#include   // For ASC_DEFAULTMAXPDU
+
+#include 
+
+// By default, the default timeout for client DICOM connections is set to 10 seconds
+static boost::mutex  defaultConfigurationMutex_;
+static uint32_t      defaultTimeout_ = 10;
+static std::string   defaultOwnPrivateKeyPath_;
+static std::string   defaultOwnCertificatePath_;
+static std::string   defaultTrustedCertificatesPath_;
+static unsigned int  defaultMaximumPduLength_ = ASC_DEFAULTMAXPDU;
+static bool          defaultRemoteCertificateRequired_ = true;
+static unsigned int  minimumTlsVersion_ = 0;
+static std::set acceptedCiphers_;
+
+namespace Orthanc
+{
+  void DicomAssociationParameters::CheckHost(const std::string& host)
+  {
+    if (host.size() > HOST_NAME_MAX - 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Invalid host name (too long): " + host);
+    }
+  }
+
+  
+  uint32_t DicomAssociationParameters::GetDefaultTimeout()
+  {
+    boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+    return defaultTimeout_;
+  }
+
+
+  void DicomAssociationParameters::SetDefaultParameters()
+  {
+    boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+    timeout_ = defaultTimeout_;
+    ownPrivateKeyPath_ = defaultOwnPrivateKeyPath_;
+    ownCertificatePath_ = defaultOwnCertificatePath_;
+    trustedCertificatesPath_ = defaultTrustedCertificatesPath_;
+    maximumPduLength_ = defaultMaximumPduLength_;
+    remoteCertificateRequired_ = defaultRemoteCertificateRequired_;
+  }
+
+
+  DicomAssociationParameters::DicomAssociationParameters() :
+    localAet_("ORTHANC"),
+    timeout_(0),  // Will be set by SetDefaultParameters()
+    maximumPduLength_(0)  // Will be set by SetDefaultParameters()
+  {
+    SetDefaultParameters();
+    remote_.SetApplicationEntityTitle("ANY-SCP");
+  }
+
+    
+  DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet,
+                                                         const RemoteModalityParameters& remote) :
+    localAet_(localAet),
+    timeout_(0),  // Will be set by SetDefaultParameters()
+    maximumPduLength_(0)  // Will be set by SetDefaultParameters()
+  {
+    SetDefaultParameters();
+    SetRemoteModality(remote);
+  }
+
+  const std::string &DicomAssociationParameters::GetLocalApplicationEntityTitle() const
+  {
+    return localAet_;
+  }
+
+  void DicomAssociationParameters::SetLocalApplicationEntityTitle(const std::string &aet)
+  {
+    localAet_ = aet;
+  }
+
+  const RemoteModalityParameters &DicomAssociationParameters::GetRemoteModality() const
+  {
+    return remote_;
+  }
+
+
+  void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    CheckHost(remote.GetHost());
+    remote_ = remote;
+
+    if (remote.HasTimeout())
+    {
+      timeout_ = remote.GetTimeout();
+      assert(timeout_ != 0);
+    }
+  }
+
+  void DicomAssociationParameters::SetRemoteApplicationEntityTitle(const std::string &aet)
+  {
+    remote_.SetApplicationEntityTitle(aet);
+  }
+
+
+  void DicomAssociationParameters::SetRemoteHost(const std::string& host)
+  {
+    CheckHost(host);
+    remote_.SetHost(host);
+  }
+
+  void DicomAssociationParameters::SetRemotePort(uint16_t port)
+  {
+    remote_.SetPortNumber(port);
+  }
+
+  void DicomAssociationParameters::SetRemoteManufacturer(ModalityManufacturer manufacturer)
+  {
+    remote_.SetManufacturer(manufacturer);
+  }
+
+
+  bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const
+  {
+    return (localAet_ == other.localAet_ &&
+            remote_.GetApplicationEntityTitle() == other.remote_.GetApplicationEntityTitle() &&
+            remote_.GetHost() == other.remote_.GetHost() &&
+            remote_.GetPortNumber() == other.remote_.GetPortNumber() &&
+            remote_.GetManufacturer() == other.remote_.GetManufacturer() &&
+            timeout_ == other.timeout_ &&
+            ownPrivateKeyPath_ == other.ownPrivateKeyPath_ &&
+            ownCertificatePath_ == other.ownCertificatePath_ &&
+            trustedCertificatesPath_ == other.trustedCertificatesPath_ &&
+            maximumPduLength_ == other.maximumPduLength_);
+  }
+
+  void DicomAssociationParameters::SetTimeout(uint32_t seconds)
+  {
+    timeout_ = seconds;
+  }
+
+  uint32_t DicomAssociationParameters::GetTimeout() const
+  {
+    return timeout_;
+  }
+
+  bool DicomAssociationParameters::HasTimeout() const
+  {
+    return timeout_ != 0;
+  }
+
+
+  void DicomAssociationParameters::CheckDicomTlsConfiguration() const
+  {
+    if (!remote_.IsDicomTlsEnabled())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "DICOM TLS is not enabled");
+    }
+    else if (ownPrivateKeyPath_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "DICOM TLS - No path to the private key of the local certificate was provided");
+    }
+    else if (ownCertificatePath_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "DICOM TLS - No path to the local certificate was provided");
+    }
+    else if (remoteCertificateRequired_ && trustedCertificatesPath_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "DICOM TLS - No path to the trusted remote certificates was provided");
+    }
+  }
+  
+  void DicomAssociationParameters::SetOwnCertificatePath(const std::string& privateKeyPath,
+                                                         const std::string& certificatePath)
+  {
+    ownPrivateKeyPath_ = privateKeyPath;
+    ownCertificatePath_ = certificatePath;
+  }
+
+  void DicomAssociationParameters::SetTrustedCertificatesPath(const std::string& path)
+  {
+    trustedCertificatesPath_ = path;
+  }
+
+  const std::string& DicomAssociationParameters::GetOwnPrivateKeyPath() const
+  {
+    CheckDicomTlsConfiguration();
+    return ownPrivateKeyPath_;
+  }
+    
+  const std::string& DicomAssociationParameters::GetOwnCertificatePath() const
+  {
+    CheckDicomTlsConfiguration();
+    return ownCertificatePath_;
+  }
+
+  const std::string& DicomAssociationParameters::GetTrustedCertificatesPath() const
+  {
+    CheckDicomTlsConfiguration();
+    return trustedCertificatesPath_;
+  }
+
+  unsigned int DicomAssociationParameters::GetMaximumPduLength() const
+  {
+    return maximumPduLength_;
+  }
+
+  void DicomAssociationParameters::SetMaximumPduLength(unsigned int pdu)
+  {
+    CheckMaximumPduLength(pdu);
+    maximumPduLength_ = pdu;
+  }
+
+  void DicomAssociationParameters::SetRemoteCertificateRequired(bool required)
+  {
+    remoteCertificateRequired_ = required;
+  }
+
+  bool DicomAssociationParameters::IsRemoteCertificateRequired() const
+  {
+    return remoteCertificateRequired_;
+  }
+
+  unsigned int DicomAssociationParameters::GetMinimumTlsVersion()
+  {
+    return minimumTlsVersion_;
+  }
+  
+  void DicomAssociationParameters::SetMinimumTlsVersion(unsigned int version)
+  {
+    minimumTlsVersion_ = version;
+  }
+
+  void DicomAssociationParameters::SetAcceptedCiphers(const std::set& acceptedCiphers)
+  {
+    acceptedCiphers_ = acceptedCiphers;
+  }
+
+  const std::set& DicomAssociationParameters::GetAcceptedCiphers()
+  {
+    return acceptedCiphers_;
+  }
+
+
+  static const char* const LOCAL_AET = "LocalAet";
+  static const char* const REMOTE = "Remote";
+  static const char* const TIMEOUT = "Timeout";                           // New in Orthanc in 1.7.0
+  static const char* const OWN_PRIVATE_KEY = "OwnPrivateKey";             // New in Orthanc 1.9.0
+  static const char* const OWN_CERTIFICATE = "OwnCertificate";            // New in Orthanc 1.9.0
+  static const char* const TRUSTED_CERTIFICATES = "TrustedCertificates";  // New in Orthanc 1.9.0
+  static const char* const MAXIMUM_PDU_LENGTH = "MaximumPduLength";       // New in Orthanc 1.9.0
+  static const char* const REMOTE_CERTIFICATE_REQUIRED = "RemoteCertificateRequired";  // New in Orthanc 1.9.3
+
+  
+  void DicomAssociationParameters::SerializeJob(Json::Value& target) const
+  {
+    if (target.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      target[LOCAL_AET] = localAet_;
+      remote_.Serialize(target[REMOTE], true /* force advanced format */);
+      target[TIMEOUT] = timeout_;
+      target[MAXIMUM_PDU_LENGTH] = maximumPduLength_;
+      target[REMOTE_CERTIFICATE_REQUIRED] = remoteCertificateRequired_;
+
+      // Don't write the DICOM TLS parameters if they are not required
+      if (ownPrivateKeyPath_.empty())
+      {
+        target.removeMember(OWN_PRIVATE_KEY);
+      }
+      else
+      {
+        target[OWN_PRIVATE_KEY] = ownPrivateKeyPath_;
+      }
+      
+      if (ownCertificatePath_.empty())
+      {
+        target.removeMember(OWN_CERTIFICATE);
+      }
+      else
+      {
+        target[OWN_CERTIFICATE] = ownCertificatePath_;
+      }
+      
+      if (trustedCertificatesPath_.empty())
+      {
+        target.removeMember(TRUSTED_CERTIFICATES);
+      }
+      else
+      {
+        target[TRUSTED_CERTIFICATES] = trustedCertificatesPath_;
+      }
+    }
+  }
+
+
+  DicomAssociationParameters DicomAssociationParameters::UnserializeJob(const Json::Value& serialized)
+  {
+    if (serialized.type() == Json::objectValue)
+    {
+      DicomAssociationParameters result;
+
+      if (!serialized.isMember(REMOTE))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      result.remote_ = RemoteModalityParameters(serialized[REMOTE]);
+      result.localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
+      result.timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, GetDefaultTimeout());
+
+      // The calls to "isMember()" below are for compatibility with Orthanc <= 1.8.2 serialization
+      if (serialized.isMember(MAXIMUM_PDU_LENGTH))
+      {
+        result.maximumPduLength_ = SerializationToolbox::ReadUnsignedInteger(
+          serialized, MAXIMUM_PDU_LENGTH, defaultMaximumPduLength_);
+      }
+
+      if (serialized.isMember(OWN_PRIVATE_KEY))
+      {
+        result.ownPrivateKeyPath_ = SerializationToolbox::ReadString(serialized, OWN_PRIVATE_KEY);
+      }
+      else
+      {
+        result.ownPrivateKeyPath_.clear();
+      }
+
+      if (serialized.isMember(OWN_CERTIFICATE))
+      {
+        result.ownCertificatePath_ = SerializationToolbox::ReadString(serialized, OWN_CERTIFICATE);
+      }
+      else
+      {
+        result.ownCertificatePath_.clear();
+      }
+
+      if (serialized.isMember(TRUSTED_CERTIFICATES))
+      {
+        result.trustedCertificatesPath_ = SerializationToolbox::ReadString(serialized, TRUSTED_CERTIFICATES);
+      }
+      else
+      {
+        result.trustedCertificatesPath_.clear();
+      }
+
+      if (serialized.isMember(REMOTE_CERTIFICATE_REQUIRED))
+      {
+        result.remoteCertificateRequired_ = SerializationToolbox::ReadBoolean(serialized, REMOTE_CERTIFICATE_REQUIRED);
+      }
+      
+      return result;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+    
+
+  void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds)
+  {
+    CLOG(INFO, DICOM) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " 
+                      << seconds << " seconds (0 = no timeout)";
+
+    {
+      boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+      defaultTimeout_ = seconds;
+    }
+  }
+
+
+  void DicomAssociationParameters::SetDefaultOwnCertificatePath(const std::string& privateKeyPath,
+                                                                const std::string& certificatePath)
+  {
+    if (!privateKeyPath.empty() &&
+        !certificatePath.empty())
+    {
+      CLOG(INFO, DICOM) << "Setting the default TLS certificate for DICOM SCU connections: " 
+                        << privateKeyPath << " (key), " << certificatePath << " (certificate)";
+
+      if (certificatePath.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "No path to the default DICOM TLS certificate was provided");
+      }
+      
+      if (privateKeyPath.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "No path to the private key for the default DICOM TLS certificate was provided");
+      }
+      
+      if (!SystemToolbox::IsRegularFile(privateKeyPath))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + privateKeyPath);
+      }
+
+      if (!SystemToolbox::IsRegularFile(certificatePath))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + certificatePath);
+      }
+      
+      {
+        boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+        defaultOwnPrivateKeyPath_ = privateKeyPath;
+        defaultOwnCertificatePath_ = certificatePath;
+      }
+    }
+    else
+    {
+      boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+      defaultOwnPrivateKeyPath_.clear();
+      defaultOwnCertificatePath_.clear();
+    }
+  }    
+
+  
+  void DicomAssociationParameters::SetDefaultTrustedCertificatesPath(const std::string& path)
+  {
+    if (!path.empty())
+    {
+      CLOG(INFO, DICOM) << "Setting the default trusted certificates for DICOM SCU connections: " << path;
+
+      if (!SystemToolbox::IsRegularFile(path))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + path);
+      }
+      
+      {
+        boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+        defaultTrustedCertificatesPath_ = path;
+      }
+    }
+    else
+    {
+      boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+      defaultTrustedCertificatesPath_.clear();
+    }
+  }
+
+
+
+  void DicomAssociationParameters::CheckMaximumPduLength(unsigned int pdu)
+  {
+    if (pdu > ASC_MAXIMUMPDUSIZE)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Maximum PDU length must be smaller than " +
+                             boost::lexical_cast(ASC_MAXIMUMPDUSIZE));
+    }
+    else if (pdu < ASC_MINIMUMPDUSIZE)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Maximum PDU length must be greater than " +
+                             boost::lexical_cast(ASC_MINIMUMPDUSIZE));
+    }
+  }
+
+
+  void DicomAssociationParameters::SetDefaultMaximumPduLength(unsigned int pdu)
+  {
+    CheckMaximumPduLength(pdu);
+
+    {
+      boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+      defaultMaximumPduLength_ = pdu;
+    }
+  }
+
+
+  unsigned int DicomAssociationParameters::GetDefaultMaximumPduLength()
+  {
+    boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+    return defaultMaximumPduLength_;
+  }
+
+
+  void DicomAssociationParameters::SetDefaultRemoteCertificateRequired(bool required)
+  {
+    boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+    defaultRemoteCertificateRequired_ = required;
+  }
+  
+
+  bool DicomAssociationParameters::GetDefaultRemoteCertificateRequired()
+  {
+    boost::mutex::scoped_lock lock(defaultConfigurationMutex_);
+    return defaultRemoteCertificateRequired_;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h
new file mode 100644
index 0000000..d946ddb
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h
@@ -0,0 +1,140 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "RemoteModalityParameters.h"
+
+#include 
+
+class OFCondition;  // From DCMTK
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomAssociationParameters
+  {
+  private:
+    std::string               localAet_;
+    RemoteModalityParameters  remote_;
+    uint32_t                  timeout_;
+    std::string               ownPrivateKeyPath_;
+    std::string               ownCertificatePath_;
+    std::string               trustedCertificatesPath_;
+    unsigned int              maximumPduLength_;
+    bool                      remoteCertificateRequired_;  // New in 1.9.3, for DICOM TLS
+
+    static void CheckHost(const std::string& host);
+
+    void SetDefaultParameters();
+    
+    void CheckDicomTlsConfiguration() const;
+
+  public:
+    DicomAssociationParameters();
+    
+    DicomAssociationParameters(const std::string& localAet,
+                               const RemoteModalityParameters& remote);
+    
+    const std::string& GetLocalApplicationEntityTitle() const;
+
+    void SetLocalApplicationEntityTitle(const std::string& aet);
+
+    const RemoteModalityParameters& GetRemoteModality() const;
+
+    void SetRemoteModality(const RemoteModalityParameters& parameters);
+    
+    void SetRemoteApplicationEntityTitle(const std::string& aet);
+
+    void SetRemoteHost(const std::string& host);
+
+    void SetRemotePort(uint16_t port);
+
+    void SetRemoteManufacturer(ModalityManufacturer manufacturer);
+
+    bool IsEqual(const DicomAssociationParameters& other) const;
+
+    // Setting it to "0" disables the timeout (infinite wait)
+    void SetTimeout(uint32_t seconds);
+
+    uint32_t GetTimeout() const;
+
+    bool HasTimeout() const;
+
+    // This corresponds to the "--enable-tls" or "+tls" argument of
+    // the command-line tools of DCMTK. Both files must be in the PEM format.
+    // The private key file must not be password-protected.
+    void SetOwnCertificatePath(const std::string& privateKeyPath,
+                               const std::string& certificatePath);
+
+    // This corresponds to the "--add-cert-file" or "+cf" argument of
+    // the command-line tools of DCMTK. The file must contain a list
+    // of PEM certificates.
+    void SetTrustedCertificatesPath(const std::string& path);
+
+    const std::string& GetOwnPrivateKeyPath() const;
+    
+    const std::string& GetOwnCertificatePath() const;
+
+    const std::string& GetTrustedCertificatesPath() const;
+
+    unsigned int GetMaximumPduLength() const;
+
+    void SetMaximumPduLength(unsigned int pdu);
+    
+    void SetRemoteCertificateRequired(bool required);
+
+    bool IsRemoteCertificateRequired() const;
+
+    void SerializeJob(Json::Value& target) const;
+
+    static DicomAssociationParameters UnserializeJob(const Json::Value& serialized);
+    
+    static void SetDefaultTimeout(uint32_t seconds);
+
+    static uint32_t GetDefaultTimeout();
+
+    static void SetDefaultOwnCertificatePath(const std::string& privateKeyPath,
+                                             const std::string& certificatePath);
+
+    static void SetDefaultTrustedCertificatesPath(const std::string& path);
+
+    static void CheckMaximumPduLength(unsigned int pdu);
+
+    static void SetDefaultMaximumPduLength(unsigned int pdu);
+
+    static unsigned int GetDefaultMaximumPduLength();
+
+    static void SetDefaultRemoteCertificateRequired(bool required);
+
+    static bool GetDefaultRemoteCertificateRequired();
+
+    static void SetMinimumTlsVersion(unsigned int version);
+
+    static unsigned int GetMinimumTlsVersion();
+
+    static void SetAcceptedCiphers(const std::set& acceptedCiphers);
+
+    static const std::set& GetAcceptedCiphers();
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp
new file mode 100644
index 0000000..1351654
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp
@@ -0,0 +1,981 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomControlUserConnection.h"
+
+#include "../Compatibility.h"
+#include "../DicomFormat/DicomArray.h"
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "DicomAssociation.h"
+
+#include 
+#include 
+
+namespace Orthanc
+{
+  static void TestAndCopyTag(DicomMap& result,
+                             const DicomMap& source,
+                             const DicomTag& tag)
+  {
+    if (!source.HasTag(tag))
+    {
+      throw OrthancException(ErrorCode_BadRequest, "Missing tag " + tag.Format());
+    }
+    else
+    {
+      result.SetValue(tag, source.GetValue(tag));
+    }
+  }
+
+
+  namespace
+  {
+    struct FindPayload
+    {
+      DicomFindAnswers* answers;
+      const char*       level;
+      bool              isWorklist;
+    };
+  }
+
+
+  static void FindCallback(
+    /* in */
+    void *callbackData,
+    T_DIMSE_C_FindRQ *request,      /* original find request */
+    int responseCount,
+    T_DIMSE_C_FindRSP *response,    /* pending response received */
+    DcmDataset *responseIdentifiers /* pending response identifiers */
+    )
+  {
+    if (response != NULL)
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Received Find Response " << responseCount << ":" << std::endl
+                         << DIMSE_dumpMessage(str, *response, DIMSE_INCOMING);
+    }
+      
+    if (responseIdentifiers != NULL)
+    {
+      std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+      responseIdentifiers->print(s);
+      CLOG(TRACE, DICOM) << "Response Identifiers "  << responseCount << ":" << std::endl << s.str();
+    }
+    
+    if (responseIdentifiers != NULL)
+    {
+      FindPayload& payload = *reinterpret_cast(callbackData);
+
+      if (payload.isWorklist)
+      {
+        const ParsedDicomFile answer(*responseIdentifiers);
+        payload.answers->Add(answer);
+      }
+      else
+      {
+        DicomMap m;
+        std::set ignoreTagLength;
+        FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers, 0 /* don't truncate tags */, ignoreTagLength);
+        
+        if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+        {
+          m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
+        }
+
+        payload.answers->Add(m);
+      }
+    }
+  }
+
+
+  static void NormalizeFindQuery(DicomMap& fixedQuery,
+                                 ResourceType level,
+                                 const DicomMap& fields)
+  {
+    std::set allowedTags;
+
+    // WARNING: Do not add "break" or reorder items in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
+
+      case ResourceType_Series:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
+
+      case ResourceType_Study:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
+
+      case ResourceType_Patient:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
+        break;
+
+      case ResourceType_Study:
+        allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
+        allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+        break;
+
+      case ResourceType_Series:
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
+        break;
+
+      default:
+        break;
+    }
+
+    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
+    DicomArray query(fields);
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      const DicomTag& tag = query.GetElement(i).GetTag();
+      if (allowedTags.find(tag) == allowedTags.end())
+      {
+        CLOG(WARNING, DICOM) << "Tag not allowed for this C-Find level, will be ignored: ("
+                             << tag.Format() << ")";
+      }
+      else
+      {
+        fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
+      }
+    }
+  }
+
+
+
+  static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
+                                             ModalityManufacturer manufacturer)
+  {
+    // Fix outgoing C-Find requests issue for Syngo.Via and its
+    // solution was reported by Emsy Chan by private mail on
+    // 2015-06-17. According to Robert van Ommen (2015-11-30), the
+    // same fix is required for Agfa Impax. This was generalized for
+    // generic manufacturer since it seems to affect PhilipsADW,
+    // GEWAServer as well:
+    // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=31
+
+    switch (manufacturer)
+    {
+      case ModalityManufacturer_GenericNoWildcardInDates:
+      case ModalityManufacturer_GenericNoUniversalWildcard:
+      {
+        std::unique_ptr fix(fields.Clone());
+
+        std::set tags;
+        fix->GetTags(tags);
+
+        for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it)
+        {
+          // Replace a "*" wildcard query by an empty query ("") for
+          // "date" or "all" value representations depending on the
+          // type of manufacturer.
+          if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
+              (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
+               FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
+          {
+            const DicomValue* value = fix->TestAndGetValue(*it);
+
+            if (value != NULL && 
+                !value->IsNull() &&
+                value->GetContent() == "*")
+            {
+              fix->SetValue(*it, "", false);
+            }
+          }
+        }
+
+        return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(),
+                                   false /* be strict */);
+      }
+
+      default:
+        return new ParsedDicomFile(fields, GetDefaultDicomEncoding(),
+                                   false /* be strict */);
+    }
+  }
+
+
+
+  void DicomControlUserConnection::SetupPresentationContexts(
+    ScuOperationFlags scuOperation,
+    const std::set& acceptedStorageSopClasses,
+    const std::list& proposedStorageTransferSyntaxes)
+  {
+    assert(association_.get() != NULL);
+
+    if ((scuOperation & ScuOperationFlags_Echo) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindPatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindWorklist) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MovePatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MoveStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_Get) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel);
+      association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel);
+
+      if (acceptedStorageSopClasses.size() == 0)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls); // the acceptedStorageSopClassUids should always be defined for a C-Get
+      }
+
+      for (std::set::const_iterator it = acceptedStorageSopClasses.begin(); it != acceptedStorageSopClasses.end(); ++it)
+      {
+        association_->ProposePresentationContext(*it, proposedStorageTransferSyntaxes, DicomAssociationRole_Scp);
+      }
+    }
+  }
+    
+
+  void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers,
+                                                DcmDataset* dataset,
+                                                const char* sopClass,
+                                                bool isWorklist,
+                                                const char* level)
+  {
+    assert(dataset != NULL);
+    assert(isWorklist ^ (level != NULL));
+    assert(association_.get() != NULL);
+
+    association_->Open(parameters_);
+
+    FindPayload payload;
+    payload.answers = &answers;
+    payload.level = level;
+    payload.isWorklist = isWorklist;
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(
+      &association_->GetDcmtkAssociation(), sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomFindUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_C_FindRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+
+    T_DIMSE_C_FindRSP response;
+    DcmDataset* statusDetail = NULL;
+
+#if DCMTK_VERSION_NUMBER >= 364
+    int responseCount;
+#endif
+
+    {
+      std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+      dataset->print(s);
+
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Find Request:" << std::endl
+                         << DIMSE_dumpMessage(str, request, DIMSE_OUTGOING, NULL, presID) << std::endl
+                         << s.str();
+    }
+
+    OFCondition cond = DIMSE_findUser(
+      &association_->GetDcmtkAssociation(), presID, &request, dataset,
+#if DCMTK_VERSION_NUMBER >= 364
+      responseCount,
+#endif
+      FindCallback, &payload,
+      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+      &response, &statusDetail);
+    
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Received Final Find Response:" << std::endl
+                         << DIMSE_dumpMessage(str, response, DIMSE_INCOMING);
+    }
+
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-FIND.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xFF00 &&  // Pending - Matches are continuing 
+        response.DimseStatus != 0xFF01)    // Pending - Matches are continuing 
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+
+      if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               HttpStatus_422_UnprocessableEntity,
+                               "C-FIND SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf +
+                               " (unable to process - invalid query ?)");
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf);
+      }
+    }
+  }
+
+  void MoveProgressCallback(void *callbackData,
+                            T_DIMSE_C_MoveRQ *request,
+                            int responseCount, 
+                            T_DIMSE_C_MoveRSP *response)
+  {
+    DicomControlUserConnection::IProgressListener* listener = reinterpret_cast(callbackData);
+    if (listener)
+    {
+      listener->OnProgressUpdated(response->NumberOfRemainingSubOperations,
+                                  response->NumberOfCompletedSubOperations,
+                                  response->NumberOfFailedSubOperations,
+                                  response->NumberOfWarningSubOperations);
+    }
+  }
+
+    
+  void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
+                                                ResourceType level,
+                                                const DicomMap& fields)
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    std::unique_ptr query(
+      ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+
+    const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
+    DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(level));
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomMoveUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_C_MoveRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Move Request:" << std::endl
+                         << DIMSE_dumpMessage(str, request, DIMSE_OUTGOING, NULL, presID);
+    }
+    
+    T_DIMSE_C_MoveRSP response;
+    DcmDataset* statusDetail = NULL;
+    DcmDataset* responseIdentifiers = NULL;
+    OFCondition cond = DIMSE_moveUser(
+      &association_->GetDcmtkAssociation(), presID, &request, dataset, 
+      (progressListener_ != NULL ? MoveProgressCallback : NULL), progressListener_,
+      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+      &association_->GetDcmtkNetwork(), /*subOpCallback*/ NULL, NULL,
+      &response, &statusDetail, &responseIdentifiers);
+
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    if (responseIdentifiers)
+    {
+      delete responseIdentifiers;
+    }
+
+    DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Received Final Move Response:" << std::endl
+                         << DIMSE_dumpMessage(str, response, DIMSE_INCOMING);
+
+      if (progressListener_ != NULL)
+      {
+        progressListener_->OnProgressUpdated(response.NumberOfRemainingSubOperations,
+                                             response.NumberOfCompletedSubOperations,
+                                             response.NumberOfFailedSubOperations,
+                                             response.NumberOfWarningSubOperations);
+      }
+    }
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xFF00)    // Pending - Sub-operations are continuing
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+
+      if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               HttpStatus_422_UnprocessableEntity,
+                               "C-MOVE SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf +
+                               " (unable to process - resource not found ?)");
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf);
+      }
+    }
+  }
+    
+
+  void DicomControlUserConnection::Get(const DicomMap& findResult,
+                                       CGetInstanceReceivedCallback instanceReceivedCallback,
+                                       void* callbackContext)
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    std::unique_ptr query(
+      ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer()));
+    DcmDataset* queryDataset = query->GetDcmtkObject().getDataset();
+
+    std::string remoteAet;
+    std::string remoteIp;
+    std::string calledAet;
+
+    association_->GetAssociationParameters(remoteAet, remoteIp, calledAet);
+
+    const char* sopClass = NULL;
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+    switch (level)
+    {
+      case ResourceType_Patient:
+        sopClass = UID_GETPatientRootQueryRetrieveInformationModel;
+        break;
+      case ResourceType_Study:
+      case ResourceType_Series:
+      case ResourceType_Instance:
+        sopClass = UID_GETStudyRootQueryRetrieveInformationModel;
+        break;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (cgetPresID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomGetUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_Message msgGetRequest;
+    memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest));
+    msgGetRequest.CommandField = DIMSE_C_GET_RQ;
+
+    T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ);
+    request->MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request->Priority = DIMSE_PRIORITY_MEDIUM;
+    request->DataSetType = DIMSE_DATASET_PRESENT;
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl
+                         << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID);
+    }
+    
+    OFCondition cond = DIMSE_sendMessageUsingMemoryData(
+          &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset,
+          NULL, NULL, NULL /* commandSet */);
+      
+    if (cond.bad())
+    {
+        OFString tempStr;
+        CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond);
+        // return cond;
+    }
+
+    // equivalent to handleCGETSession in DCMTK
+    bool continueSession = true;
+
+    // As long we want to continue (usually, as long as we receive more objects,
+    // i.e. the final C-GET response has not arrived yet)
+    while (continueSession)
+    {
+        T_DIMSE_Message rsp;
+        // Make sure everything is zeroed (especially options)
+        memset((char*)&rsp, 0, sizeof(rsp));
+
+        // DcmDataset* statusDetail = NULL;
+        T_ASC_PresentationContextID cmdPresId = 0;
+
+        OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &rsp,
+                                                  NULL /* statusDetail */,
+                                                  NULL /* not interested in the command set */);
+
+        if (result.bad())
+        {
+          OFString tempStr;
+          CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result);
+          // delete statusDetail;
+          break;  // TODO: return value
+        }
+        // Handle C-GET Response
+        if (rsp.CommandField == DIMSE_C_GET_RSP)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          if (progressListener_ != NULL)
+          {
+            progressListener_->OnProgressUpdated(rsp.msg.CGetRSP.NumberOfRemainingSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfCompletedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfFailedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfWarningSubOperations);
+          }
+
+          if (rsp.msg.CGetRSP.DimseStatus == 0x0000)  // final success message
+          {
+            continueSession = false;
+          }
+        }
+        // Handle C-STORE Request
+        else if (rsp.CommandField == DIMSE_C_STORE_RQ)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ);
+
+          // Check if dataset is announced correctly
+          if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL)
+          {
+            CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset";
+          }
+
+          Uint16 desiredCStoreReturnStatus = 0;
+          DcmDataset* dataObject = NULL;
+
+          // Receive dataset
+          result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &dataObject,
+                                                  NULL, NULL);
+
+          if (result.bad())
+          {
+            LOG(WARNING) << "C-GET SCU handler: Failed to receive dataset: " << result.text();
+            desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand;
+          }
+          else
+          {
+            // callback the OrthancServer with the received data
+            if (instanceReceivedCallback != NULL)
+            {
+              desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet);
+            }
+
+            // send the Store response
+            T_DIMSE_Message storeResponse;
+            memset((char*)&storeResponse, 0, sizeof(storeResponse));
+            storeResponse.CommandField         = DIMSE_C_STORE_RSP;
+
+            T_DIMSE_C_StoreRSP& storeRsp       = storeResponse.msg.CStoreRSP;
+            storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID;
+            storeRsp.DimseStatus               = desiredCStoreReturnStatus;
+            storeRsp.DataSetType               = DIMSE_DATASET_NULL;
+
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID));
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID));
+            storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID;
+
+            result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), 
+                                                      cmdPresId, 
+                                                      &storeResponse, NULL /* statusDetail */, NULL /* dataObject */,
+                                                      NULL, NULL, NULL /* commandSet */);
+            if (result.bad())
+            {
+              continueSession = false;
+            }
+            else
+            {
+              OFString tempStr;
+              CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl
+                << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId);
+            }
+          }
+        }
+        // Handle other DIMSE command (error since other command than GET/STORE not expected)
+        else
+        {
+          CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x"
+                               << std::hex << std::setfill('0') << std::setw(4)
+                               << static_cast(rsp.CommandField);
+          
+          result          = DIMSE_BADCOMMANDTYPE;
+          continueSession = false;
+        }
+
+        // delete statusDetail; // should be NULL if not existing or added to response list
+        // statusDetail = NULL;
+    }
+    /* All responses received or break signal occurred */
+
+    // return result;
+}
+
+
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    progressListener_(NULL)
+  {
+    assert((scuOperation & ScuOperationFlags_Get) == 0);  // you must provide acceptedStorageSopClassUids for Get SCU
+    std::set emptyStorageSopClasses;
+    std::list emptyStorageTransferSyntaxes;
+
+    SetupPresentationContexts(scuOperation, emptyStorageSopClasses, emptyStorageTransferSyntaxes);
+  }
+    
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                                         ScuOperationFlags scuOperation,
+                                                         const std::set& acceptedStorageSopClasses,
+                                                         const std::list& proposedStorageTransferSyntaxes) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    progressListener_(NULL)
+  {
+    SetupPresentationContexts(scuOperation, acceptedStorageSopClasses, proposedStorageTransferSyntaxes);
+  }
+    
+
+  void DicomControlUserConnection::Close()
+  {
+    assert(association_.get() != NULL);
+    association_->Close();
+  }
+
+
+  bool DicomControlUserConnection::Echo()
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    DIC_US status;
+    DicomAssociation::CheckCondition(
+      DIMSE_echoUser(&association_->GetDcmtkAssociation(),
+                     association_->GetDcmtkAssociation().nextMsgID++, 
+                     /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                     /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+                     &status, NULL),
+      parameters_, "C-ECHO");
+      
+    return status == STATUS_Success;
+  }
+
+
+  void DicomControlUserConnection::Find(DicomFindAnswers& result,
+                                        ResourceType level,
+                                        const DicomMap& originalFields,
+                                        bool normalize)
+  {
+    std::unique_ptr query;
+
+    if (normalize)
+    {
+      DicomMap fields;
+      NormalizeFindQuery(fields, level, originalFields);
+      query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
+    }
+    else
+    {
+      query.reset(new ParsedDicomFile(originalFields, GetDefaultDicomEncoding(),
+                                      false /* be strict */));
+    }
+    
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+    assert(dataset != NULL);
+
+    const char* clevel = ResourceTypeToDicomQueryRetrieveLevel(level);
+    const char* sopClass = NULL;
+
+    DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, clevel);
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Study:
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Series:
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Instance:
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+
+    const char* universal;
+    if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE)
+    {
+      universal = "*";
+    }
+    else
+    {
+      universal = "";
+    }      
+    
+
+    // Add the expected tags for this query level.
+    // WARNING: Do not reorder or add "break" in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        if (!dataset->tagExists(DCM_SOPInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
+        }
+
+      case ResourceType_Series:
+        if (!dataset->tagExists(DCM_SeriesInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
+        }
+
+      case ResourceType_Study:
+        if (!dataset->tagExists(DCM_AccessionNumber))
+        {
+          DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
+        }
+
+        if (!dataset->tagExists(DCM_StudyInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
+        }
+
+      case ResourceType_Patient:
+        if (!dataset->tagExists(DCM_PatientID))
+        {
+          DU_putStringDOElement(dataset, DCM_PatientID, universal);
+        }
+        
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(clevel != NULL && sopClass != NULL);
+    FindInternal(result, dataset, sopClass, false, clevel);
+  }
+    
+
+  void DicomControlUserConnection::Move(const std::string& targetAet,
+                                        ResourceType level,
+                                        const DicomMap& findResult)
+  {
+    DicomMap move;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
+        break;
+
+      case ResourceType_Study:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        break;
+
+      case ResourceType_Series:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        break;
+
+      case ResourceType_Instance:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    MoveInternal(targetAet, level, move);
+  }
+
+
+  void DicomControlUserConnection::Move(const std::string& targetAet,
+                                        const DicomMap& findResult)
+  {
+    if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+
+    Move(targetAet, level, findResult);
+  }
+
+
+  void DicomControlUserConnection::MovePatient(const std::string& targetAet,
+                                               const std::string& patientId)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
+    MoveInternal(targetAet, ResourceType_Patient, query);
+  }
+    
+
+  void DicomControlUserConnection::MoveStudy(const std::string& targetAet,
+                                             const std::string& studyUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    MoveInternal(targetAet, ResourceType_Study, query);
+  }
+
+    
+  void DicomControlUserConnection::MoveSeries(const std::string& targetAet,
+                                              const std::string& studyUid,
+                                              const std::string& seriesUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
+    MoveInternal(targetAet, ResourceType_Series, query);
+  }
+
+
+  void DicomControlUserConnection::MoveInstance(const std::string& targetAet,
+                                                const std::string& studyUid,
+                                                const std::string& seriesUid,
+                                                const std::string& instanceUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
+    query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
+    MoveInternal(targetAet, ResourceType_Instance, query);
+  }
+
+
+  void DicomControlUserConnection::FindWorklist(DicomFindAnswers& result,
+                                                ParsedDicomFile& query)
+  {
+    DcmDataset* dataset = query.GetDcmtkObject().getDataset();
+    const char* sopClass = UID_FINDModalityWorklistInformationModel;
+
+    FindInternal(result, dataset, sopClass, true, NULL);
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h
new file mode 100644
index 0000000..0eacd45
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h
@@ -0,0 +1,152 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#include "DicomAssociationParameters.h"
+#include "DicomFindAnswers.h"
+
+#include 
+#include 
+
+namespace Orthanc
+{
+  class DicomAssociation;  // Forward declaration for PImpl design pattern
+  
+  typedef uint16_t (*CGetInstanceReceivedCallback) (void *callbackContext,
+                                                    DcmDataset& dataset,
+                                                    const std::string& remoteAet,
+                                                    const std::string& remoteIp,
+                                                    const std::string& calledAet);
+
+
+  enum ScuOperationFlags
+  {
+    ScuOperationFlags_Echo = 1 << 0,
+    ScuOperationFlags_FindPatient = 1 << 1,
+    ScuOperationFlags_FindStudy = 1 << 2,
+    ScuOperationFlags_FindWorklist = 1 << 3,
+    ScuOperationFlags_MoveStudy = 1 << 4,
+    ScuOperationFlags_MovePatient = 1 << 5,
+    // C-Store is not using DicomControlUserConnection but DicomStoreUserConnection
+    ScuOperationFlags_Get = 1 << 6,
+
+    ScuOperationFlags_Find = ScuOperationFlags_FindPatient | ScuOperationFlags_FindStudy | ScuOperationFlags_FindWorklist,
+    ScuOperationFlags_Move = ScuOperationFlags_MoveStudy | ScuOperationFlags_MovePatient,
+    ScuOperationFlags_All = ScuOperationFlags_Echo | ScuOperationFlags_Find | ScuOperationFlags_Move | ScuOperationFlags_Get
+  };
+
+  class DicomControlUserConnection : public boost::noncopyable
+  {
+  public:
+    class IProgressListener
+    {
+    public:
+      virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                     uint16_t nbCompletedSubOperations,
+                                     uint16_t nbFailedSubOperations,
+                                     uint16_t nbWarningSubOperations) = 0;
+    };
+
+  private:
+    DicomAssociationParameters           parameters_;
+    boost::shared_ptr  association_;
+    IProgressListener*                   progressListener_;
+
+    void SetupPresentationContexts(ScuOperationFlags scuOperation,
+                                   const std::set& acceptedStorageSopClasses,
+                                   const std::list& proposedStorageTransferSyntaxes);
+
+    void FindInternal(DicomFindAnswers& answers,
+                      DcmDataset* dataset,
+                      const char* sopClass,
+                      bool isWorklist,
+                      const char* level);
+    
+    void MoveInternal(const std::string& targetAet,
+                      ResourceType level,
+                      const DicomMap& fields);
+    
+  public:
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation);
+
+    // specific constructor for CGet SCU
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                        ScuOperationFlags scuOperation,
+                                        const std::set& acceptedStorageSopClasses,
+                                        const std::list& proposedStorageTransferSyntaxes);
+
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void Close();
+
+    bool Echo();
+
+    void SetProgressListener(IProgressListener* progressListener)
+    {
+      progressListener_ = progressListener;
+    }
+
+    void Find(DicomFindAnswers& result,
+              ResourceType level,
+              const DicomMap& originalFields,
+              bool normalize);
+
+    void Get(const DicomMap& getQuery,
+             CGetInstanceReceivedCallback instanceReceivedCallback,
+             void* callbackContext);
+
+    void Move(const std::string& targetAet,
+              ResourceType level,
+              const DicomMap& findResult);
+    
+    void Move(const std::string& targetAet,
+              const DicomMap& findResult);
+    
+    void MovePatient(const std::string& targetAet,
+                     const std::string& patientId);
+
+    void MoveStudy(const std::string& targetAet,
+                   const std::string& studyUid);
+
+    void MoveSeries(const std::string& targetAet,
+                    const std::string& studyUid,
+                    const std::string& seriesUid);
+
+    void MoveInstance(const std::string& targetAet,
+                      const std::string& studyUid,
+                      const std::string& seriesUid,
+                      const std::string& instanceUid);
+
+    void FindWorklist(DicomFindAnswers& result,
+                      ParsedDicomFile& query);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp
new file mode 100644
index 0000000..14848ff
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp
@@ -0,0 +1,256 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomFindAnswers.h"
+
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../OrthancException.h"
+
+#include 
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  void DicomFindAnswers::AddAnswerInternal(ParsedDicomFile* answer)
+  {
+    std::unique_ptr protection(answer);
+
+    if (isWorklist_)
+    {
+      // These lines are necessary when serving worklists, otherwise
+      // Orthanc does not behave as "wlmscpfs"
+      protection->Remove(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID);
+      protection->Remove(DICOM_TAG_SOP_INSTANCE_UID);
+    }
+
+    protection->ChangeEncoding(encoding_);
+
+    answers_.push_back(protection.release());
+  }
+
+
+  DicomFindAnswers::DicomFindAnswers(bool isWorklist) : 
+    encoding_(GetDefaultDicomEncoding()),
+    isWorklist_(isWorklist),
+    complete_(true)
+  {
+  }
+
+  DicomFindAnswers::~DicomFindAnswers()
+  {
+    Clear();
+  }
+
+  Encoding DicomFindAnswers::GetEncoding() const
+  {
+    return encoding_;
+  }
+
+
+  void DicomFindAnswers::SetEncoding(Encoding encoding)
+  {
+    for (size_t i = 0; i < answers_.size(); i++)
+    {
+      assert(answers_[i] != NULL);
+      answers_[i]->ChangeEncoding(encoding);
+    }
+
+    encoding_ = encoding;
+  }
+
+
+  void DicomFindAnswers::SetWorklist(bool isWorklist)
+  {
+    if (answers_.empty())
+    {
+      isWorklist_ = isWorklist;
+    }
+    else
+    {
+      // This set of answers is not empty anymore, cannot change its type
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  bool DicomFindAnswers::IsWorklist() const
+  {
+    return isWorklist_;
+  }
+
+
+  void DicomFindAnswers::Clear()
+  {
+    for (size_t i = 0; i < answers_.size(); i++)
+    {
+      assert(answers_[i] != NULL);
+      delete answers_[i];
+    }
+
+    answers_.clear();
+  }
+
+
+  void DicomFindAnswers::Reserve(size_t size)
+  {
+    if (size > answers_.size())
+    {
+      answers_.reserve(size);
+    }
+  }
+
+
+  void DicomFindAnswers::Add(const DicomMap& map)
+  {
+    // We use the permissive mode to be tolerant wrt. invalid DICOM
+    // files that contain some tags with out-of-range values (such
+    // tags are removed from the answers)
+    AddAnswerInternal(new ParsedDicomFile(map, encoding_, true /* permissive */));
+                                          //"" /* no private creator */));
+  }
+
+
+  void DicomFindAnswers::Add(const ParsedDicomFile& dicom)
+  {
+    AddAnswerInternal(dicom.Clone(true));
+  }
+
+  void DicomFindAnswers::Add(const void* dicom,
+                             size_t size)
+  {
+    AddAnswerInternal(new ParsedDicomFile(dicom, size));
+  }
+
+  size_t DicomFindAnswers::GetSize() const
+  {
+    return answers_.size();
+  }
+
+
+  ParsedDicomFile& DicomFindAnswers::GetAnswer(size_t index) const
+  {
+    if (index < answers_.size())
+    {
+      return *answers_[index];
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const
+  {
+    // As "DicomFindAnswers" stores its content using class
+    // "ParsedDicomFile" (that internally uses "DcmFileFormat" from
+    // DCMTK), the dataset can contain tags that are reserved if
+    // storing the media on the disk, notably tag
+    // "MediaStorageSOPClassUID" (0002,0002). In this function, we
+    // remove all those tags whose group is below 0x0008. The
+    // resulting data set is clean for emission in the C-FIND SCP.
+
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
+    // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
+
+    DcmDataset& source = *GetAnswer(index).GetDcmtkObject().getDataset();
+
+    std::unique_ptr target(new DcmDataset);
+
+    for (unsigned long i = 0; i < source.card(); i++)
+    {
+      const DcmElement* element = source.getElement(i);
+      assert(element != NULL);
+
+      if (element != NULL &&
+          element->getTag().getGroup() >= 0x0008 &&
+          element->getTag().getElement() != 0x0000)
+      {
+        target->insert(dynamic_cast(element->clone()));
+      }
+    }
+    
+    return target.release();
+  }
+
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                size_t index,
+                                DicomToJsonFormat format) const
+  {
+    const ParsedDicomFile& answer = GetAnswer(index);
+    answer.DatasetToJson(target, format, DicomToJsonFlags_IncludePrivateTags, 0);
+  }
+
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                DicomToJsonFormat format) const
+  {
+    target = Json::arrayValue;
+
+    for (size_t i = 0; i < GetSize(); i++)
+    {
+      Json::Value answer;
+      ToJson(answer, i, format);
+      target.append(answer);
+    }
+  }
+
+
+  bool DicomFindAnswers::IsComplete() const
+  {
+    return complete_;
+  }
+
+  void DicomFindAnswers::SetComplete(bool isComplete)
+  {
+    complete_ = isComplete;
+  }
+
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+  void DicomFindAnswers::Add(ParsedDicomFile& dicom)
+  {
+    return Add(const_cast(dicom));
+  }
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                size_t index,
+                                bool simplify) const
+  {
+    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
+    ToJson(target, index, format);
+  }
+
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                bool simplify) const
+  {
+    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full);
+    ToJson(target, format);
+  }
+#endif
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h
new file mode 100644
index 0000000..b292d64
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h
@@ -0,0 +1,94 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../DicomParsing/ParsedDicomFile.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomFindAnswers : public boost::noncopyable
+  {
+  private:
+    Encoding                      encoding_;
+    bool                          isWorklist_;
+    std::vector answers_;
+    bool                          complete_;
+
+    void AddAnswerInternal(ParsedDicomFile* answer);
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
+    void Add(ParsedDicomFile& dicom);
+
+    void ToJson(Json::Value& target,
+                bool simplify) const;
+
+    void ToJson(Json::Value& target,
+                size_t index,
+                bool simplify) const;
+#endif
+
+  public:
+    explicit DicomFindAnswers(bool isWorklist);
+
+    ~DicomFindAnswers();
+
+    Encoding GetEncoding() const;
+
+    void SetEncoding(Encoding encoding);
+
+    void SetWorklist(bool isWorklist);
+
+    bool IsWorklist() const;
+
+    void Clear();
+
+    void Reserve(size_t index);
+
+    void Add(const DicomMap& map);
+
+    void Add(const ParsedDicomFile& dicom);
+
+    void Add(const void* dicom,
+             size_t size);
+
+    size_t GetSize() const;
+
+    ParsedDicomFile& GetAnswer(size_t index) const;
+
+    DcmDataset* ExtractDcmDataset(size_t index) const;
+
+    void ToJson(Json::Value& target,
+                DicomToJsonFormat format) const;
+
+    void ToJson(Json::Value& target,
+                size_t index,
+                DicomToJsonFormat format) const;
+
+    bool IsComplete() const;
+
+    void SetComplete(bool isComplete);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp
new file mode 100644
index 0000000..7c023a7
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp
@@ -0,0 +1,623 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomServer.h"
+
+#include "../Logging.h"
+#include "../MultiThreading/RunnableWorkersPool.h"
+#include "../OrthancException.h"
+#include "../SystemToolbox.h"
+#include "../Toolbox.h"
+#include "DicomAssociationParameters.h"
+#include "Internals/CommandDispatcher.h"
+
+#include 
+
+#if ORTHANC_ENABLE_SSL == 1
+#  include "Internals/DicomTls.h"
+#endif
+
+#if defined(__linux__)
+#  include 
+#endif
+
+
+namespace Orthanc
+{
+  struct DicomServer::PImpl
+  {
+    boost::thread  thread_;
+    T_ASC_Network *network_;
+    std::unique_ptr  workers_;
+
+#if ORTHANC_ENABLE_SSL == 1
+    std::unique_ptr tls_;
+#endif
+  };
+
+
+  void DicomServer::ServerThread(DicomServer* server,
+                                 unsigned int maximumPduLength,
+                                 bool useDicomTls)
+  {
+    Logging::SetCurrentThreadName("DICOM-SERVER");
+    CLOG(INFO, DICOM) << "DICOM server started";
+
+    while (server->continue_)
+    {
+      /* receive an association and acknowledge or reject it. If the association was */
+      /* acknowledged, offer corresponding services and invoke one or more if required. */
+      std::unique_ptr dispatcher(
+        Internals::AcceptAssociation(*server, server->pimpl_->network_, maximumPduLength, useDicomTls));
+
+      try
+      {
+        if (dispatcher.get() != NULL)
+        {
+          server->pimpl_->workers_->Add(dispatcher.release());
+        }
+      }
+      catch (OrthancException& e)
+      {
+        CLOG(ERROR, DICOM) << "Exception in the DICOM server thread: " << e.What();
+      }
+    }
+
+    CLOG(INFO, DICOM) << "DICOM server stopping";
+  }
+
+
+  DicomServer::DicomServer() : 
+    pimpl_(new PImpl),
+    checkCalledAet_(true),
+    aet_("ANY-SCP"),
+    port_(104),
+    continue_(false),
+    associationTimeout_(30),
+    threadsCount_(4),
+    modalities_(NULL),
+    findRequestHandlerFactory_(NULL),
+    moveRequestHandlerFactory_(NULL),
+    getRequestHandlerFactory_(NULL),
+    storeRequestHandlerFactory_(NULL),
+    worklistRequestHandlerFactory_(NULL),
+    storageCommitmentFactory_(NULL),
+    applicationEntityFilter_(NULL),
+    useDicomTls_(false),
+    maximumPduLength_(ASC_DEFAULTMAXPDU),
+    remoteCertificateRequired_(true),
+    minimumTlsVersion_(0)
+  {
+  }
+
+  DicomServer::~DicomServer()
+  {
+    if (continue_)
+    {
+      CLOG(ERROR, DICOM) << "INTERNAL ERROR: DicomServer::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+  void DicomServer::SetPortNumber(uint16_t port)
+  {
+    Stop();
+    port_ = port;
+  }
+
+  uint16_t DicomServer::GetPortNumber() const
+  {
+    return port_;
+  }
+
+  void DicomServer::SetAssociationTimeout(uint32_t seconds)
+  {
+    CLOG(INFO, DICOM) << "Setting timeout for DICOM connections if Orthanc acts as SCP (server): " 
+                      << seconds << " seconds (0 = no timeout)";
+
+    Stop();
+    associationTimeout_ = seconds;
+  }
+
+  uint32_t DicomServer::GetAssociationTimeout() const
+  {
+    return associationTimeout_;
+  }
+
+
+  void DicomServer::SetCalledApplicationEntityTitleCheck(bool check)
+  {
+    Stop();
+    checkCalledAet_ = check;
+  }
+
+  bool DicomServer::HasCalledApplicationEntityTitleCheck() const
+  {
+    return checkCalledAet_;
+  }
+
+  void DicomServer::SetApplicationEntityTitle(const std::string& aet)
+  {
+    if (aet.size() == 0)
+    {
+      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
+    }
+
+    if (aet.size() > 16)
+    {
+      throw OrthancException(ErrorCode_BadApplicationEntityTitle);
+    }
+
+    for (size_t i = 0; i < aet.size(); i++)
+    {
+      if (!(aet[i] == '-' ||
+            aet[i] == '_' ||
+            isdigit(aet[i]) ||
+            (aet[i] >= 'A' && aet[i] <= 'Z')))
+      {
+        CLOG(WARNING, DICOM) << "For best interoperability, only upper case, alphanumeric characters should be present in AET: \"" << aet << "\"";
+        break;
+      }
+    }
+
+    Stop();
+    aet_ = aet;
+  }
+
+  const std::string& DicomServer::GetApplicationEntityTitle() const
+  {
+    return aet_;
+  }
+
+  void DicomServer::SetRemoteModalities(IRemoteModalities& modalities)
+  {
+    Stop();
+    modalities_ = &modalities;
+  }
+  
+  DicomServer::IRemoteModalities& DicomServer::GetRemoteModalities() const
+  {
+    if (modalities_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *modalities_;
+    }
+  }
+    
+  void DicomServer::SetFindRequestHandlerFactory(IFindRequestHandlerFactory& factory)
+  {
+    Stop();
+    findRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasFindRequestHandlerFactory() const
+  {
+    return (findRequestHandlerFactory_ != NULL);
+  }
+
+  IFindRequestHandlerFactory& DicomServer::GetFindRequestHandlerFactory() const
+  {
+    if (HasFindRequestHandlerFactory())
+    {
+      return *findRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCFindHandler);
+    }
+  }
+
+  void DicomServer::SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& factory)
+  {
+    Stop();
+    moveRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasMoveRequestHandlerFactory() const
+  {
+    return (moveRequestHandlerFactory_ != NULL);
+  }
+
+  IMoveRequestHandlerFactory& DicomServer::GetMoveRequestHandlerFactory() const
+  {
+    if (HasMoveRequestHandlerFactory())
+    {
+      return *moveRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCMoveHandler);
+    }
+  }
+
+  void DicomServer::SetGetRequestHandlerFactory(IGetRequestHandlerFactory& factory)
+  {
+    Stop();
+    getRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasGetRequestHandlerFactory() const
+  {
+    return (getRequestHandlerFactory_ != NULL);
+  }
+
+  IGetRequestHandlerFactory& DicomServer::GetGetRequestHandlerFactory() const
+  {
+    if (HasGetRequestHandlerFactory())
+    {
+      return *getRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCGetHandler);
+    }
+  }
+
+  void DicomServer::SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& factory)
+  {
+    Stop();
+    storeRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasStoreRequestHandlerFactory() const
+  {
+    return (storeRequestHandlerFactory_ != NULL);
+  }
+
+  IStoreRequestHandlerFactory& DicomServer::GetStoreRequestHandlerFactory() const
+  {
+    if (HasStoreRequestHandlerFactory())
+    {
+      return *storeRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCStoreHandler);
+    }
+  }
+
+  void DicomServer::SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& factory)
+  {
+    Stop();
+    worklistRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasWorklistRequestHandlerFactory() const
+  {
+    return (worklistRequestHandlerFactory_ != NULL);
+  }
+
+  IWorklistRequestHandlerFactory& DicomServer::GetWorklistRequestHandlerFactory() const
+  {
+    if (HasWorklistRequestHandlerFactory())
+    {
+      return *worklistRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoWorklistHandler);
+    }
+  }
+
+  void DicomServer::SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& factory)
+  {
+    Stop();
+    storageCommitmentFactory_ = &factory;
+  }
+
+  bool DicomServer::HasStorageCommitmentRequestHandlerFactory() const
+  {
+    return (storageCommitmentFactory_ != NULL);
+  }
+
+  IStorageCommitmentRequestHandlerFactory& DicomServer::GetStorageCommitmentRequestHandlerFactory() const
+  {
+    if (HasStorageCommitmentRequestHandlerFactory())
+    {
+      return *storageCommitmentFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoStorageCommitmentHandler);
+    }
+  }
+
+  void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory)
+  {
+    Stop();
+    applicationEntityFilter_ = &factory;
+  }
+
+  bool DicomServer::HasApplicationEntityFilter() const
+  {
+    return (applicationEntityFilter_ != NULL);
+  }
+
+  IApplicationEntityFilter& DicomServer::GetApplicationEntityFilter() const
+  {
+    if (HasApplicationEntityFilter())
+    {
+      return *applicationEntityFilter_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoApplicationEntityFilter);
+    }
+  }
+
+
+  void DicomServer::Start()
+  {
+    if (modalities_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No list of modalities was provided to the DICOM server");
+    }
+
+    if (useDicomTls_)
+    {
+      if (ownCertificatePath_.empty() ||
+          ownPrivateKeyPath_.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "DICOM TLS is enabled in Orthanc SCP, but no certificate was provided");
+      }
+    }
+    
+    Stop();
+
+    /* initialize network, i.e. create an instance of T_ASC_Network*. */
+    OFCondition cond = ASC_initializeNetwork
+      (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_);
+    if (cond.bad())
+    {
+      throw OrthancException(ErrorCode_DicomPortInUse,
+                             " (port = " + boost::lexical_cast(port_) +
+                             ") cannot create network: " + std::string(cond.text()));
+    }
+
+#if ORTHANC_ENABLE_SSL == 1
+    assert(pimpl_->tls_.get() == NULL);
+
+    if (useDicomTls_)
+    {
+      CLOG(INFO, DICOM) << "Orthanc SCP will use DICOM TLS";
+
+      try
+      {
+        pimpl_->tls_.reset(Internals::InitializeDicomTls(
+                             pimpl_->network_, NET_ACCEPTOR, ownPrivateKeyPath_, ownCertificatePath_,
+                             trustedCertificatesPath_, remoteCertificateRequired_, minimumTlsVersion_, acceptedCiphers_));
+      }
+      catch (OrthancException&)
+      {
+        ASC_dropNetwork(&pimpl_->network_);
+        throw;
+      }
+    }
+    else
+    {
+      CLOG(INFO, DICOM) << "Orthanc SCP will *not* use DICOM TLS";
+    }
+#else
+    CLOG(INFO, DICOM) << "Orthanc SCP will *not* use DICOM TLS";
+#endif
+
+    continue_ = true;
+
+    CLOG(INFO, DICOM) << "The embedded DICOM server will use " << threadsCount_ << " threads";
+
+    pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-"));
+    pimpl_->thread_ = boost::thread(ServerThread, this, maximumPduLength_, useDicomTls_);
+  }
+
+
+  void DicomServer::Stop()
+  {
+    if (continue_)
+    {
+      continue_ = false;
+
+      if (pimpl_->thread_.joinable())
+      {
+        pimpl_->thread_.join();
+      }
+
+      pimpl_->workers_.reset(NULL);
+
+#if ORTHANC_ENABLE_SSL == 1
+      pimpl_->tls_.reset(NULL);  // Transport layer must be destroyed before the association itself
+#endif
+
+      /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
+      /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
+      OFCondition cond = ASC_dropNetwork(&pimpl_->network_);
+      if (cond.bad())
+      {
+        CLOG(ERROR, DICOM) << "Error while dropping the network: " << cond.text();
+      }
+    }
+  }
+
+
+  bool DicomServer::IsMyAETitle(const std::string& aet) const
+  {
+    if (modalities_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    
+    if (!HasCalledApplicationEntityTitleCheck())
+    {
+      // OK, no check on the AET.
+      return true;
+    }
+    else
+    {
+      return modalities_->IsSameAETitle(aet, GetApplicationEntityTitle());
+    }
+  }
+
+
+  void DicomServer::SetDicomTlsEnabled(bool enabled)
+  {
+    Stop();
+    useDicomTls_ = enabled;
+  }
+  
+  bool DicomServer::IsDicomTlsEnabled() const
+  {
+    return useDicomTls_;
+  }
+
+  void DicomServer::SetMinimumTlsVersion(unsigned int version)
+  {
+    minimumTlsVersion_ = version;
+    DicomAssociationParameters::SetMinimumTlsVersion(version);
+  }
+
+  void DicomServer::SetAcceptedCiphers(const std::set& ciphers)
+  {
+    acceptedCiphers_ = ciphers;
+    DicomAssociationParameters::SetAcceptedCiphers(ciphers);
+  }
+
+  void DicomServer::SetOwnCertificatePath(const std::string& privateKeyPath,
+                                          const std::string& certificatePath)
+  {
+    Stop();
+
+    if (!privateKeyPath.empty() &&
+        !certificatePath.empty())
+    {
+      CLOG(INFO, DICOM) << "Setting the TLS certificate for DICOM SCP connections: " 
+                        << privateKeyPath << " (key), " << certificatePath << " (certificate)";
+
+      if (certificatePath.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "No path to the default DICOM TLS certificate was provided");
+      }
+      
+      if (privateKeyPath.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "No path to the private key for the default DICOM TLS certificate was provided");
+      }
+      
+      if (!SystemToolbox::IsRegularFile(privateKeyPath))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + privateKeyPath);
+      }
+
+      if (!SystemToolbox::IsRegularFile(certificatePath))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + certificatePath);
+      }
+      
+      ownPrivateKeyPath_ = privateKeyPath;
+      ownCertificatePath_ = certificatePath;
+    }
+    else
+    {
+      ownPrivateKeyPath_.clear();
+      ownCertificatePath_.clear();
+    }
+  }
+  
+  const std::string& DicomServer::GetOwnPrivateKeyPath() const
+  {
+    return ownPrivateKeyPath_;
+  }
+  
+  const std::string& DicomServer::GetOwnCertificatePath() const
+  {
+    return ownCertificatePath_;
+  }
+    
+  void DicomServer::SetTrustedCertificatesPath(const std::string& path)
+  {
+    Stop();
+
+    if (!path.empty())
+    {
+      CLOG(INFO, DICOM) << "Setting the trusted certificates for DICOM SCP connections: " << path;
+
+      if (!SystemToolbox::IsRegularFile(path))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + path);
+      }
+      
+      trustedCertificatesPath_ = path;
+    }
+    else
+    {
+      trustedCertificatesPath_.clear();
+    }
+  }
+  
+  const std::string& DicomServer::GetTrustedCertificatesPath() const
+  {
+    return trustedCertificatesPath_;
+  }
+
+  unsigned int DicomServer::GetMaximumPduLength() const
+  {
+    return maximumPduLength_;
+  }
+
+  void DicomServer::SetMaximumPduLength(unsigned int pdu)
+  {
+    DicomAssociationParameters::CheckMaximumPduLength(pdu);
+
+    Stop();
+    maximumPduLength_ = pdu;
+  }
+
+  void DicomServer::SetRemoteCertificateRequired(bool required)
+  {
+    Stop();
+    remoteCertificateRequired_ = required;
+  }
+  
+  bool DicomServer::IsRemoteCertificateRequired() const
+  {
+    return remoteCertificateRequired_;
+  }
+
+  void DicomServer::SetThreadsCount(unsigned int threads)
+  {
+    if (threads == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    Stop();
+    threadsCount_ = threads;
+  }
+
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomServer.h b/OrthancFramework/Sources/DicomNetworking/DicomServer.h
new file mode 100644
index 0000000..652ccef
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h
@@ -0,0 +1,179 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#include "IFindRequestHandlerFactory.h"
+#include "IMoveRequestHandlerFactory.h"
+#include "IGetRequestHandlerFactory.h"
+#include "IStoreRequestHandlerFactory.h"
+#include "IWorklistRequestHandlerFactory.h"
+#include "IStorageCommitmentRequestHandlerFactory.h"
+#include "IApplicationEntityFilter.h"
+#include "RemoteModalityParameters.h"
+
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  class DicomServer : public boost::noncopyable
+  {
+  public:
+    // WARNING: The methods of this class must be thread-safe
+    class IRemoteModalities : public boost::noncopyable
+    {
+    public:
+      virtual ~IRemoteModalities()
+      {
+      }
+      
+      virtual bool IsSameAETitle(const std::string& aet1,
+                                 const std::string& aet2) = 0;
+
+      virtual bool LookupAETitle(RemoteModalityParameters& modality,
+                                 const std::string& aet) = 0;
+    };
+    
+  private:
+    struct PImpl;
+    boost::shared_ptr pimpl_;
+
+    bool checkCalledAet_;
+    std::string aet_;
+    uint16_t port_;
+    bool continue_;
+    uint32_t associationTimeout_;
+    unsigned int threadsCount_;
+    IRemoteModalities* modalities_;
+    IFindRequestHandlerFactory* findRequestHandlerFactory_;
+    IMoveRequestHandlerFactory* moveRequestHandlerFactory_;
+    IGetRequestHandlerFactory* getRequestHandlerFactory_;
+    IStoreRequestHandlerFactory* storeRequestHandlerFactory_;
+    IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_;
+    IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_;
+    IApplicationEntityFilter* applicationEntityFilter_;
+
+    // New in Orthanc 1.9.0 for DICOM TLS
+    bool         useDicomTls_;
+    std::string  ownPrivateKeyPath_;
+    std::string  ownCertificatePath_;
+    std::string  trustedCertificatesPath_;
+    unsigned int maximumPduLength_;
+    bool         remoteCertificateRequired_;  // New in 1.9.3
+    unsigned int minimumTlsVersion_;          // New in 1.12.4
+    std::set acceptedCiphers_;   // New in 1.12.4
+
+
+    static void ServerThread(DicomServer* server,
+                             unsigned int maximumPduLength,
+                             bool useDicomTls);
+
+  public:
+    DicomServer();
+
+    ~DicomServer();
+
+    void SetPortNumber(uint16_t port);
+    uint16_t GetPortNumber() const;
+
+    void SetAssociationTimeout(uint32_t seconds);
+    uint32_t GetAssociationTimeout() const;
+
+    void SetCalledApplicationEntityTitleCheck(bool check);
+    bool HasCalledApplicationEntityTitleCheck() const;
+
+    void SetApplicationEntityTitle(const std::string& aet);
+    const std::string& GetApplicationEntityTitle() const;
+
+    void SetRemoteModalities(IRemoteModalities& modalities);
+    IRemoteModalities& GetRemoteModalities() const;
+    
+    void SetFindRequestHandlerFactory(IFindRequestHandlerFactory& handler);
+    bool HasFindRequestHandlerFactory() const;
+    IFindRequestHandlerFactory& GetFindRequestHandlerFactory() const;
+
+    void SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& handler);
+    bool HasMoveRequestHandlerFactory() const;
+    IMoveRequestHandlerFactory& GetMoveRequestHandlerFactory() const;
+
+    void SetGetRequestHandlerFactory(IGetRequestHandlerFactory& handler);
+    bool HasGetRequestHandlerFactory() const;
+    IGetRequestHandlerFactory& GetGetRequestHandlerFactory() const;
+
+    void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler);
+    bool HasStoreRequestHandlerFactory() const;
+    IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const;
+
+    void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler);
+    bool HasWorklistRequestHandlerFactory() const;
+    IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const;
+
+    void SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& handler);
+    bool HasStorageCommitmentRequestHandlerFactory() const;
+    IStorageCommitmentRequestHandlerFactory& GetStorageCommitmentRequestHandlerFactory() const;
+
+    void SetApplicationEntityFilter(IApplicationEntityFilter& handler);
+    bool HasApplicationEntityFilter() const;
+    IApplicationEntityFilter& GetApplicationEntityFilter() const;
+
+    void Start();
+  
+    void Stop();
+
+    bool IsMyAETitle(const std::string& aet) const;
+
+    void SetDicomTlsEnabled(bool enabled);
+    bool IsDicomTlsEnabled() const;
+
+    void SetMinimumTlsVersion(unsigned int version);
+    void SetAcceptedCiphers(const std::set& ciphers);
+
+    void SetOwnCertificatePath(const std::string& privateKeyPath,
+                               const std::string& certificatePath);
+    const std::string& GetOwnPrivateKeyPath() const;    
+    const std::string& GetOwnCertificatePath() const;
+    
+    void SetTrustedCertificatesPath(const std::string& path);
+    const std::string& GetTrustedCertificatesPath() const;
+
+    unsigned int GetMaximumPduLength() const;
+    void SetMaximumPduLength(unsigned int pdu);
+
+    void SetRemoteCertificateRequired(bool required);
+    bool IsRemoteCertificateRequired() const;
+
+    void SetThreadsCount(unsigned int threadsCount);
+
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp
new file mode 100644
index 0000000..7911e65
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp
@@ -0,0 +1,678 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomStoreUserConnection.h"
+
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../DicomParsing/ParsedDicomFile.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "DicomAssociation.h"
+
+#include 
+
+#include 
+
+
+namespace Orthanc
+{
+  static void ProgressCallback(void * /*callbackData*/,
+                               T_DIMSE_StoreProgress *progress,
+                               T_DIMSE_C_StoreRQ * req)
+  {
+    if (req != NULL &&
+        progress->state == DIMSE_StoreBegin)
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Store Request:" << std::endl
+                         << DIMSE_dumpMessage(str, *req, DIMSE_OUTGOING);
+    }
+  }
+
+
+  bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid,
+                                                     const std::set& sourceSyntaxes,
+                                                     bool hasPreferred,
+                                                     DicomTransferSyntax preferred)
+  {
+    typedef std::list< std::list >  GroupsOfSyntaxes;
+
+    GroupsOfSyntaxes  groups;
+
+    // Firstly, add one group for each individual transfer syntax
+    for (std::set::const_iterator
+           it = sourceSyntaxes.begin(); it != sourceSyntaxes.end(); ++it)
+    {
+      std::list group;
+      group.push_back(*it);
+      groups.push_back(group);
+    }
+
+    // Secondly, add one group with the preferred transfer syntax
+    if (hasPreferred &&
+        sourceSyntaxes.find(preferred) == sourceSyntaxes.end())
+    {
+      std::list group;
+      group.push_back(preferred);
+      groups.push_back(group);
+    }
+
+    // Thirdly, add all the uncompressed transfer syntaxes as one single group
+    if (proposeUncompressedSyntaxes_)
+    {
+      static const size_t N = 3;
+      static const DicomTransferSyntax UNCOMPRESSED_SYNTAXES[N] = {
+        DicomTransferSyntax_LittleEndianImplicit,
+        DicomTransferSyntax_LittleEndianExplicit,
+        DicomTransferSyntax_BigEndianExplicit
+      };
+
+      std::list group;
+
+      for (size_t i = 0; i < N; i++)
+      {
+        DicomTransferSyntax syntax = UNCOMPRESSED_SYNTAXES[i];
+        if (sourceSyntaxes.find(syntax) == sourceSyntaxes.end() &&
+            (!hasPreferred || preferred != syntax))
+        {
+          group.push_back(syntax);
+        }
+      }
+
+      if (!group.empty())
+      {
+        groups.push_back(group);
+      }
+    }
+
+    // Now, propose each of these groups of transfer syntaxes
+    if (association_->GetRemainingPropositions() <= groups.size())
+    {
+      return false;  // Not enough room
+    }
+    else
+    {
+      for (GroupsOfSyntaxes::const_iterator it = groups.begin(); it != groups.end(); ++it)
+      {
+        association_->ProposePresentationContext(sopClassUid, *it);
+
+        // Remember the syntaxes that were individually proposed, in
+        // order to avoid renegociation if they are seen again (**)
+        if (it->size() == 1)
+        {
+          DicomTransferSyntax syntax = *it->begin();
+          proposedOriginalClasses_.insert(std::make_pair(sopClassUid, syntax));
+        }
+      }
+
+      return true;
+    }
+  }
+
+
+  bool DicomStoreUserConnection::LookupPresentationContext(
+    uint8_t& presentationContextId,
+    const std::string& sopClassUid,
+    DicomTransferSyntax transferSyntax)
+  {
+    typedef std::map  PresentationContexts;
+
+    PresentationContexts pc;
+    if (association_->IsOpen() &&
+        association_->LookupAcceptedPresentationContext(pc, sopClassUid))
+    {
+      PresentationContexts::const_iterator found = pc.find(transferSyntax);
+      if (found != pc.end())
+      {
+        presentationContextId = found->second;
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  DicomStoreUserConnection::DicomStoreUserConnection(
+    const DicomAssociationParameters& params) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    proposeCommonClasses_(true),
+    proposeUncompressedSyntaxes_(true),
+    proposeRetiredBigEndian_(false)
+  {
+  }
+
+  const DicomAssociationParameters &DicomStoreUserConnection::GetParameters() const
+  {
+    return parameters_;
+  }
+
+  void DicomStoreUserConnection::SetCommonClassesProposed(bool proposed)
+  {
+    proposeCommonClasses_ = proposed;
+  }
+
+  bool DicomStoreUserConnection::IsCommonClassesProposed() const
+  {
+    return proposeCommonClasses_;
+  }
+
+  void DicomStoreUserConnection::SetUncompressedSyntaxesProposed(bool proposed)
+  {
+    proposeUncompressedSyntaxes_ = proposed;
+  }
+
+  bool DicomStoreUserConnection::IsUncompressedSyntaxesProposed() const
+  {
+    return proposeUncompressedSyntaxes_;
+  }
+
+  void DicomStoreUserConnection::SetRetiredBigEndianProposed(bool propose)
+  {
+    proposeRetiredBigEndian_ = propose;
+  }
+
+  bool DicomStoreUserConnection::IsRetiredBigEndianProposed() const
+  {
+    return proposeRetiredBigEndian_;
+  }
+
+
+  void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid,
+                                                      DicomTransferSyntax syntax)
+  {
+    RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid);
+
+    if (found == registeredClasses_.end())
+    {
+      std::set ts;
+      ts.insert(syntax);
+      registeredClasses_[sopClassUid] = ts;
+    }
+    else
+    {
+      found->second.insert(syntax);
+    }
+  }
+
+
+  void DicomStoreUserConnection::LookupParameters(std::string& sopClassUid,
+                                                  std::string& sopInstanceUid,
+                                                  DicomTransferSyntax& transferSyntax,
+                                                  DcmFileFormat& dicom)
+  {
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    OFString a, b;
+    if (!dicom.getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() ||
+        !dicom.getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good())
+    {
+      throw OrthancException(ErrorCode_NoSopClassOrInstance,
+                             "Unable to determine the SOP class/instance for C-STORE with AET " +
+                             parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    sopClassUid.assign(a.c_str());
+    sopInstanceUid.assign(b.c_str());
+
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Unknown transfer syntax from DCMTK");
+    }
+  }
+  
+
+  bool DicomStoreUserConnection::NegotiatePresentationContext(
+    uint8_t& presentationContextId,
+    const std::string& sopClassUid,
+    DicomTransferSyntax transferSyntax,
+    bool hasPreferred,
+    DicomTransferSyntax preferred)
+  {
+    /**
+     * Step 1: Check whether this presentation context is already
+     * available in the previously negotiated assocation.
+     **/
+
+    if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
+    {
+      CLOG(INFO, DICOM) << "Found an accepted presentation context for SOPClassUID " << sopClassUid << " and transfer syntax " << GetTransferSyntaxUid(transferSyntax);
+      return true;
+    }
+
+    // The association must be re-negotiated
+    if (association_->IsOpen())
+    {
+      CLOG(INFO, DICOM) << "No accepted presentation context found, re-negotiating DICOM association with "
+                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
+                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);
+
+      // Check if we know that the remote modality was
+      // already proposed this individual transfer syntax (**)
+      if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) != proposedOriginalClasses_.end())
+      {
+        CLOG(INFO, DICOM) << "The remote modality has already rejected SOP class UID \""
+                          << sopClassUid << "\" with transfer syntax \""
+                          << GetTransferSyntaxUid(transferSyntax) << "\", but we will renegotiate anyway";
+        // always renegotiating since 1.12.2 // return false;
+      }
+    }
+    else
+    {
+      CLOG(INFO, DICOM) << "Negotiating DICOM association with "
+                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
+                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);
+    }
+
+    association_->ClearPresentationContexts();
+    proposedOriginalClasses_.clear();
+    RegisterStorageClass(sopClassUid, transferSyntax);  // (*)
+
+    
+    /**
+     * Step 2: Propose at least the mandatory SOP class.
+     **/
+
+    {
+      RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid);
+
+      if (mandatory == registeredClasses_.end() ||
+          mandatory->second.find(transferSyntax) == mandatory->second.end())
+      {
+        // Should never fail because of (*)
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!ProposeStorageClass(sopClassUid, mandatory->second, hasPreferred, preferred))
+      {
+        // Should never happen in real life: There are no more than
+        // 128 transfer syntaxes in DICOM!
+        throw OrthancException(ErrorCode_InternalError,
+                               "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
+      }
+    }
+
+      
+    /**
+     * Step 3: Propose all the previously spotted SOP classes, as
+     * registered through the "RegisterStorageClass()" method.
+     **/
+      
+    for (RegisteredClasses::const_iterator it = registeredClasses_.begin();
+         it != registeredClasses_.end(); ++it)
+    {
+      if (it->first != sopClassUid)
+      {
+        ProposeStorageClass(it->first, it->second, hasPreferred, preferred);
+      }
+    }
+      
+
+    /**
+     * Step 4: As long as there is room left in the proposed
+     * presentation contexts, propose the uncompressed transfer syntaxes
+     * for the most common SOP classes, as can be found in the
+     * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
+     * preferred transfer syntax is "LittleEndianImplicit".
+     **/
+
+    if (proposeCommonClasses_)
+    {
+      // The method "ProposeStorageClass()" will automatically add
+      // "LittleEndianImplicit"
+      std::set ts;
+        
+      for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
+      {
+        std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
+          
+        if (c != sopClassUid &&
+            registeredClasses_.find(c) == registeredClasses_.end())
+        {
+          ProposeStorageClass(c, ts, hasPreferred, preferred);
+        }
+      }
+    }
+
+
+    /**
+     * Step 5: Open the association, and check whether the pair (SOP
+     * class UID, transfer syntax) was accepted by the remote host.
+     **/
+
+    association_->Open(parameters_);
+    return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
+  }
+
+
+  void DicomStoreUserConnection::Store(std::string& sopClassUid,
+                                       std::string& sopInstanceUid,
+                                       DcmFileFormat& dicom,
+                                       bool hasMoveOriginator,
+                                       const std::string& moveOriginatorAET,
+                                       uint16_t moveOriginatorID)
+  {
+    DicomTransferSyntax transferSyntax;
+    LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom);
+
+    LOG(INFO) << "Performing C-Store on instance of SOPClassUID '" << sopClassUid << "'";
+
+    uint8_t presID;
+    if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax, proposeUncompressedSyntaxes_,
+                                      DicomTransferSyntax_LittleEndianExplicit))
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "No valid presentation context was negotiated for "
+                             "SOP class UID [" + sopClassUid + "] and transfer "
+                             "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] "
+                             "while sending to modality [" +
+                             parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]");
+    }
+    
+    // Prepare the transmission of data
+    T_DIMSE_C_StoreRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
+
+    if (hasMoveOriginator)
+    {    
+      strncpy(request.MoveOriginatorApplicationEntityTitle, 
+              moveOriginatorAET.c_str(), DIC_AE_LEN);
+      request.opts = O_STORE_MOVEORIGINATORAETITLE;
+
+      request.MoveOriginatorID = moveOriginatorID;  // The type DIC_US is an alias for uint16_t
+      request.opts |= O_STORE_MOVEORIGINATORID;
+    }
+
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Finally conduct transmission of data
+    T_DIMSE_C_StoreRSP response;
+    DcmDataset* statusDetail = NULL;
+    DicomAssociation::CheckCondition(
+      DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request,
+                      NULL, dicom.getDataset(), ProgressCallback, NULL,
+                      /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                      /*opt_dimse_timeout*/ GetParameters().GetTimeout(),
+                      &response, &statusDetail, NULL),
+      GetParameters(), "C-STORE");
+
+    if (statusDetail != NULL) 
+    {
+      delete statusDetail;
+    }
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Received Store Response:" << std::endl
+                         << DIMSE_dumpMessage(str, response, DIMSE_INCOMING, NULL, presID);
+    }
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-STORE.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
+        response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
+        response.DimseStatus != 0xB006 &&  // Warning - Elements Discarded
+        response.DimseStatus != 0x0111)    // Warning - Duplicate SOPInstanceUID (https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3)
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "C-STORE SCU to AET \"" +
+                             GetParameters().GetRemoteModality().GetApplicationEntityTitle() +
+                             "\" has failed with DIMSE status 0x" + buf);
+    }
+  }
+
+
+  void DicomStoreUserConnection::Store(std::string& sopClassUid,
+                                       std::string& sopInstanceUid,
+                                       const void* buffer,
+                                       size_t size,
+                                       bool hasMoveOriginator,
+                                       const std::string& moveOriginatorAET,
+                                       uint16_t moveOriginatorID)
+  {
+    std::unique_ptr dicom(
+      FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
+
+    if (dicom.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+  }
+
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+  void DicomStoreUserConnection::LookupTranscoding(std::set& acceptedSyntaxes,
+                                                   const std::string& sopClassUid,
+                                                   DicomTransferSyntax sourceSyntax,
+                                                   bool hasPreferred,
+                                                   DicomTransferSyntax preferred)
+  {
+    acceptedSyntaxes.clear();
+    std::map contexts;
+
+    // Make sure a negotiation has already occurred for this transfer
+    // syntax if we have not negotiated yet. 
+    // We don't use the return code: Transcoding is possible even if the "sourceSyntax" is not supported.
+    if (!association_->IsOpen() || !association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
+    {
+      uint8_t presID;
+      NegotiatePresentationContext(presID, sopClassUid, sourceSyntax, hasPreferred, preferred);
+    }
+
+    if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
+    {
+      for (std::map::const_iterator
+             it = contexts.begin(); it != contexts.end(); ++it)
+      {
+        acceptedSyntaxes.insert(it->first);
+      }
+    }
+  }
+#endif
+  
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
+                                           std::string& sopInstanceUid /* out */,
+                                           IDicomTranscoder& transcoder,
+                                           const void* buffer,
+                                           size_t size,
+                                           DicomTransferSyntax preferredTransferSyntax,
+                                           bool hasMoveOriginator,
+                                           const std::string& moveOriginatorAET,
+                                           uint16_t moveOriginatorID)
+  {
+    std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
+    if (dicom.get() == NULL ||
+        dicom->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    DicomTransferSyntax sourceSyntax;
+    LookupParameters(sopClassUid, sopInstanceUid, sourceSyntax, *dicom);
+
+    std::set accepted;
+    LookupTranscoding(accepted, sopClassUid, sourceSyntax, true, preferredTransferSyntax);
+
+    if (accepted.size() == 0)
+    {
+      throw OrthancException(ErrorCode_NoPresentationContext, "Cannot C-Store an instance of SOPClassUID " + 
+                             sopClassUid + ", the destination has not accepted any TransferSyntax for this SOPClassUID.");
+    }
+
+    if (accepted.find(sourceSyntax) != accepted.end())
+    {
+      // No need for transcoding
+      Store(sopClassUid, sopInstanceUid, *dicom,
+            hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+    }
+    else
+    {
+      // Transcoding is needed
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(dicom.release());
+      source.SetExternalBuffer(buffer, size);
+
+      const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed());
+        
+      IDicomTranscoder::DicomImage transcoded;
+      bool success = false;
+      bool isDestructiveCompressionAllowed = false;
+      std::set attemptedSyntaxes;
+
+      LOG(INFO) << "Transcoding is required to C-Store an instance of SOPClassUID '" << sopClassUid << "', preferredTransferSyntax is " << GetTransferSyntaxUid(preferredTransferSyntax);
+
+      if (accepted.find(preferredTransferSyntax) != accepted.end())
+      {
+        // New in Orthanc 1.9.0: The preferred transfer syntax is
+        // accepted by the remote modality => transcode to this syntax
+        std::set targetSyntaxes;
+        targetSyntaxes.insert(preferredTransferSyntax);
+        attemptedSyntaxes.insert(preferredTransferSyntax);
+
+        success = transcoder.Transcode(transcoded, source, targetSyntaxes, true);
+        isDestructiveCompressionAllowed = true;
+      }
+
+      if (!success)
+      {
+        // Transcode to either one of the uncompressed transfer
+        // syntaxes that are accepted by the remote modality
+
+        std::set targetSyntaxes;
+
+        if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end())
+        {
+          targetSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+          attemptedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+        }
+
+        if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end())
+        {
+          targetSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+          attemptedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+        }
+
+        if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end())
+        {
+          targetSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
+          attemptedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
+        }
+
+        if (!targetSyntaxes.empty())
+        {
+          success = transcoder.Transcode(transcoded, source, targetSyntaxes, false);
+          isDestructiveCompressionAllowed = false;
+        }
+      }
+
+      if (success)
+      {
+        std::string targetUid = IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed());
+        if (sourceUid != targetUid)
+        {
+          if (isDestructiveCompressionAllowed)
+          {
+            LOG(WARNING) << "Because of the use of a preferred transfer syntax that corresponds to "
+                         << "a destructive compression, C-STORE SCU has hanged the SOP Instance UID "
+                         << "of a DICOM instance from \"" << sourceUid << "\" to \"" << targetUid << "\"";
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP "
+                                   "Instance UID while transcoding to an uncompressed transfer syntax");
+          }
+        }
+
+        DicomTransferSyntax transcodedSyntax;
+          
+        // Sanity check
+        if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) ||
+            accepted.find(transcodedSyntax) == accepted.end())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+        else
+        {
+          Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(),
+                hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+        }
+      }
+      else
+      {
+        std::string s;
+        for (std::set::const_iterator
+               it = attemptedSyntaxes.begin(); it != attemptedSyntaxes.end(); ++it)
+        {
+          s += " " + std::string(GetTransferSyntaxUid(*it));
+        }
+        
+        throw OrthancException(ErrorCode_InternalError, "Cannot transcode instance of SOPClassUID " + 
+                               sopClassUid + " from " +
+                               std::string(GetTransferSyntaxUid(sourceSyntax)) +
+                               " to one of [" + s + " ]");
+      }
+    }
+  }
+#endif
+  
+  
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
+                                           std::string& sopInstanceUid /* out */,
+                                           IDicomTranscoder& transcoder,
+                                           const void* buffer,
+                                           size_t size,
+                                           bool hasMoveOriginator,
+                                           const std::string& moveOriginatorAET,
+                                           uint16_t moveOriginatorID)
+  {
+    Transcode(sopClassUid, sopInstanceUid, transcoder, buffer, size, DicomTransferSyntax_LittleEndianExplicit,
+              hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+  }
+#endif
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h
new file mode 100644
index 0000000..309fd8b
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h
@@ -0,0 +1,171 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING)
+#  error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file
+#endif
+
+#include "DicomAssociationParameters.h"
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#  include "../DicomParsing/IDicomTranscoder.h"
+#endif
+
+#include 
+#include 
+#include 
+#include   // For uint8_t
+
+
+class DcmFileFormat;
+
+namespace Orthanc
+{
+  /**
+
+     Orthanc < 1.7.0:
+
+     Input        | Output
+     -------------+---------------------------------------------
+     Compressed   | Same transfer syntax
+     Uncompressed | Same transfer syntax, or other uncompressed
+
+     Orthanc >= 1.7.0:
+
+     Input        | Output
+     -------------+---------------------------------------------
+     Compressed   | Same transfer syntax, or uncompressed
+     Uncompressed | Same transfer syntax, or other uncompressed
+
+  **/
+
+  class DicomAssociation;  // Forward declaration for PImpl design pattern
+
+  class ORTHANC_PUBLIC DicomStoreUserConnection : public boost::noncopyable
+  {
+  private:
+    typedef std::map > RegisteredClasses;
+
+    // "ProposedOriginalClasses" keeps track of the storage classes
+    // that were proposed with a single transfer syntax
+    typedef std::set< std::pair > ProposedOriginalClasses;
+
+    DicomAssociationParameters           parameters_;
+    boost::shared_ptr  association_;  // "shared_ptr" is for PImpl
+    RegisteredClasses                    registeredClasses_;
+    ProposedOriginalClasses              proposedOriginalClasses_;
+    bool                                 proposeCommonClasses_;
+    bool                                 proposeUncompressedSyntaxes_;
+    bool                                 proposeRetiredBigEndian_;
+
+    // Return "false" if there is not enough room remaining in the association
+    bool ProposeStorageClass(const std::string& sopClassUid,
+                             const std::set& sourceSyntaxes,
+                             bool hasPreferred,
+                             DicomTransferSyntax preferred);
+
+    bool LookupPresentationContext(uint8_t& presentationContextId,
+                                   const std::string& sopClassUid,
+                                   DicomTransferSyntax transferSyntax);
+    
+    bool NegotiatePresentationContext(uint8_t& presentationContextId,
+                                      const std::string& sopClassUid,
+                                      DicomTransferSyntax transferSyntax,
+                                      bool hasPreferred,
+                                      DicomTransferSyntax preferred);
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    void LookupTranscoding(std::set& acceptedSyntaxes,
+                           const std::string& sopClassUid,
+                           DicomTransferSyntax sourceSyntax,
+                           bool hasPreferred,
+                           DicomTransferSyntax preferred);
+#endif
+
+  public:
+    explicit DicomStoreUserConnection(const DicomAssociationParameters& params);
+    
+    const DicomAssociationParameters& GetParameters() const;
+
+    void SetCommonClassesProposed(bool proposed);
+
+    bool IsCommonClassesProposed() const;
+
+    void SetUncompressedSyntaxesProposed(bool proposed);
+
+    bool IsUncompressedSyntaxesProposed() const;
+
+    void SetRetiredBigEndianProposed(bool propose);
+
+    bool IsRetiredBigEndianProposed() const;
+
+    void RegisterStorageClass(const std::string& sopClassUid,
+                              DicomTransferSyntax syntax);
+
+    void Store(std::string& sopClassUid,
+               std::string& sopInstanceUid,
+               DcmFileFormat& dicom,
+               bool hasMoveOriginator,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void Store(std::string& sopClassUid,
+               std::string& sopInstanceUid,
+               const void* buffer,
+               size_t size,
+               bool hasMoveOriginator,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void LookupParameters(std::string& sopClassUid,
+                          std::string& sopInstanceUid,
+                          DicomTransferSyntax& transferSyntax,
+                          DcmFileFormat& dicom);
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    void Transcode(std::string& sopClassUid /* out */,
+                   std::string& sopInstanceUid /* out */,
+                   IDicomTranscoder& transcoder,
+                   const void* buffer,
+                   size_t size,
+                   DicomTransferSyntax preferredTransferSyntax,
+                   bool hasMoveOriginator,
+                   const std::string& moveOriginatorAET,
+                   uint16_t moveOriginatorID);
+#endif
+    
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    void Transcode(std::string& sopClassUid /* out */,
+                   std::string& sopInstanceUid /* out */,
+                   IDicomTranscoder& transcoder,
+                   const void* buffer,
+                   size_t size,
+                   bool hasMoveOriginator,
+                   const std::string& moveOriginatorAET,
+                   uint16_t moveOriginatorID);
+#endif
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h
new file mode 100644
index 0000000..fa1fafa
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h
@@ -0,0 +1,70 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Enumerations.h"
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class IApplicationEntityFilter : public boost::noncopyable
+  {
+  public:
+    virtual ~IApplicationEntityFilter()
+    {
+    }
+
+    virtual bool IsAllowedConnection(const std::string& remoteIp,
+                                     const std::string& remoteAet,
+                                     const std::string& calledAet) = 0;
+
+    virtual bool IsAllowedRequest(const std::string& remoteIp,
+                                  const std::string& remoteAet,
+                                  const std::string& calledAet,
+                                  DicomRequestType type) = 0;
+
+    // Get the set of TransferSyntaxes that are accepted when negotiation a C-Store association, acting as SCP when it has been initiated by the C-Store SCU.
+    virtual void GetAcceptedTransferSyntaxes(std::set& target,
+                                             const std::string& remoteIp,
+                                             const std::string& remoteAet,
+                                             const std::string& calledAet) = 0;
+
+    // Get the list of TransferSyntaxes that are proposed when initiating a C-Store SCP which actually only happens in a C-Get SCU
+    virtual void GetProposedStorageTransferSyntaxes(std::list& target,
+                                                    const std::string& remoteIp,
+                                                    const std::string& remoteAet,
+                                                    const std::string& calledAet) = 0;
+
+    virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
+                                           const std::string& remoteAet,
+                                           const std::string& calledAet) = 0;
+
+    virtual void GetAcceptedSopClasses(std::set& sopClasses,
+                                       size_t maxCount) = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h
new file mode 100644
index 0000000..2d8c292
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h
@@ -0,0 +1,48 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomFindAnswers.h"
+
+#include 
+
+namespace Orthanc
+{
+  class IFindRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IFindRequestHandler()
+    {
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        const DicomMap& input,
+                        const std::list& sequencesToReturn,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer) = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h
new file mode 100644
index 0000000..32747cc
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h
@@ -0,0 +1,40 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IFindRequestHandler.h"
+
+namespace Orthanc
+{
+  class IFindRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IFindRequestHandlerFactory()
+    {
+    }
+
+    virtual IFindRequestHandler* ConstructFindRequestHandler() = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h
new file mode 100644
index 0000000..c0e68c6
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h
@@ -0,0 +1,62 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include 
+
+#include "../DicomFormat/DicomMap.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  class IGetRequestHandler : boost::noncopyable
+  {
+  public:
+    virtual ~IGetRequestHandler()
+    {
+    }
+
+    virtual bool Handle(const DicomMap& input,
+                        const std::string& originatorIp,
+                        const std::string& originatorAet,
+                        const std::string& calledAet,
+                        uint32_t timeout) = 0;
+    
+    virtual unsigned int GetSubOperationCount() const = 0;
+
+    // Must return "false" iff. a "Cancel" was returned by the remote SCU
+    virtual bool DoNext(T_ASC_Association *) = 0;
+    
+    virtual unsigned int GetCompletedCount() const = 0;
+    
+    virtual unsigned int GetWarningCount() const = 0;
+    
+    virtual unsigned int GetFailedCount() const = 0;
+    
+    virtual const std::string& GetFailedUids() const = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h
new file mode 100644
index 0000000..699dc7f
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h
@@ -0,0 +1,40 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IGetRequestHandler.h"
+
+namespace Orthanc
+{
+  class IGetRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IGetRequestHandlerFactory()
+    {
+    }
+
+    virtual IGetRequestHandler* ConstructGetRequestHandler() = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h
new file mode 100644
index 0000000..159877e
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h
@@ -0,0 +1,70 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../DicomFormat/DicomMap.h"
+
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  class IMoveRequestIterator : public boost::noncopyable
+  {
+  public:
+    enum Status
+    {
+      Status_Success,
+      Status_Failure,
+      Status_Warning
+    };
+
+    virtual ~IMoveRequestIterator()
+    {
+    }
+
+    virtual unsigned int GetSubOperationCount() const = 0;
+
+    virtual Status DoNext() = 0;
+  };
+
+
+  class IMoveRequestHandler
+  {
+  public:
+    virtual ~IMoveRequestHandler()
+    {
+    }
+
+    virtual IMoveRequestIterator* Handle(const std::string& targetAet,
+                                         const DicomMap& input,
+                                         const std::string& originatorIp,
+                                         const std::string& originatorAet,
+                                         const std::string& calledAet,
+                                         uint16_t originatorId) = 0;
+  };
+
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h
new file mode 100644
index 0000000..5e48745
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h
@@ -0,0 +1,40 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IMoveRequestHandler.h"
+
+namespace Orthanc
+{
+  class IMoveRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IMoveRequestHandlerFactory()
+    {
+    }
+
+    virtual IMoveRequestHandler* ConstructMoveRequestHandler() = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h
new file mode 100644
index 0000000..f62edd1
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h
@@ -0,0 +1,57 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class IStorageCommitmentRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentRequestHandler()
+    {
+    }
+
+    virtual void HandleRequest(const std::string& transactionUid,
+                               const std::vector& sopClassUids,
+                               const std::vector& sopInstanceUids,
+                               const std::string& remoteIp,
+                               const std::string& remoteAet,
+                               const std::string& calledAet) = 0;
+
+    virtual void HandleReport(const std::string& transactionUid,
+                              const std::vector& successSopClassUids,
+                              const std::vector& successSopInstanceUids,
+                              const std::vector& failedSopClassUids,
+                              const std::vector& failedSopInstanceUids,
+                              const std::vector& failureReasons,
+                              const std::string& remoteIp,
+                              const std::string& remoteAet,
+                              const std::string& calledAet) = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h
new file mode 100644
index 0000000..7bffff8
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h
@@ -0,0 +1,40 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IStorageCommitmentRequestHandler.h"
+
+namespace Orthanc
+{
+  class IStorageCommitmentRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentRequestHandlerFactory()
+    {
+    }
+
+    virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h
new file mode 100644
index 0000000..3cf3eec
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h
@@ -0,0 +1,49 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../DicomFormat/DicomMap.h"
+
+#include 
+#include 
+#include 
+
+class DcmDataset;
+
+namespace Orthanc
+{
+  class IStoreRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStoreRequestHandler()
+    {
+    }
+
+    virtual uint16_t Handle(DcmDataset& dicom,
+                            const std::string& remoteIp,
+                            const std::string& remoteAet,
+                            const std::string& calledAet) = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h
new file mode 100644
index 0000000..ae9a7ef
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h
@@ -0,0 +1,40 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IStoreRequestHandler.h"
+
+namespace Orthanc
+{
+  class IStoreRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IStoreRequestHandlerFactory()
+    {
+    }
+
+    virtual IStoreRequestHandler* ConstructStoreRequestHandler() = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h
new file mode 100644
index 0000000..4110fdb
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h
@@ -0,0 +1,45 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "DicomFindAnswers.h"
+
+namespace Orthanc
+{
+  class IWorklistRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IWorklistRequestHandler()
+    {
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        ParsedDicomFile& query,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        ModalityManufacturer manufacturer) = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h
new file mode 100644
index 0000000..2ece5a9
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h
@@ -0,0 +1,40 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IWorklistRequestHandler.h"
+
+namespace Orthanc
+{
+  class IWorklistRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IWorklistRequestHandlerFactory()
+    {
+    }
+
+    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp
new file mode 100644
index 0000000..8d625d1
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp
@@ -0,0 +1,1383 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+  Copyright (C) 1994-2011, OFFIS e.V.
+  All rights reserved.
+
+  This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions
+  are met:
+
+  - Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+  - Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+  - Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "CommandDispatcher.h"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "../../Compatibility.h"
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+#include "../../Toolbox.h"
+#include "FindScp.h"
+#include "GetScp.h"
+#include "MoveScp.h"
+#include "StoreScp.h"
+
+#include      /* for storage commitment */
+#include      /* for class DcmSequenceOfItems */
+#include         /* for variable dcmAllStorageSOPClassUIDs */
+#include       /* for class DcmAssociationConfiguration */
+
+#include 
+
+static OFBool    opt_rejectWithoutImplementationUID = OFFalse;
+
+
+
+static DUL_PRESENTATIONCONTEXT *
+findPresentationContextID(LST_HEAD * head,
+                          T_ASC_PresentationContextID presentationContextID)
+{
+  DUL_PRESENTATIONCONTEXT *pc;
+  LST_HEAD **l;
+  OFBool found = OFFalse;
+
+  if (head == NULL)
+    return NULL;
+
+  l = &head;
+  if (*l == NULL)
+    return NULL;
+
+  pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Head(l));
+  (void)LST_Position(l, OFstatic_cast(LST_NODE *, pc));
+
+  while (pc && !found) {
+    if (pc->presentationContextID == presentationContextID) {
+      found = OFTrue;
+    } else {
+      pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Next(l));
+    }
+  }
+  return pc;
+}
+
+
+/** accept all presenstation contexts for unknown SOP classes,
+ *  i.e. UIDs appearing in the list of abstract syntaxes
+ *  where no corresponding name is defined in the UID dictionary.
+ *  @param params pointer to association parameters structure
+ *  @param transferSyntax transfer syntax to accept
+ *  @param acceptedRole SCU/SCP role to accept
+ */
+static OFCondition acceptUnknownContextsWithTransferSyntax(
+  T_ASC_Parameters * params,
+  const char* transferSyntax,
+  T_ASC_SC_ROLE acceptedRole)
+{
+  int n, i, k;
+  DUL_PRESENTATIONCONTEXT *dpc;
+  T_ASC_PresentationContext pc;
+
+  n = ASC_countPresentationContexts(params);
+  for (i = 0; i < n; i++)
+  {
+    OFCondition cond = ASC_getPresentationContext(params, i, &pc);
+    if (cond.bad()) return cond;
+    OFBool abstractOK = OFFalse;
+    OFBool accepted = OFFalse;
+
+    if (dcmFindNameOfUID(pc.abstractSyntax) == NULL)
+    {
+      abstractOK = OFTrue;
+
+      /* check the transfer syntax */
+      for (k = 0; (k < OFstatic_cast(int, pc.transferSyntaxCount)) && !accepted; k++)
+      {
+        if (strcmp(pc.proposedTransferSyntaxes[k], transferSyntax) == 0)
+        {
+          accepted = OFTrue;
+        }
+      }
+    }
+    
+    if (accepted)
+    {
+      cond = ASC_acceptPresentationContext(
+        params, pc.presentationContextID,
+        transferSyntax, acceptedRole);
+      if (cond.bad()) return cond;
+    } else {
+      T_ASC_P_ResultReason reason;
+
+      /* do not refuse if already accepted */
+      dpc = findPresentationContextID(params->DULparams.acceptedPresentationContext,
+                                      pc.presentationContextID);
+      if (dpc == NULL || dpc->result != ASC_P_ACCEPTANCE)
+      {
+
+        if (abstractOK) {
+          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
+        } else {
+          reason = ASC_P_ABSTRACTSYNTAXNOTSUPPORTED;
+        }
+        /*
+         * If previously this presentation context was refused
+         * because of bad transfer syntax let it stay that way.
+         */
+        if ((dpc != NULL) && (dpc->result == ASC_P_TRANSFERSYNTAXESNOTSUPPORTED))
+          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
+
+        cond = ASC_refusePresentationContext(params, pc.presentationContextID, reason);
+        if (cond.bad()) return cond;
+      }
+    }
+  }
+  return EC_Normal;
+}
+
+
+/** accept all presenstation contexts for unknown SOP classes,
+ *  i.e. UIDs appearing in the list of abstract syntaxes
+ *  where no corresponding name is defined in the UID dictionary.
+ *  This method is passed a list of "preferred" transfer syntaxes.
+ *  @param params pointer to association parameters structure
+ *  @param transferSyntax transfer syntax to accept
+ *  @param acceptedRole SCU/SCP role to accept
+ */
+static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes(
+  T_ASC_Parameters * params,
+  const char* transferSyntaxes[], int transferSyntaxCount,
+  T_ASC_SC_ROLE acceptedRole)
+{
+  OFCondition cond = EC_Normal;
+  /*
+  ** Accept in the order "least wanted" to "most wanted" transfer
+  ** syntax.  Accepting a transfer syntax will override previously
+  ** accepted transfer syntaxes.
+  */
+  for (int i = transferSyntaxCount - 1; i >= 0; i--)
+  {
+    cond = acceptUnknownContextsWithTransferSyntax(params, transferSyntaxes[i], acceptedRole);
+    if (cond.bad()) return cond;
+  }
+  return cond;
+}
+
+
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition AssociationCleanup(T_ASC_Association *assoc)
+    {
+      OFCondition cond = ASC_dropSCPAssociation(assoc);
+      if (cond.bad())
+      {
+        CLOG(ERROR, DICOM) << cond.text();
+        return cond;
+      }
+
+      cond = ASC_destroyAssociation(&assoc);
+      if (cond.bad())
+      {
+        CLOG(ERROR, DICOM) << cond.text();
+        return cond;
+      }
+
+      return cond;
+    }
+
+
+
+    CommandDispatcher* AcceptAssociation(const DicomServer& server,
+                                         T_ASC_Network *net,
+                                         unsigned int maximumPduLength,
+                                         bool useDicomTls)
+    {
+      DcmAssociationConfiguration asccfg;
+      char buf[BUFSIZ];
+      T_ASC_Association *assoc;
+      OFCondition cond;
+      OFString sprofile;
+      OFString temp_str;
+
+      cond = ASC_receiveAssociation(net, &assoc, maximumPduLength, NULL, NULL,
+                                    useDicomTls /*opt_secureConnection*/,
+                                    DUL_NOBLOCK, 1);
+
+      if (cond == DUL_NOASSOCIATIONREQUEST)
+      {
+        // Timeout
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      // if some kind of error occured, take care of it
+      if (cond.bad())
+      {
+        CLOG(ERROR, DICOM) << "Receiving Association failed: " << cond.text();
+        // no matter what kind of error occurred, we need to do a cleanup
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      {
+        OFString str;
+        CLOG(TRACE, DICOM) << "Received Association Parameters:" << std::endl
+                           << ASC_dumpParameters(str, assoc->params, ASC_ASSOC_RQ);
+      }
+
+      // Retrieve the AET and the IP address of the remote modality
+      std::string remoteAet;
+      std::string remoteIp;
+      std::string calledAet;
+  
+      {
+        DIC_AE remoteAet_C;
+        DIC_AE calledAet_C;
+        DIC_AE remoteIp_C;
+        DIC_AE calledIP_C;
+
+        if (
+#if DCMTK_VERSION_NUMBER >= 364
+          ASC_getAPTitles(assoc->params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).bad() ||
+          ASC_getPresentationAddresses(assoc->params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).bad()
+#else
+          ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() ||
+          ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()
+#endif
+          )
+        {
+          T_ASC_RejectParameters rej =
+            {
+              ASC_RESULT_REJECTEDPERMANENT,
+              ASC_SOURCE_SERVICEUSER,
+              ASC_REASON_SU_NOREASON
+            };
+          ASC_rejectAssociation(assoc, &rej);
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+        remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+        remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
+        calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      }
+
+      CLOG(INFO, DICOM) << "Association Received from AET " << remoteAet 
+                        << " on IP " << remoteIp;
+
+
+      {
+        /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE,
+           and storage commitment, if presented */
+
+        std::vector genericTransferSyntaxes;
+        genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
+
+        std::vector knownAbstractSyntaxes;
+
+        // For C-ECHO (always enabled since Orthanc 1.6.0; in earlier
+        // versions, only enabled if C-STORE was also enabled)
+        knownAbstractSyntaxes.push_back(UID_VerificationSOPClass);
+
+        // For C-FIND
+        if (server.HasFindRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
+          knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
+        }
+
+        if (server.HasWorklistRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel);
+        }
+
+        // For C-MOVE
+        if (server.HasMoveRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
+          knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel);
+        }
+
+        // For C-GET
+        if (server.HasGetRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_GETStudyRootQueryRetrieveInformationModel);
+          knownAbstractSyntaxes.push_back(UID_GETPatientRootQueryRetrieveInformationModel);
+        }
+
+        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+          assoc->params,
+          &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(),
+          &genericTransferSyntaxes[0], genericTransferSyntaxes.size());
+        if (cond.bad())
+        {
+          CLOG(INFO, DICOM) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+      
+        /* storage commitment support, new in Orthanc 1.6.0 */
+        if (server.HasStorageCommitmentRequestHandlerFactory())
+        {
+          /**
+           * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept
+           * remote storage commitment requests, and the "SCP" role is
+           * needed to receive storage commitments answers.
+           **/        
+          const char* as[1] = { UID_StorageCommitmentPushModelSOPClass }; 
+          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+            assoc->params, as, 1,
+            &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP);
+          if (cond.bad())
+          {
+            CLOG(INFO, DICOM) << cond.text();
+            AssociationCleanup(assoc);
+            return NULL;
+          }
+        }
+      }
+      
+
+      {
+        /* accept the abstract syntaxes for C-STORE, if presented */
+        
+        std::set storageTransferSyntaxes;
+
+        if (server.HasApplicationEntityFilter())
+        {
+          server.GetApplicationEntityFilter().GetAcceptedTransferSyntaxes(
+            storageTransferSyntaxes, remoteIp, remoteAet, calledAet);
+        }
+        else
+        {
+          /**
+           * In the absence of filter, accept all the known transfer
+           * syntaxes. Note that this is different from Orthanc
+           * framework <= 1.8.2, where only the uncompressed transfer
+           * syntaxes are accepted by default.
+           **/
+          GetAllDicomTransferSyntaxes(storageTransferSyntaxes);
+        }
+
+        if (storageTransferSyntaxes.empty())
+        {
+          LOG(WARNING) << "The DICOM server accepts no transfer syntax, thus C-STORE SCP is disabled";
+        }
+        else
+        {
+          /**
+           * If accepted, put "Little Endian Explicit" at the first
+           * place in the accepted transfer syntaxes. This first place
+           * has an impact on the result of "getscu" (cf. integration
+           * test "test_getscu"). We choose "Little Endian Explicit",
+           * as this preserves the VR of the private tags, even if the
+           * remote modality doesn't have the dictionary of private tags.
+           *
+           * TODO - Should "PREFERRED_TRANSFER_SYNTAX" be an option of
+           * class "DicomServer"?
+           **/
+          const DicomTransferSyntax PREFERRED_TRANSFER_SYNTAX = DicomTransferSyntax_LittleEndianExplicit;
+
+          E_TransferSyntax dummy;
+          assert(FromDcmtkBridge::LookupDcmtkTransferSyntax(dummy, PREFERRED_TRANSFER_SYNTAX));
+
+          std::vector storageTransferSyntaxesC;
+          storageTransferSyntaxesC.reserve(storageTransferSyntaxes.size());
+
+          if (storageTransferSyntaxes.find(PREFERRED_TRANSFER_SYNTAX) != storageTransferSyntaxes.end())
+          {
+            storageTransferSyntaxesC.push_back(GetTransferSyntaxUid(PREFERRED_TRANSFER_SYNTAX));
+          }
+          
+          for (std::set::const_iterator
+                 syntax = storageTransferSyntaxes.begin(); syntax != storageTransferSyntaxes.end(); ++syntax)
+          {
+            if (*syntax != PREFERRED_TRANSFER_SYNTAX &&  // Don't add the preferred transfer syntax twice
+                // Make sure that the current version of DCMTK knows this transfer syntax
+                FromDcmtkBridge::LookupDcmtkTransferSyntax(dummy, *syntax))
+            {
+              storageTransferSyntaxesC.push_back(GetTransferSyntaxUid(*syntax));
+            }
+          }
+
+          /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */
+          size_t count = 0;
+          while (dcmAllStorageSOPClassUIDs[count] != NULL)
+          {
+            count++;
+          }
+        
+#if DCMTK_VERSION_NUMBER >= 362
+          // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
+          // only published if DCMTK >= 3.6.2:
+          // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=137
+          assert(static_cast(count) == numberOfDcmAllStorageSOPClassUIDs);
+#endif
+      
+          if (!server.HasGetRequestHandlerFactory())    // dcmqrsrv.cc line 828
+          {
+            // This branch exactly corresponds to Orthanc <= 1.6.1 (in
+            // which C-GET SCP was not supported)
+            cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+              assoc->params, dcmAllStorageSOPClassUIDs, count,
+              &storageTransferSyntaxesC[0], storageTransferSyntaxesC.size());
+            if (cond.bad())
+            {
+              CLOG(INFO, DICOM) << cond.text();
+              AssociationCleanup(assoc);
+              return NULL;
+            }
+          }
+          else                                         // see dcmqrsrv.cc lines 839 - 876
+          {
+            std::set acceptedStorageClasses;
+
+            if (server.HasApplicationEntityFilter())
+            {
+              server.GetApplicationEntityFilter().GetAcceptedSopClasses(acceptedStorageClasses, 0);
+            }
+
+            /* accept storage syntaxes with proposed role */
+            int npc = ASC_countPresentationContexts(assoc->params);
+            for (int i = 0; i < npc; i++)
+            {
+              T_ASC_PresentationContext pc;
+              ASC_getPresentationContext(assoc->params, i, &pc);
+              if (acceptedStorageClasses.find(pc.abstractSyntax) != acceptedStorageClasses.end()
+                 || (!server.HasApplicationEntityFilter() && dcmIsaStorageSOPClassUID(pc.abstractSyntax)))  // previous behavior kept for compatibility in case the server does not have an ApplicationEntityFilter
+              {
+                /**
+                 * We are prepared to accept whatever role the caller
+                 * proposes.  Normally we can be the SCP of the Storage
+                 * Service Class.  When processing the C-GET operation
+                 * we can be the SCU of the Storage Service Class.
+                 **/
+                const T_ASC_SC_ROLE role = pc.proposedRole;
+            
+                /**
+                 * Accept in the order "least wanted" to "most wanted"
+                 * transfer syntax.  Accepting a transfer syntax will
+                 * override previously accepted transfer syntaxes.
+                 * Since Orthanc 1.11.2+, we give priority to the transfer
+                 * syntaxes proposed in the presentation context.
+                 **/
+                for (int j = static_cast(pc.transferSyntaxCount)-1; j >=0; j--)
+                {
+                  for (int k = static_cast(storageTransferSyntaxesC.size()) - 1; k >= 0; k--)
+                  {
+                    /**
+                     * If the transfer syntax was proposed then we can accept it
+                     * appears in our supported list of transfer syntaxes
+                     **/
+                    if (strcmp(pc.proposedTransferSyntaxes[j], storageTransferSyntaxesC[k]) == 0)
+                    {
+                      cond = ASC_acceptPresentationContext(
+                        assoc->params, pc.presentationContextID, storageTransferSyntaxesC[k], role);
+                      if (cond.bad())
+                      {
+                        CLOG(INFO, DICOM) << cond.text();
+                        AssociationCleanup(assoc);
+                        return NULL;
+                      }
+                    }
+                  }
+                }
+              }
+            } /* for */
+          }
+
+          if (!server.HasApplicationEntityFilter() ||
+              server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet))
+          {
+            /*
+             * Promiscous mode is enabled: Accept everything not known not
+             * to be a storage SOP class.
+             **/
+            cond = acceptUnknownContextsWithPreferredTransferSyntaxes(
+              assoc->params, &storageTransferSyntaxesC[0], storageTransferSyntaxesC.size(), ASC_SC_ROLE_DEFAULT);
+            if (cond.bad())
+            {
+              CLOG(INFO, DICOM) << cond.text();
+              AssociationCleanup(assoc);
+              return NULL;
+            }
+          }
+        }
+      }
+
+      /* set our app title */
+      ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str());
+
+      /* acknowledge or reject this association */
+#if DCMTK_VERSION_NUMBER >= 364
+      cond = ASC_getApplicationContextName(assoc->params, buf, sizeof(buf));
+#else
+      cond = ASC_getApplicationContextName(assoc->params, buf);
+#endif
+
+      if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0)
+      {
+        /* reject: the application context name is not supported */
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED
+          };
+
+        CLOG(INFO, DICOM) << "Association Rejected: Bad Application Context Name: " << buf;
+        cond = ASC_rejectAssociation(assoc, &rej);
+        if (cond.bad())
+        {
+          CLOG(INFO, DICOM) << cond.text();
+        }
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      /* check the AETs */
+      if (!server.IsMyAETitle(calledAet))
+      {
+        CLOG(WARNING, DICOM) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")";
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (server.HasApplicationEntityFilter() &&
+          !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet))
+      {
+        CLOG(WARNING, DICOM) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp;
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (opt_rejectWithoutImplementationUID && 
+          strlen(assoc->params->theirImplementationClassUID) == 0)
+      {
+        /* reject: the no implementation Class UID provided */
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_NOREASON
+          };
+
+        CLOG(INFO, DICOM) << "Association Rejected: No Implementation Class UID provided";
+        cond = ASC_rejectAssociation(assoc, &rej);
+        if (cond.bad())
+        {
+          CLOG(INFO, DICOM) << cond.text();
+        }
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      {
+        cond = ASC_acknowledgeAssociation(assoc);
+        if (cond.bad())
+        {
+          CLOG(ERROR, DICOM) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+        {
+          std::string suffix;
+          if (ASC_countAcceptedPresentationContexts(assoc->params) == 0)
+            suffix = " (but no valid presentation contexts)";
+          
+          CLOG(INFO, DICOM) << "Association Acknowledged (Max Send PDV: " << assoc->sendPDVLength
+                            << ") to AET " << remoteAet << " on IP " << remoteIp << suffix;
+        }
+
+        {
+          OFString str;
+          CLOG(TRACE, DICOM) << "Association Acknowledged Details:" << std::endl
+                             << ASC_dumpParameters(str, assoc->params, ASC_ASSOC_AC);
+        }
+      }
+
+      IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
+      return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, maximumPduLength, filter);
+    }
+
+
+    CommandDispatcher::CommandDispatcher(const DicomServer& server,
+                                         T_ASC_Association* assoc,
+                                         const std::string& remoteIp,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet,
+                                         unsigned int maximumPduLength,
+                                         IApplicationEntityFilter* filter) :
+      server_(server),
+      assoc_(assoc),
+      remoteIp_(remoteIp),
+      remoteAet_(remoteAet),
+      calledAet_(calledAet),
+      filter_(filter)
+    {
+      associationTimeout_ = server.GetAssociationTimeout();
+      elapsedTimeSinceLastCommand_ = 0;
+    }
+
+
+    CommandDispatcher::~CommandDispatcher()
+    {
+      try
+      {
+        AssociationCleanup(assoc_);
+      }
+      catch (...)
+      {
+        CLOG(ERROR, DICOM) << "Some association was not cleanly aborted";
+      }
+    }
+
+
+    bool CommandDispatcher::Step()
+    /*
+     * This function receives DIMSE commmands over the network connection
+     * and handles these commands correspondingly. Note that in case of
+     * storscp only C-ECHO-RQ and C-STORE-RQ commands can be processed.
+     */
+    {
+      bool finished = false;
+
+      // receive a DIMSE command over the network, with a timeout of 1 second
+      DcmDataset *statusDetail = NULL;
+      T_ASC_PresentationContextID presID = 0;
+      T_DIMSE_Message msg;
+
+      OFCondition cond = DIMSE_receiveCommand(assoc_, DIMSE_NONBLOCKING, 1, &presID, &msg, &statusDetail);
+      elapsedTimeSinceLastCommand_++;
+    
+      // if the command which was received has extra status
+      // detail information, dump this information
+      if (statusDetail != NULL)
+      {
+        std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+        statusDetail->print(s);
+        CLOG(TRACE, DICOM) << "Status Detail:" << std::endl << s.str();
+
+        delete statusDetail;
+      }
+
+      if (cond == DIMSE_OUTOFRESOURCES)
+      {
+        finished = true;
+      }
+      else if (cond == DIMSE_NODATAAVAILABLE)
+      {
+        // Timeout due to DIMSE_NONBLOCKING
+        if (associationTimeout_ != 0 && 
+            elapsedTimeSinceLastCommand_ >= associationTimeout_)
+        {
+          // This timeout is actually a association timeout
+          finished = true;
+        }
+      }
+      else if (cond == EC_Normal)
+      {
+        {
+          OFString str;
+          CLOG(TRACE, DICOM) << "Received Command:" << std::endl
+                             << DIMSE_dumpMessage(str, msg, DIMSE_INCOMING, NULL, presID);
+        }
+        
+        // Reset the association timeout counter
+        elapsedTimeSinceLastCommand_ = 0;
+
+        // Convert the type of request to Orthanc's internal type
+        bool supported = false;
+        DicomRequestType request;
+        switch (msg.CommandField)
+        {
+          case DIMSE_C_ECHO_RQ:
+            request = DicomRequestType_Echo;
+            supported = true;
+            break;
+
+          case DIMSE_C_STORE_RQ:
+            request = DicomRequestType_Store;
+            supported = true;
+            break;
+
+          case DIMSE_C_MOVE_RQ:
+            request = DicomRequestType_Move;
+            supported = true;
+            break;
+            
+          case DIMSE_C_GET_RQ:
+            request = DicomRequestType_Get;
+            supported = true;
+            break;
+
+          case DIMSE_C_FIND_RQ:
+          {
+            std::string sopClassUid(msg.msg.CFindRQ.AffectedSOPClassUID);
+            if (sopClassUid == UID_FINDModalityWorklistInformationModel)
+            {
+              request = DicomRequestType_FindWorklist;
+            }
+            else
+            {
+              request = DicomRequestType_Find;
+            }
+            supported = true;
+            break;
+          }
+          
+          case DIMSE_N_ACTION_RQ:
+            request = DicomRequestType_NAction;
+            supported = true;
+            break;
+
+          case DIMSE_N_EVENT_REPORT_RQ:
+            request = DicomRequestType_NEventReport;
+            supported = true;
+            break;
+
+          default:
+            // we cannot handle this kind of message
+            cond = DIMSE_BADCOMMANDTYPE;
+            CLOG(ERROR, DICOM) << "cannot handle command: 0x" << std::hex << msg.CommandField;
+            break;
+        }
+
+
+        // Check whether this request is allowed by the security filter
+        if (supported && 
+            filter_ != NULL &&
+            !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request))
+        {
+          CLOG(WARNING, DICOM) << "Rejected " << EnumerationToString(request)
+                               << " request from remote DICOM modality with AET \""
+                               << remoteAet_ << "\" and hostname \"" << remoteIp_ << "\"";
+          cond = DIMSE_ILLEGALASSOCIATION;
+          supported = false;
+          finished = true;
+        }
+
+        // in case we received a supported message, process this command
+        if (supported)
+        {
+          // If anything goes wrong, there will be a "BADCOMMANDTYPE" answer
+          cond = DIMSE_BADCOMMANDTYPE;
+
+          switch (request)
+          {
+            case DicomRequestType_Echo:
+              cond = EchoScp(assoc_, &msg, presID);
+              break;
+
+            case DicomRequestType_Store:
+              if (server_.HasStoreRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr handler
+                  (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_, associationTimeout_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Move:
+              if (server_.HasMoveRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr handler
+                  (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+                }
+              }
+              break;
+              
+            case DicomRequestType_Get:
+              if (server_.HasGetRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr handler
+                  (server_.GetGetRequestHandlerFactory().ConstructGetRequestHandler());
+                
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::getScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Find:
+            case DicomRequestType_FindWorklist:
+              if (server_.HasFindRequestHandlerFactory() || // Should always be true
+                  server_.HasWorklistRequestHandlerFactory())
+              {
+                std::unique_ptr findHandler;
+                if (server_.HasFindRequestHandlerFactory())
+                {
+                  findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
+                }
+
+                std::unique_ptr worklistHandler;
+                if (server_.HasWorklistRequestHandlerFactory())
+                {
+                  worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler());
+                }
+
+                cond = Internals::findScp(assoc_, &msg, presID, findHandler.get(), worklistHandler.get(),
+                                          remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+              }
+              break;
+
+            case DicomRequestType_NAction:
+              cond = NActionScp(&msg, presID);
+              break;              
+
+            case DicomRequestType_NEventReport:
+              cond = NEventReportScp(&msg, presID);
+              break;              
+
+            default:
+              // Should never happen
+              break;
+          }
+        }
+      }
+      else
+      {
+        // Bad status, which indicates the closing of the connection by
+        // the peer or a network error
+        finished = true;
+
+        CLOG(INFO, DICOM) << "Finishing association with AET " << remoteAet_
+                          << " on IP " << remoteIp_ << ": " << cond.text();
+      }
+    
+      if (finished)
+      {
+        if (cond == DUL_PEERREQUESTEDRELEASE)
+        {
+          CLOG(INFO, DICOM) << "Association Release with AET " << remoteAet_ << " on IP " << remoteIp_;
+          ASC_acknowledgeRelease(assoc_);
+        }
+        else if (cond == DUL_PEERABORTEDASSOCIATION)
+        {
+          CLOG(INFO, DICOM) << "Association Aborted with AET " << remoteAet_ << " on IP " << remoteIp_;
+        }
+        else
+        {
+          OFString temp_str;
+          CLOG(INFO, DICOM) << "DIMSE failure (aborting association with AET " << remoteAet_
+                            << " on IP " << remoteIp_ << "): " << cond.text();
+          /* some kind of error so abort the association */
+          ASC_abortAssociation(assoc_);
+        }
+      }
+
+      return !finished;
+    }
+
+
+    OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID)
+    {
+      OFString temp_str;
+      CLOG(INFO, DICOM) << "Received Echo Request";
+
+      /* the echo succeeded !! */
+      OFCondition cond = DIMSE_sendEchoResponse(assoc, presID, &msg->msg.CEchoRQ, STATUS_Success, NULL);
+      if (cond.bad())
+      {
+        CLOG(ERROR, DICOM) << "Echo SCP Failed: " << cond.text();
+      }
+      return cond;
+    }
+
+
+    static DcmDataset* ReadDataset(T_ASC_Association* assoc,
+                                   const char* errorMessage,
+                                   int timeout)
+    {
+      DcmDataset *tmp = NULL;
+      T_ASC_PresentationContextID presIdData;
+    
+      OFCondition cond = DIMSE_receiveDataSetInMemory(
+        assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
+        &presIdData, &tmp, NULL, NULL);
+      if (!cond.good() ||
+          tmp == NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, errorMessage);
+      }
+
+      return tmp;
+    }
+
+
+    static std::string ReadString(DcmDataset& dataset,
+                                  const DcmTagKey& tag)
+    {
+      const char* s = NULL;
+      if (!dataset.findAndGetString(tag, s).good() ||
+          s == NULL)
+      {
+        char buf[64];
+        sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)",
+                tag.getGroup(), tag.getElement());
+        throw OrthancException(ErrorCode_NetworkProtocol, buf);
+      }
+
+      return std::string(s);
+    }
+
+
+    static void ReadSopSequence(
+      std::vector& sopClassUids,
+      std::vector& sopInstanceUids,
+      std::vector* failureReasons, // Can be NULL
+      DcmDataset& dataset,
+      const DcmTagKey& tag,
+      bool mandatory)
+    {
+      sopClassUids.clear();
+      sopInstanceUids.clear();
+
+      if (failureReasons)
+      {
+        failureReasons->clear();
+      }
+
+      DcmSequenceOfItems* sequence = NULL;
+      if (!dataset.findAndGetSequence(tag, sequence).good() ||
+          sequence == NULL)
+      {
+        if (mandatory)
+        {        
+          char buf[64];
+          sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)",
+                  tag.getGroup(), tag.getElement());
+          throw OrthancException(ErrorCode_NetworkProtocol, buf);
+        }
+        else
+        {
+          return;
+        }
+      }
+
+      sopClassUids.reserve(sequence->card());
+      sopInstanceUids.reserve(sequence->card());
+
+      if (failureReasons)
+      {
+        failureReasons->reserve(sequence->card());
+      }
+
+      for (unsigned long i = 0; i < sequence->card(); i++)
+      {
+        const char* a = NULL;
+        const char* b = NULL;
+        if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() ||
+            !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() ||
+            a == NULL ||
+            b == NULL)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol,
+                                 "Missing Referenced SOP Class/Instance UID "
+                                 "in storage commitment dataset");
+        }
+
+        sopClassUids.push_back(a);
+        sopInstanceUids.push_back(b);
+
+        if (failureReasons != NULL)
+        {
+          Uint16 reason;
+          if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good())
+          {
+            throw OrthancException(ErrorCode_NetworkProtocol,
+                                   "Missing Failure Reason (0008,1197) "
+                                   "in storage commitment dataset");
+          }
+
+          failureReasons->push_back(static_cast(reason));
+        }
+      }
+    }
+
+    
+    OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg,
+                                              T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, only storage commitment is
+       * supported with DICOM N-ACTION. This corresponds to the case
+       * where "Action Type ID" equals "1".
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
+       **/
+      
+      if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment request is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ;
+
+      if (request.ActionTypeID != 1)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Only storage commitment is implemented for DICOM N-ACTION SCP");
+      }
+
+      if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (request.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment request without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-1. Storage Commitment Request - Action Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1
+       **/
+      
+      std::unique_ptr dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_));
+      assert(dataset.get() != NULL);
+
+      {
+        std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+        dataset->print(s);
+        CLOG(TRACE, DICOM) << "Received Storage Commitment Request:" << std::endl << s.str();
+      }
+      
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector sopClassUid, sopInstanceUid;
+      ReadSopSequence(sopClassUid, sopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence, true /* mandatory */);
+
+      CLOG(INFO, DICOM) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < sopClassUid.size(); i++)
+      {
+        CLOG(INFO, DICOM) << "  (" << (i + 1) << "/" << sopClassUid.size()
+                          << ") queried SOP Class/Instance UID: "
+                          << sopClassUid[i] << " / " << sopInstanceUid[i];
+      }
+
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+  
+      try
+      {
+        std::unique_ptr handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid,
+                               remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        CLOG(ERROR, DICOM) << "Error while processing an incoming storage commitment request: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_ACTION_RSP;
+
+        T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP;
+        content.MessageIDBeingRespondedTo = request.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID;
+
+        {
+          OFString str;
+          CLOG(TRACE, DICOM) << "Sending Storage Commitment Request Response:" << std::endl
+                             << DIMSE_dumpMessage(str, response, DIMSE_OUTGOING);
+        }
+        
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
+
+
+    OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg,
+                                                   T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for
+       * storage commitment.
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
+       **/
+
+      if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment report is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ;
+
+      if (report.EventTypeID != 1 /* successful */ &&
+          report.EventTypeID != 2 /* failures exist */)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Unknown event for DICOM N-EVENT-REPORT SCP");
+      }
+
+      if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (report.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment report without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-2. Storage Commitment Result - Event Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2
+       **/
+      
+      std::unique_ptr dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_));
+      assert(dataset.get() != NULL);
+
+      {
+        std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+        dataset->print(s);
+        CLOG(TRACE, DICOM) << "Received Storage Commitment Report:" << std::endl << s.str();
+      }
+      
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector successSopClassUid, successSopInstanceUid;
+      ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence,
+                      (report.EventTypeID == 1) /* mandatory in the case of success */);
+
+      std::vector failedSopClassUid, failedSopInstanceUid;
+      std::vector failureReasons;
+
+      if (report.EventTypeID == 2 /* failures exist */)
+      {
+        ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons,
+                        *dataset, DCM_FailedSOPSequence, true);
+      }
+
+      CLOG(INFO, DICOM) << "Incoming storage commitment report, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < successSopClassUid.size(); i++)
+      {
+        CLOG(INFO, DICOM) << "  (success " << (i + 1) << "/" << successSopClassUid.size()
+                          << ") SOP Class/Instance UID: "
+                          << successSopClassUid[i] << " / " << successSopInstanceUid[i];
+      }
+
+      for (size_t i = 0; i < failedSopClassUid.size(); i++)
+      {
+        CLOG(INFO, DICOM) << "  (failure " << (i + 1) << "/" << failedSopClassUid.size()
+                          << ") SOP Class/Instance UID: "
+                          << failedSopClassUid[i] << " / " << failedSopInstanceUid[i];
+      }
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+
+      try
+      {
+        std::unique_ptr handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
+                              failedSopClassUid, failedSopInstanceUid, failureReasons,
+                              remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        CLOG(ERROR, DICOM) << "Error while processing an incoming storage commitment report: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+      
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_EVENT_REPORT_RSP;
+
+        T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP;
+        content.MessageIDBeingRespondedTo = report.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID;
+
+        {
+          OFString str;
+          CLOG(TRACE, DICOM) << "Sending Storage Commitment Report Response:" << std::endl
+                             << DIMSE_dumpMessage(str, response, DIMSE_OUTGOING);
+        }
+
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h
new file mode 100644
index 0000000..00893bd
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h
@@ -0,0 +1,79 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../DicomServer.h"
+#include "../../MultiThreading/IRunnableBySteps.h"
+
+#include 
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition AssociationCleanup(T_ASC_Association *assoc);
+
+    class CommandDispatcher : public IRunnableBySteps
+    {
+    private:
+      uint32_t associationTimeout_;
+      uint32_t elapsedTimeSinceLastCommand_;
+      const DicomServer& server_;
+      T_ASC_Association* assoc_;
+      std::string remoteIp_;
+      std::string remoteAet_;
+      std::string calledAet_;
+      IApplicationEntityFilter* filter_;
+
+      OFCondition NActionScp(T_DIMSE_Message* msg, 
+                             T_ASC_PresentationContextID presID);
+
+      OFCondition NEventReportScp(T_DIMSE_Message* msg, 
+                                  T_ASC_PresentationContextID presID);
+      
+    public:
+      CommandDispatcher(const DicomServer& server,
+                        T_ASC_Association* assoc,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        unsigned int maximumPduLength,
+                        IApplicationEntityFilter* filter);
+
+      virtual ~CommandDispatcher();
+
+      virtual bool Step();
+    };
+
+    CommandDispatcher* AcceptAssociation(const DicomServer& server, 
+                                         T_ASC_Network *net,
+                                         unsigned int maximumPduLength,
+                                         bool useDicomTls);
+
+    OFCondition EchoScp(T_ASC_Association* assoc, 
+                        T_DIMSE_Message* msg, 
+                        T_ASC_PresentationContextID presID);
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp
new file mode 100644
index 0000000..8669445
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp
@@ -0,0 +1,324 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "DicomTls.h"
+
+
+// This must be *before* the inclusion of "Logging.h"
+#if defined(__ORTHANC_FILE__)
+// Prevents the system-wide DCMTK library from leaking the
+// full path of this source file in "DCMTLS_ERROR()"
+#  undef __FILE__
+#  define __FILE__ __ORTHANC_FILE__
+#endif
+
+
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+#include "../../SystemToolbox.h"
+#include "../../Toolbox.h"
+#include 
+#include 
+
+#if DCMTK_VERSION_NUMBER < 364
+#  define DCF_Filetype_PEM  SSL_FILETYPE_PEM
+#  if OPENSSL_VERSION_NUMBER >= 0x0090700fL
+// This seems to correspond to TSP_Profile_AES: https://support.dcmtk.org/docs/tlsciphr_8h.html
+static std::string opt_ciphersuites(TLS1_TXT_RSA_WITH_AES_128_SHA ":" SSL3_TXT_RSA_DES_192_CBC3_SHA);
+#  else
+// This seems to correspond to TSP_Profile_Basic in DCMTK >= 3.6.4: https://support.dcmtk.org/docs/tlsciphr_8h.html
+static std::string opt_ciphersuites(SSL3_TXT_RSA_DES_192_CBC3_SHA);
+#  endif
+#endif
+
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+#if DCMTK_VERSION_NUMBER >= 367
+    static bool IsFailure(OFCondition cond)
+    {
+      return !cond.good();
+    }
+#else
+    static bool IsFailure(DcmTransportLayerStatus status)
+    {
+      return (status != TCS_ok);
+    }
+#endif
+
+
+#if DCMTK_VERSION_NUMBER >= 367
+    static OFCondition MyConvertOpenSSLError(unsigned long errorCode, OFBool logAsError)
+    {
+      return DcmTLSTransportLayer::convertOpenSSLError(errorCode, logAsError);
+    }
+#else
+    static OFCondition MyConvertOpenSSLError(unsigned long errorCode, OFBool logAsError)
+    {
+      if (errorCode == 0)
+      {
+        return EC_Normal;
+      }
+      else
+      {
+        const char *err = ERR_reason_error_string(errorCode);
+        if (err == NULL)
+        {
+          err = "OpenSSL error";
+        }
+
+        if (logAsError)
+        {
+          DCMTLS_ERROR("OpenSSL error " << STD_NAMESPACE hex << STD_NAMESPACE setfill('0')
+                       << STD_NAMESPACE setw(8) << errorCode << ": " << err);
+        }
+
+        // The "2" below corresponds to the same error code as "DCMTLS_EC_FailedToSetCiphersuites"
+        return OFCondition(OFM_dcmtls, 2, OF_error, err);
+      }
+    }
+#endif
+
+
+    DcmTLSTransportLayer* InitializeDicomTls(T_ASC_Network *network,
+                                             T_ASC_NetworkRole role,
+                                             const std::string& ownPrivateKeyPath,
+                                             const std::string& ownCertificatePath,
+                                             const std::string& trustedCertificatesPath,
+                                             bool requireRemoteCertificate,
+                                             unsigned int minimalTlsVersion,
+                                             const std::set& ciphers)
+    {
+      if (network == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      if (role != NET_ACCEPTOR &&
+          role != NET_REQUESTOR)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown role");
+      }
+    
+      if (requireRemoteCertificate && !SystemToolbox::IsRegularFile(trustedCertificatesPath))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Cannot read file with trusted certificates for DICOM TLS: " +
+                               trustedCertificatesPath);
+      }
+
+      if (!SystemToolbox::IsRegularFile(ownPrivateKeyPath))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Cannot read file with own private key for DICOM TLS: " +
+                               ownPrivateKeyPath);
+      }
+
+      if (!SystemToolbox::IsRegularFile(ownCertificatePath))
+      {
+        throw OrthancException(ErrorCode_InexistentFile, "Cannot read file with own certificate for DICOM TLS: " +
+                               ownCertificatePath);
+      }
+
+      CLOG(INFO, DICOM) << "Initializing DICOM TLS for Orthanc "
+                        << (role == NET_ACCEPTOR ? "SCP" : "SCU");
+
+#if DCMTK_VERSION_NUMBER >= 364
+      const T_ASC_NetworkRole tmpRole = role;
+#else
+      int tmpRole;
+      switch (role)
+      {
+        case NET_ACCEPTOR:
+          tmpRole = DICOM_APPLICATION_ACCEPTOR;
+          break;
+          
+        case NET_REQUESTOR:
+          tmpRole = DICOM_APPLICATION_REQUESTOR;
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }          
+#endif
+      
+      std::unique_ptr tls(
+        new DcmTLSTransportLayer(tmpRole /*opt_networkRole*/, NULL /*opt_readSeedFile*/,
+                                 OFFalse /*initializeOpenSSL, done by Orthanc::Toolbox::InitializeOpenSsl()*/));
+
+      if (requireRemoteCertificate && IsFailure(tls->addTrustedCertificateFile(trustedCertificatesPath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/)))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with trusted certificates for DICOM TLS: " +
+                               trustedCertificatesPath);
+      }
+
+      if (IsFailure(tls->setPrivateKeyFile(ownPrivateKeyPath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/)))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with private key for DICOM TLS: " +
+                               ownPrivateKeyPath);
+      }
+
+      if (IsFailure(tls->setCertificateFile(
+                      ownCertificatePath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/
+#if DCMTK_VERSION_NUMBER >= 368
+                      /**
+                       * DICOM BCP 195 RFC 8996 TLS Profile, based on RFC 8996 and RFC 9325.
+                       * This profile only negotiates TLS 1.2 or newer, and will not fall back to
+                       * previous TLS versions. It provides the higher security level offered by the
+                       * 2021 revised edition of BCP 195.
+                       **/
+                      , TSP_Profile_BCP_195_RFC_8996
+#endif
+                      )))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with own certificate for DICOM TLS: " +
+                               ownCertificatePath);
+      }
+
+      if (!tls->checkPrivateKeyMatchesCertificate())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "The private key doesn't match the own certificate: " +
+                               ownPrivateKeyPath + " vs. " + ownCertificatePath);
+      }
+
+#if DCMTK_VERSION_NUMBER >= 364
+      if (minimalTlsVersion == 0) // use the default values (same behavior as before 1.12.4)
+      {
+        if (ciphers.size() > 0)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "The cipher suites can not be specified when using the default BCP profile");
+        }
+
+        if (IsFailure(tls->setTLSProfile(TSP_Profile_BCP195 /*opt_tlsProfile*/)))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        }
+      
+        if (IsFailure(tls->activateCipherSuites()))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot activate the cipher suites for DICOM TLS");
+        }
+      }
+      else
+      {
+        // Fine tune the SSL context
+        if (IsFailure(tls->setTLSProfile(TSP_Profile_None)))
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot set the DICOM TLS profile");
+        }
+
+        DcmTLSTransportLayer::native_handle_type sslNativeHandle = tls->getNativeHandle();
+        SSL_CTX_clear_options(sslNativeHandle, SSL_OP_NO_SSL_MASK);
+        if (minimalTlsVersion > 1) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_SSLv3);
+        }
+        if (minimalTlsVersion > 2) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1);
+        }
+        if (minimalTlsVersion > 3) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1_1);
+        }
+        if (minimalTlsVersion > 4) 
+        {
+          SSL_CTX_set_options(sslNativeHandle, SSL_OP_NO_TLSv1_2);
+        }
+
+        std::set ciphersTls;
+        std::set ciphersTls13;
+
+        // DCMTK 3.8 is missing a method to add TLS13 cipher suite in the DcmTLSTransportLayer interface.
+        // And, anyway, since we do not run dcmtkPrepare.cmake, DCMTK is not aware of TLS v1.3 cipher suite names.
+        for (std::set::const_iterator it = ciphers.begin(); it != ciphers.end(); ++it)
+        {
+          bool isValid = false;
+          if (DcmTLSCiphersuiteHandler::lookupCiphersuiteByOpenSSLName(it->c_str()) != DcmTLSCiphersuiteHandler::unknownCipherSuiteIndex)
+          {
+            ciphersTls.insert(it->c_str());
+            isValid = true;
+          }
+          
+          // list of TLS v1.3 ciphers according to https://www.openssl.org/docs/man3.3/man1/openssl-ciphers.html
+          if (strstr("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256:TLS_AES_128_CCM_8_SHA256", it->c_str()) != NULL)
+          {
+            ciphersTls13.insert(it->c_str());
+            isValid = true;
+          }
+
+          if (!isValid)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat, "The cipher suite " + *it + " is not recognized as valid cipher suite by OpenSSL ");
+          }
+        }
+
+        std::string joinedCiphersTls;
+        std::string joinedCiphersTls13;
+        Toolbox::JoinStrings(joinedCiphersTls, ciphersTls, ":");
+        Toolbox::JoinStrings(joinedCiphersTls13, ciphersTls13, ":");
+
+        if (joinedCiphersTls.size() > 0 && SSL_CTX_set_cipher_list(sslNativeHandle, joinedCiphersTls.c_str()) != 1)
+        {
+          OFCondition cond = MyConvertOpenSSLError(ERR_get_error(), OFTrue);
+          throw OrthancException(ErrorCode_InternalError, "Unable to configure cipher suite.  OpenSSL error: " + boost::lexical_cast(cond.code()) + " - " + cond.text());
+        }
+
+        if (joinedCiphersTls13.size() > 0 && SSL_CTX_set_ciphersuites(sslNativeHandle, joinedCiphersTls13.c_str()) != 1)
+        {
+          OFCondition cond = MyConvertOpenSSLError(ERR_get_error(), OFTrue);
+          throw OrthancException(ErrorCode_InternalError, "Unable to configure cipher suite for TLS 1.3.  OpenSSL error: " + boost::lexical_cast(cond.code()) + " - " + cond.text());
+        }
+
+      }
+#else
+      CLOG(INFO, DICOM) << "Using the following cipher suites for DICOM TLS: " << opt_ciphersuites;
+      if (IsFailure(tls->setCipherSuites(opt_ciphersuites.c_str())))
+      {
+        throw OrthancException(ErrorCode_InternalError, "Unable to set cipher suites to: " + opt_ciphersuites);
+      }
+#endif
+
+      if (requireRemoteCertificate)
+      {
+        // Check remote certificate, fail if no certificate is present
+        tls->setCertificateVerification(DCV_requireCertificate /*opt_certVerification*/);
+      }
+      else
+      {
+        // From 1.12.4, do not even request remote certificate (prior to 1.12.4, we were requesting a certificates, checking it if present and succeeding if not present)
+        tls->setCertificateVerification(DCV_ignoreCertificate /*opt_certVerification*/);
+      }
+      
+      if (ASC_setTransportLayer(network, tls.get(), 0).bad())
+      {
+        throw OrthancException(ErrorCode_InternalError, "Cannot enable DICOM TLS in the Orthanc " +
+                               std::string(role == NET_ACCEPTOR ? "SCP" : "SCU"));
+      }
+
+      return tls.release();
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h
new file mode 100644
index 0000000..4064207
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h
@@ -0,0 +1,59 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if ORTHANC_ENABLE_SSL != 1
+#  error SSL support must be enabled to use this file
+#endif
+
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    DcmTLSTransportLayer* InitializeDicomTls(
+      T_ASC_Network *network,
+      T_ASC_NetworkRole role,
+      const std::string& ownPrivateKeyPath,        // This is the first argument of "+tls" option from DCMTK command-line tools
+      const std::string& ownCertificatePath,       // This is the second argument of "+tls" option
+      const std::string& trustedCertificatesPath,  // This is the "--add-cert-file" ("+cf") option
+      bool requireRemoteCertificate,               // "true" means "--require-peer-cert", "false" means "--ignore-peer-cert"
+      unsigned int minimalTlsVersion,              // 0 = default BCP195, 5 = TLS1.3 only
+      const std::set& acceptedCiphers
+    );
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp
new file mode 100644
index 0000000..81f7fd7
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp
@@ -0,0 +1,387 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+  Copyright (C) 1994-2011, OFFIS e.V.
+  All rights reserved.
+
+  This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions
+  are met:
+
+  - Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+  - Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+  - Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+
+#include "../../PrecompiledHeaders.h"
+#include "FindScp.h"
+
+#include "../../DicomFormat/DicomArray.h"
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include 
+#include 
+
+
+
+/**
+ * The function below is extracted from DCMTK 3.6.0, cf. file
+ * "dcmtk-3.6.0/dcmwlm/libsrc/wldsfs.cc".
+ **/
+
+static void HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(DcmDataset *dataset, 
+                                                                             const DcmTagKey &sequenceTagKey)
+// Date         : May 3, 2005
+// Author       : Thomas Wilkens
+// Task         : This function performs a check on a sequence attribute in the given dataset. At two different places
+//                in the definition of the DICOM worklist management service, a sequence attribute with a return type
+//                of 2 is mentioned containing two 1C attributes in its item; the condition of the two 1C attributes
+//                specifies that in case a sequence item is present, then these two attributes must be existent and
+//                must contain a value. (I am talking about ReferencedStudySequence and ReferencedPatientSequence.)
+//                In cases where the sequence attribute contains exactly one item with an empty ReferencedSOPClass
+//                and an empty ReferencedSOPInstance, we want to remove the item from the sequence. This is what
+//                this function does.
+// Parameters   : dataset         - [in] Dataset in which the consistency of the sequence attribute shall be checked.
+//                sequenceTagKey  - [in] DcmTagKey of the sequence attribute which shall be checked.
+// Return Value : none.
+{
+  DcmElement *sequenceAttribute = NULL, *referencedSOPClassUIDAttribute = NULL, *referencedSOPInstanceUIDAttribute = NULL;
+
+  // in case the sequence attribute contains exactly one item with an empty
+  // ReferencedSOPClassUID and an empty ReferencedSOPInstanceUID, remove the item
+  if( dataset->findAndGetElement( sequenceTagKey, sequenceAttribute ).good() &&
+      ( (DcmSequenceOfItems*)sequenceAttribute )->card() == 1 &&
+      ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPClassUID, referencedSOPClassUIDAttribute ).good() &&
+      referencedSOPClassUIDAttribute->getLength() == 0 &&
+      ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPInstanceUID, referencedSOPInstanceUIDAttribute, OFFalse ).good() &&
+      referencedSOPInstanceUIDAttribute->getLength() == 0 )
+  {
+    DcmItem *item = ((DcmSequenceOfItems*)sequenceAttribute)->remove( ((DcmSequenceOfItems*)sequenceAttribute)->getItem(0) );
+    delete item;
+  }
+}
+
+
+
+namespace Orthanc
+{
+  namespace
+  {  
+    struct FindScpData
+    {
+      IFindRequestHandler* findHandler_;
+      IWorklistRequestHandler* worklistHandler_;
+      DicomFindAnswers answers_;
+      DcmDataset* lastRequest_;
+      const std::string* remoteIp_;
+      const std::string* remoteAet_;
+      const std::string* calledAet_;
+
+      FindScpData() :
+        findHandler_(NULL),
+        worklistHandler_(NULL),
+        answers_(false),
+        lastRequest_(NULL),
+        remoteIp_(NULL),
+        remoteAet_(NULL),
+        calledAet_(NULL)
+      {
+      }
+    };
+
+
+
+    static void FixWorklistQuery(ParsedDicomFile& query)
+    {
+      // TODO: Check out
+      // WlmDataSourceFileSystem::HandleExistentButEmptyDescriptionAndCodeSequenceAttributes()"
+      // in DCMTK 3.6.0
+
+      DcmDataset* dataset = query.GetDcmtkObject().getDataset();      
+      HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(dataset, DCM_ReferencedStudySequence);
+      HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(dataset, DCM_ReferencedPatientSequence);
+    }
+
+
+    static void FixFindQuery(DicomMap& target,
+                             const DicomMap& source)
+    {
+      // "The definition of a Data Set in PS3.5 specifically excludes
+      // the range of groups below group 0008, and this includes in
+      // particular Meta Information Header elements such as Transfer
+      // Syntax UID (0002,0010)."
+      // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
+      // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
+
+      // GroupLength are removed as well since they make no sense in the filtering as well as in the response.  
+      // Note that it seems that only some GE devices include them.
+
+      DicomArray a(source);
+
+      for (size_t i = 0; i < a.GetSize(); i++)
+      {
+        if (a.GetElement(i).GetTag().GetGroup() >= 0x0008 
+          && a.GetElement(i).GetTag().GetElement() != 0x0000)
+        {
+          target.SetValue(a.GetElement(i).GetTag(), a.GetElement(i).GetValue());
+        }
+      }
+    }
+
+
+
+    void FindScpCallback(
+      /* in */ 
+      void *callbackData,  
+      OFBool cancelled, 
+      T_DIMSE_C_FindRQ *request, 
+      DcmDataset *requestIdentifiers, 
+      int responseCount,
+      /* out */
+      T_DIMSE_C_FindRSP *response,
+      DcmDataset **responseIdentifiers,
+      DcmDataset **statusDetail)
+    {
+      assert(response != NULL);
+      assert(requestIdentifiers != NULL);
+      
+      memset(response, 0, sizeof(T_DIMSE_C_FindRSP));
+      *statusDetail = NULL;
+
+      std::string sopClassUid(request->AffectedSOPClassUID);
+
+      FindScpData& data = *reinterpret_cast(callbackData);
+      if (data.lastRequest_ == NULL)
+      {
+        {
+          std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+          requestIdentifiers->print(s);
+          CLOG(TRACE, DICOM) << "Received C-FIND Request:" << std::endl << s.str();
+        }
+      
+        bool ok = false;
+
+        try
+        {
+          RemoteModalityParameters modality;
+
+          /**
+           * Ensure that the remote modality is known to Orthanc for C-FIND requests.
+           **/
+
+          if (sopClassUid == UID_FINDModalityWorklistInformationModel)
+          {
+            data.answers_.SetWorklist(true);
+
+            if (data.worklistHandler_ != NULL)
+            {
+              ParsedDicomFile query(*requestIdentifiers);
+              FixWorklistQuery(query);
+
+              data.worklistHandler_->Handle(data.answers_, query,
+                                            *data.remoteIp_, *data.remoteAet_,
+                                            *data.calledAet_, modality.GetManufacturer());
+              ok = true;
+            }
+            else
+            {
+              CLOG(ERROR, DICOM) << "No worklist handler is installed, cannot handle this C-FIND request";
+            }
+          }
+          else
+          {
+            data.answers_.SetWorklist(false);
+
+            if (data.findHandler_ != NULL)
+            {
+              std::list sequencesToReturn;
+
+              for (unsigned long i = 0; i < requestIdentifiers->card(); i++)
+              {
+                DcmElement* element = requestIdentifiers->getElement(i);
+                if (element && !element->isLeaf())
+                {
+                  const DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
+
+                  DcmSequenceOfItems& sequence = dynamic_cast(*element);
+                  if (sequence.card() != 0)
+                  {
+                    CLOG(WARNING, DICOM) << "Orthanc only supports sequence matching on worklists, "
+                                         << "ignoring C-FIND SCU constraint on tag (" << tag.Format() 
+                                         << ") " << FromDcmtkBridge::GetTagName(*element);
+                  }
+
+                  sequencesToReturn.push_back(tag);
+                }
+              }
+
+              DicomMap input;
+              std::set ignoreTagLength;
+              FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers, 0 /* don't truncate tags */, ignoreTagLength);
+              input.RemoveSequences();
+
+              DicomMap filtered;
+              FixFindQuery(filtered, input);
+
+              data.findHandler_->Handle(data.answers_, filtered, sequencesToReturn,
+                                        *data.remoteIp_, *data.remoteAet_,
+                                        *data.calledAet_, modality.GetManufacturer());
+              ok = true;
+            }
+            else
+            {
+              CLOG(ERROR, DICOM) << "No C-Find handler is installed, cannot handle this request";
+            }
+          }
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          CLOG(ERROR, DICOM) <<  "C-FIND request handler has failed: " << e.What();
+        }
+
+        if (!ok)
+        {
+          response->DimseStatus = STATUS_FIND_Failed_UnableToProcess;
+          *responseIdentifiers = NULL;   
+          return;
+        }
+
+        data.lastRequest_ = requestIdentifiers;
+      }
+      else if (data.lastRequest_ != requestIdentifiers)
+      {
+        // Internal error!
+        response->DimseStatus = STATUS_FIND_Failed_UnableToProcess;
+        *responseIdentifiers = NULL;   
+        return;
+      }
+
+      if (responseCount <= static_cast(data.answers_.GetSize()))
+      {
+        // There are pending results that are still to be sent
+        response->DimseStatus = STATUS_Pending;
+        *responseIdentifiers = data.answers_.ExtractDcmDataset(responseCount - 1);
+
+        if (*responseIdentifiers)
+        {
+          std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+          (*responseIdentifiers)->print(s);
+          OFString str;
+          CLOG(TRACE, DICOM) << "Sending C-FIND Response "
+                             << responseCount << "/" << data.answers_.GetSize() << ":" << std::endl
+                             << s.str();
+        }
+      }
+      else if (data.answers_.IsComplete())
+      {
+        // Success: All the results have been sent
+        response->DimseStatus = STATUS_Success;
+        *responseIdentifiers = NULL;
+      }
+      else
+      {
+        // Success, but the results were too numerous and had to be cropped
+        CLOG(WARNING, DICOM) <<  "Too many results for an incoming C-FIND query";
+        response->DimseStatus = STATUS_FIND_Cancel_MatchingTerminatedDueToCancelRequest;
+        *responseIdentifiers = NULL;
+      }
+    }
+  }
+
+
+  OFCondition Internals::findScp(T_ASC_Association * assoc, 
+                                 T_DIMSE_Message * msg, 
+                                 T_ASC_PresentationContextID presID,
+                                 IFindRequestHandler* findHandler,
+                                 IWorklistRequestHandler* worklistHandler,
+                                 const std::string& remoteIp,
+                                 const std::string& remoteAet,
+                                 const std::string& calledAet,
+                                 int timeout)
+  {
+    FindScpData data;
+    data.findHandler_ = findHandler;
+    data.worklistHandler_ = worklistHandler;
+    data.lastRequest_ = NULL;
+    data.remoteIp_ = &remoteIp;
+    data.remoteAet_ = &remoteAet;
+    data.calledAet_ = &calledAet;
+
+    OFCondition cond = DIMSE_findProvider(assoc, presID, &msg->msg.CFindRQ, 
+                                          FindScpCallback, &data,
+                                          /*opt_blockMode*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                          /*opt_dimse_timeout*/ timeout);
+
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      CLOG(ERROR, DICOM) << "Find SCP Failed: " << cond.text();
+    }
+
+    return cond;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h
new file mode 100644
index 0000000..530030e
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h
@@ -0,0 +1,45 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../DicomServer.h"
+
+#include 
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition findScp(T_ASC_Association * assoc, 
+                        T_DIMSE_Message * msg, 
+                        T_ASC_PresentationContextID presID,
+                        IFindRequestHandler* findHandler,   // can be NULL
+                        IWorklistRequestHandler* worklistHandler,   // can be NULL
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        int timeout);
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp
new file mode 100644
index 0000000..4429e56
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp
@@ -0,0 +1,328 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+  Copyright (C) 1994-2011, OFFIS e.V.
+  All rights reserved.
+
+  This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions
+  are met:
+
+  - Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+  - Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+  - Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include 
+#include 
+#include "GetScp.h"
+
+#include 
+
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  namespace
+  {
+    struct GetScpData
+    {
+      //  Handle returns void.
+      IGetRequestHandler* handler_;
+      DcmDataset* lastRequest_;
+      T_ASC_Association * assoc_;
+
+      std::string remoteIp_;
+      std::string remoteAet_;
+      std::string calledAet_;
+      int timeout_;
+      bool canceled_;
+
+      GetScpData() :
+        handler_(NULL),
+        lastRequest_(NULL),
+        assoc_(NULL),
+        timeout_(0),
+        canceled_(false)
+      {
+      };
+    };
+      
+    static DcmDataset *BuildFailedInstanceList(const std::string& failedUIDs)
+    {
+      if (failedUIDs.empty())
+      {
+        return NULL;
+      }
+      else
+      {
+        std::unique_ptr rspIds(new DcmDataset());
+        
+        if (!DU_putStringDOElement(rspIds.get(), DCM_FailedSOPInstanceUIDList, failedUIDs.c_str()))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "getSCP: failed to build DCM_FailedSOPInstanceUIDList");
+        }
+
+        return rspIds.release();
+      }
+    }
+
+
+    static void FillResponse(T_DIMSE_C_GetRSP& response,
+                             DcmDataset** failedIdentifiers,
+                             const IGetRequestHandler& handler)
+    {
+      response.DimseStatus = STATUS_Success;
+
+      size_t processedCount = (handler.GetCompletedCount() +
+                               handler.GetFailedCount() +
+                               handler.GetWarningCount());
+
+      if (processedCount > handler.GetSubOperationCount())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      response.NumberOfRemainingSubOperations = (handler.GetSubOperationCount() - processedCount);
+      response.NumberOfCompletedSubOperations = handler.GetCompletedCount();
+      response.NumberOfFailedSubOperations = handler.GetFailedCount();
+      response.NumberOfWarningSubOperations = handler.GetWarningCount();
+
+      // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.3.3.html
+      
+      if (handler.GetFailedCount() > 0 ||
+          handler.GetWarningCount() > 0) 
+      {
+        /**
+         * "Warning if one or more sub-operations were successfully
+         * completed and one or more sub-operations were unsuccessful
+         * or had a status of warning. Warning if all sub-operations
+         * had a status of Warning"
+         **/
+        response.DimseStatus = STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures;
+      }
+
+      if (handler.GetFailedCount() > 0 &&
+          handler.GetFailedCount() == handler.GetSubOperationCount())
+      {
+        /**
+         * "Failure or Refused if all sub-operations were
+         * unsuccessful." => We choose to generate a "Refused - Out
+         * of Resources - Unable to perform suboperations" status.
+         */
+        response.DimseStatus = STATUS_GET_Refused_OutOfResourcesSubOperations;
+      }
+            
+      *failedIdentifiers = BuildFailedInstanceList(handler.GetFailedUids());
+    }
+    
+
+    static void GetScpCallback(
+      /* in */ 
+      void *callbackData,  
+      OFBool cancelled, 
+      T_DIMSE_C_GetRQ *request, 
+      DcmDataset *requestIdentifiers, 
+      int responseCount,
+      /* out */
+      T_DIMSE_C_GetRSP *response,
+      DcmDataset **responseIdentifiers,
+      DcmDataset **statusDetail)
+    {
+      assert(response != NULL);
+      assert(responseIdentifiers != NULL);
+      assert(requestIdentifiers != NULL);
+      
+      memset(response, 0, sizeof(T_DIMSE_C_GetRSP));
+      *statusDetail = NULL;
+      *responseIdentifiers = NULL;   
+
+      GetScpData& data = *reinterpret_cast(callbackData);
+      if (data.lastRequest_ == NULL)
+      {
+        {
+          std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+          requestIdentifiers->print(s);
+          CLOG(TRACE, DICOM) << "Received C-GET Request:" << std::endl << s.str();
+        }
+
+        DicomMap input;
+        std::set ignoreTagLength;
+        FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers, 0 /* don't truncate tags */, ignoreTagLength);
+
+        try
+        {
+          if (!data.handler_->Handle(
+                input, data.remoteIp_, data.remoteAet_, data.calledAet_,
+                data.timeout_ < 0 ? 0 : static_cast(data.timeout_)))
+          {
+            response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+            return;
+          }
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          CLOG(ERROR, DICOM) << "IGetRequestHandler Failed: " << e.What();
+          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+          return;
+        }
+
+        data.lastRequest_ = requestIdentifiers;
+      }
+      else if (data.lastRequest_ != requestIdentifiers)
+      {
+        // Internal error!
+        CLOG(ERROR, DICOM) << "IGetRequestHandler Failed: Internal error lastRequestIdentifier";
+        response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+        return;
+      }
+
+      if (data.canceled_)
+      {
+        CLOG(ERROR, DICOM) << "IGetRequestHandler Failed: Cannot pursue a request that was canceled by the SCU";
+        response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+        return;
+      }
+      
+      if (data.handler_->GetSubOperationCount() ==
+          data.handler_->GetCompletedCount() +
+          data.handler_->GetFailedCount() +
+          data.handler_->GetWarningCount())
+      {
+        // We're all done
+        FillResponse(*response, responseIdentifiers, *data.handler_);
+      }
+      else
+      {
+        bool isContinue;
+        
+        try
+        {
+          isContinue = data.handler_->DoNext(data.assoc_);
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          CLOG(ERROR, DICOM) << "IGetRequestHandler Failed: " << e.What();
+          FillResponse(*response, responseIdentifiers, *data.handler_);
+
+          // Fix the status code that is computed by "FillResponse()"
+          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+          return;
+        }
+
+        FillResponse(*response, responseIdentifiers, *data.handler_);
+
+        if (isContinue)
+        {
+          response->DimseStatus = STATUS_Pending;
+        }
+        else
+        {
+          response->DimseStatus = STATUS_GET_Cancel_SubOperationsTerminatedDueToCancelIndication;
+          data.canceled_ = true;
+        }
+      }
+    }
+  }
+
+  OFCondition Internals::getScp(T_ASC_Association * assoc,
+                                T_DIMSE_Message * msg, 
+                                T_ASC_PresentationContextID presID,
+                                IGetRequestHandler& handler,
+                                const std::string& remoteIp,
+                                const std::string& remoteAet,
+                                const std::string& calledAet,
+                                int timeout)
+  {
+    GetScpData data;
+    data.lastRequest_ = NULL;
+    data.handler_ = &handler;
+    data.assoc_ = assoc;
+    data.remoteIp_ = remoteIp;
+    data.remoteAet_ = remoteAet;
+    data.calledAet_ = calledAet;
+    data.timeout_ = timeout;
+
+    OFCondition cond = DIMSE_getProvider(assoc, presID, &msg->msg.CGetRQ, 
+                                         GetScpCallback, &data,
+                                         /*opt_blockMode*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                         /*opt_dimse_timeout*/ timeout);
+    
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      CLOG(ERROR, DICOM) << "Get SCP Failed: " << cond.text();
+    }
+
+    return cond;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h
new file mode 100644
index 0000000..39c8973
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h
@@ -0,0 +1,42 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../IGetRequestHandler.h"
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition getScp(T_ASC_Association * assoc,
+                       T_DIMSE_Message * msg, 
+                       T_ASC_PresentationContextID presID,
+                       IGetRequestHandler& handler,
+                       const std::string& remoteIp,
+                       const std::string& remoteAet,
+                       const std::string& calledAet,
+                       int timeout);
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp
new file mode 100644
index 0000000..2ada196
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp
@@ -0,0 +1,307 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "MoveScp.h"
+
+#include 
+
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include 
+
+
+/**
+ * Macro specifying whether to apply the patch suggested in issue 66:
+ * "Orthanc responses C-MOVE with zero Move Originator Message ID"
+ * https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=66
+ **/
+
+#define APPLY_FIX_ISSUE_66   1
+
+
+namespace Orthanc
+{
+  namespace
+  {  
+    struct MoveScpData
+    {
+      std::string target_;
+      IMoveRequestHandler* handler_;
+      DcmDataset* lastRequest_;
+      unsigned int subOperationCount_;
+      unsigned int failureCount_;
+      unsigned int warningCount_;
+      std::unique_ptr iterator_;
+      const std::string* remoteIp_;
+      const std::string* remoteAet_;
+      const std::string* calledAet_;
+    };
+
+
+#if APPLY_FIX_ISSUE_66 != 1
+    static uint16_t GetMessageId(const DicomMap& message)
+    {
+      /**
+       * Retrieve the Message ID (0000,0110) for this C-MOVE request, if
+       * any. If present, this Message ID will be stored in the Move
+       * Originator Message ID (0000,1031) field of the C-MOVE response.
+       * http://dicom.nema.org/dicom/2013/output/chtml/part07/chapter_E.html
+       **/
+
+      const DicomValue* value = message.TestAndGetValue(DICOM_TAG_MESSAGE_ID);
+
+      if (value != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        try
+        {
+          int tmp = boost::lexical_cast(value->GetContent());
+          if (tmp >= 0 && tmp <= 0xffff)
+          {
+            return static_cast(tmp);
+          }
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          CLOG(WARNING, DICOM) << "Cannot convert the Message ID (\"" << value->GetContent()
+                               << "\") of an incoming C-MOVE request to an integer, assuming zero";
+        }
+      }
+
+      return 0;
+    }
+#endif
+
+
+    void MoveScpCallback(
+      /* in */ 
+      void *callbackData,  
+      OFBool cancelled, 
+      T_DIMSE_C_MoveRQ *request, 
+      DcmDataset *requestIdentifiers, 
+      int responseCount,
+      /* out */
+      T_DIMSE_C_MoveRSP *response,
+      DcmDataset **responseIdentifiers,
+      DcmDataset **statusDetail)
+    {
+      assert(response != NULL);
+      assert(requestIdentifiers != NULL);
+      
+      memset(response, 0, sizeof(T_DIMSE_C_MoveRSP));
+      *statusDetail = NULL;
+      *responseIdentifiers = NULL;   
+
+      MoveScpData& data = *reinterpret_cast(callbackData);
+      if (data.lastRequest_ == NULL)
+      {
+        {
+          std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
+          requestIdentifiers->print(s);
+          CLOG(TRACE, DICOM) << "Received C-MOVE Request:" << std::endl << s.str();
+        }
+
+        DicomMap input;
+        std::set ignoreTagLength;
+        FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers, 0 /* don't truncate tags */, ignoreTagLength);
+
+        try
+        {
+#if APPLY_FIX_ISSUE_66 == 1
+          uint16_t messageId = request->MessageID;
+#else
+          // The line below was the implementation for Orthanc <= 1.3.2
+          uint16_t messageId = GetMessageId(input);
+#endif
+
+          data.iterator_.reset(data.handler_->Handle(data.target_, input, *data.remoteIp_, *data.remoteAet_,
+                                                     *data.calledAet_, messageId));
+
+          if (data.iterator_.get() == NULL)
+          {
+            // Internal error!
+            response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+            return;
+          }
+
+          data.subOperationCount_ = data.iterator_->GetSubOperationCount();
+          data.failureCount_ = 0;
+          data.warningCount_ = 0;
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          CLOG(ERROR, DICOM) << "IMoveRequestHandler Failed: " << e.What();
+          response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+          return;
+        }
+
+        data.lastRequest_ = requestIdentifiers;
+      }
+      else if (data.lastRequest_ != requestIdentifiers)
+      {
+        // Internal error!
+        response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+        return;
+      }
+  
+      if (data.subOperationCount_ == 0)
+      {
+        response->DimseStatus = STATUS_Success;
+      }
+      else
+      {
+        IMoveRequestIterator::Status status;
+
+        try
+        {
+          status = data.iterator_->DoNext();
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          CLOG(ERROR, DICOM) << "IMoveRequestHandler Failed: " << e.What();
+          response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+          return;
+        }
+
+        if (status == IMoveRequestIterator::Status_Failure)
+        {
+          data.failureCount_++;
+        }
+        else if (status == IMoveRequestIterator::Status_Warning)
+        {
+          data.warningCount_++;
+        }
+
+        if (responseCount < static_cast(data.subOperationCount_))
+        {
+          response->DimseStatus = STATUS_Pending;
+        }
+        else
+        {
+          response->DimseStatus = STATUS_Success;
+        }
+      }
+
+      response->NumberOfRemainingSubOperations = data.subOperationCount_ - responseCount;
+      response->NumberOfCompletedSubOperations = responseCount;
+      response->NumberOfFailedSubOperations = data.failureCount_;
+      response->NumberOfWarningSubOperations = data.warningCount_;
+
+      if (data.failureCount_ != 0)
+      {
+        // Warning "Sub-operations Complete - One or more Failures" (0xB000)
+        response->DimseStatus = STATUS_MOVE_Warning_SubOperationsCompleteOneOrMoreFailures;
+      }
+    }
+  }
+
+
+  OFCondition Internals::moveScp(T_ASC_Association * assoc, 
+                                 T_DIMSE_Message * msg, 
+                                 T_ASC_PresentationContextID presID,
+                                 IMoveRequestHandler& handler,
+                                 const std::string& remoteIp,
+                                 const std::string& remoteAet,
+                                 const std::string& calledAet,
+                                 int timeout)
+  {
+    MoveScpData data;
+    data.target_ = std::string(msg->msg.CMoveRQ.MoveDestination);
+    data.lastRequest_ = NULL;
+    data.handler_ = &handler;
+    data.remoteIp_ = &remoteIp;
+    data.remoteAet_ = &remoteAet;
+    data.calledAet_ = &calledAet;
+
+    OFCondition cond = DIMSE_moveProvider(assoc, presID, &msg->msg.CMoveRQ, 
+                                          MoveScpCallback, &data,
+                                          /*opt_blockMode*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                          /*opt_dimse_timeout*/ timeout);
+
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      CLOG(ERROR, DICOM) << "Move SCP Failed: " << cond.text();
+    }
+
+    return cond;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h
new file mode 100644
index 0000000..553eccb
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h
@@ -0,0 +1,44 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../IMoveRequestHandler.h"
+
+#include 
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition moveScp(T_ASC_Association * assoc, 
+                        T_DIMSE_Message * msg, 
+                        T_ASC_PresentationContextID presID,
+                        IMoveRequestHandler& handler,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet,
+                        int timeout);
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp
new file mode 100644
index 0000000..e0ae450
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp
@@ -0,0 +1,280 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+  Copyright (C) 1994-2011, OFFIS e.V.
+  All rights reserved.
+
+  This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions
+  are met:
+
+  - Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+  - Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+  - Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "StoreScp.h"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../OrthancException.h"
+#include "../../Logging.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  namespace
+  {  
+    struct StoreCallbackData
+    {
+      IStoreRequestHandler* handler;
+      const std::string* remoteIp;
+      const char* remoteAET;
+      const char* calledAET;
+      const char* modality;
+      const char* affectedSOPInstanceUID;
+      uint32_t messageID;
+    };
+
+    
+    static void
+    storeScpCallback(
+      void *callbackData,
+      T_DIMSE_StoreProgress *progress,
+      T_DIMSE_C_StoreRQ *req,
+      char * /*imageFileName*/, DcmDataset **imageDataSet,
+      T_DIMSE_C_StoreRSP *rsp,
+      DcmDataset **statusDetail)
+    /*
+     * This function.is used to indicate progress when storescp receives instance data over the
+     * network. On the final call to this function (identified by progress->state == DIMSE_StoreEnd)
+     * this function will store the data set which was received over the network to a file.
+     * Earlier calls to this function will simply cause some information to be dumped to stdout.
+     *
+     * Parameters:
+     *   callbackData  - [in] data for this callback function
+     *   progress      - [in] The state of progress. (identifies if this is the initial or final call
+     *                   to this function, or a call in between these two calls.
+     *   req           - [in] The original store request message.
+     *   imageFileName - [in] The path to and name of the file the information shall be written to.
+     *   imageDataSet  - [in] The data set which shall be stored in the image file
+     *   rsp           - [inout] the C-STORE-RSP message (will be sent after the call to this function)
+     *   statusDetail  - [inout] This variable can be used to capture detailed information with regard to
+     *                   the status information which is captured in the status element (0000,0900). Note
+     *                   that this function does specify any such information, the pointer will be set to NULL.
+     */
+    {
+      StoreCallbackData *cbdata = OFstatic_cast(StoreCallbackData *, callbackData);
+
+      DIC_UI sopClass;
+      DIC_UI sopInstance;
+
+      // if this is the final call of this function, save the data which was received to a file
+      // (note that we could also save the image somewhere else, put it in database, etc.)
+      if (progress->state == DIMSE_StoreEnd)
+      {
+        OFString tmpStr;
+
+        // do not send status detail information
+        *statusDetail = NULL;
+
+        // Concerning the following line: an appropriate status code is already set in the resp structure,
+        // it need not be success. For example, if the caller has already detected an out of resources problem
+        // then the status will reflect this.  The callback function is still called to allow cleanup.
+        //rsp->DimseStatus = STATUS_Success;
+
+        // we want to write the received information to a file only if this information
+        // is present and the options opt_bitPreserving and opt_ignore are not set.
+        if ((imageDataSet != NULL) && (*imageDataSet != NULL))
+        {
+          // check the image to make sure it is consistent, i.e. that its sopClass and sopInstance correspond
+          // to those mentioned in the request. If not, set the status in the response message variable.
+          if (rsp->DimseStatus == STATUS_Success)
+          {
+            // which SOP class and SOP instance ?
+	    
+#if DCMTK_VERSION_NUMBER >= 364
+	            if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sizeof(sopClass),
+						      sopInstance, sizeof(sopInstance), /*opt_correctUIDPadding*/ OFFalse))
+#else
+              if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse))
+#endif
+              {
+		            //LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName);
+		            rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand;
+              }
+              else if (strcmp(sopClass, req->AffectedSOPClassUID) != 0)
+              {
+                rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
+              }
+              else if (strcmp(sopInstance, req->AffectedSOPInstanceUID) != 0)
+              {
+                rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
+              }
+              else
+              {
+                try
+                {
+                  rsp->DimseStatus = cbdata->handler->Handle(**imageDataSet, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET);
+                }
+                catch (OrthancException& e)
+                {
+                  rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
+
+                  if (e.GetErrorCode() == ErrorCode_InexistentTag)
+                  {
+                    LOG(ERROR) << FromDcmtkBridge::FormatMissingTagsForStore(**imageDataSet);
+                  }
+                  else
+                  {
+                    CLOG(ERROR, DICOM) << "Exception while storing DICOM: " << e.What();
+                  }
+                }
+              }
+          }
+        }
+      }
+    }
+  }
+
+/*
+ * This function processes a DIMSE C-STORE-RQ commmand that was
+ * received over the network connection.
+ *
+ * Parameters:
+ *   assoc  - [in] The association (network connection to another DICOM application).
+ *   msg    - [in] The DIMSE C-STORE-RQ message that was received.
+ *   presID - [in] The ID of the presentation context which was specified in the PDV which contained
+ *                 the DIMSE command.
+ */
+  OFCondition Internals::storeScp(T_ASC_Association * assoc, 
+                                  T_DIMSE_Message * msg, 
+                                  T_ASC_PresentationContextID presID,
+                                  IStoreRequestHandler& handler,
+                                  const std::string& remoteIp,
+                                  int timeout)
+  {
+    OFCondition cond = EC_Normal;
+    T_DIMSE_C_StoreRQ *req;
+
+    // assign the actual information of the C-STORE-RQ command to a local variable
+    req = &msg->msg.CStoreRQ;
+
+    // intialize some variables
+    StoreCallbackData data;
+    data.handler = &handler;
+    data.remoteIp = &remoteIp;
+    data.modality = dcmSOPClassUIDToModality(req->AffectedSOPClassUID/*, "UNKNOWN"*/);
+    if (data.modality == NULL)
+      data.modality = "UNKNOWN";
+
+    data.affectedSOPInstanceUID = req->AffectedSOPInstanceUID;
+    data.messageID = req->MessageID;
+    if (assoc && assoc->params)
+    {
+      data.remoteAET = assoc->params->DULparams.callingAPTitle;
+      data.calledAET = assoc->params->DULparams.calledAPTitle;
+    }
+    else
+    {
+      data.remoteAET = "";
+      data.calledAET = "";
+    }
+
+    DcmFileFormat dcmff;
+
+    // store SourceApplicationEntityTitle in metaheader
+    if (assoc && assoc->params)
+    {
+      const char *aet = assoc->params->DULparams.callingAPTitle;
+      if (aet) dcmff.getMetaInfo()->putAndInsertString(DCM_SourceApplicationEntityTitle, aet);
+    }
+
+    // define an address where the information which will be received over the network will be stored
+    DcmDataset *dset = dcmff.getDataset();
+
+    cond = DIMSE_storeProvider(assoc, presID, req, NULL, /*opt_useMetaheader*/OFFalse, &dset,
+                               storeScpCallback, &data, 
+                               /*opt_blockMode*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                               /*opt_dimse_timeout*/ timeout);
+
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      CLOG(ERROR, DICOM) << "Store SCP Failed: " << cond.text();
+    }
+
+    // return return value
+    return cond;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h
new file mode 100644
index 0000000..ebdc0ea
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h
@@ -0,0 +1,42 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../IStoreRequestHandler.h"
+
+#include 
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition storeScp(T_ASC_Association * assoc, 
+                         T_DIMSE_Message * msg, 
+                         T_ASC_PresentationContextID presID,
+                         IStoreRequestHandler& handler,
+                         const std::string& remoteIp,
+                         int timeout);
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h b/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h
new file mode 100644
index 0000000..99f5ce0
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h
@@ -0,0 +1,48 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+
+#ifdef _WIN32
+/**
+ * "The maximum length, in bytes, of the string returned in the buffer 
+ * pointed to by the name parameter is dependent on the namespace provider,
+ * but this string must be 256 bytes or less.
+ * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
+ **/
+#  define HOST_NAME_MAX 256
+#endif 
+
+
+#if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
+/**
+ * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
+ * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
+ * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
+ * that the result will fit."
+ * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
+ **/
+#  define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
+#endif
diff --git a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp
new file mode 100644
index 0000000..975dfb0
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp
@@ -0,0 +1,548 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "RemoteModalityParameters.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+
+#include 
+#include 
+
+
+static const char* KEY_AET = "AET";
+static const char* KEY_ALLOW_ECHO = "AllowEcho";
+static const char* KEY_ALLOW_FIND = "AllowFind";
+static const char* KEY_ALLOW_FIND_WORKLIST = "AllowFindWorklist";
+static const char* KEY_ALLOW_GET = "AllowGet";
+static const char* KEY_ALLOW_MOVE = "AllowMove";
+static const char* KEY_ALLOW_N_ACTION = "AllowNAction";
+static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport";
+static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment";
+static const char* KEY_ALLOW_STORE = "AllowStore";
+static const char* KEY_ALLOW_TRANSCODING = "AllowTranscoding";
+static const char* KEY_HOST = "Host";
+static const char* KEY_MANUFACTURER = "Manufacturer";
+static const char* KEY_PORT = "Port";
+static const char* KEY_USE_DICOM_TLS = "UseDicomTls";
+static const char* KEY_LOCAL_AET = "LocalAet";
+static const char* KEY_TIMEOUT = "Timeout";
+static const char* KEY_RETRIEVE_METHOD = "RetrieveMethod";
+
+
+namespace Orthanc
+{
+  void RemoteModalityParameters::Clear()
+  {
+    aet_ = "ORTHANC";
+    host_ = "127.0.0.1";
+    port_ = 104;
+    manufacturer_ = ModalityManufacturer_Generic;
+    allowEcho_ = true;
+    allowStore_ = true;
+    allowFind_ = true;
+    allowFindWorklist_ = true;
+    allowMove_ = true;
+    allowGet_ = true;
+    allowNAction_ = true;  // For storage commitment
+    allowNEventReport_ = true;  // For storage commitment
+    allowTranscoding_ = true;
+    useDicomTls_ = false;
+    localAet_.clear();
+    timeout_ = 0;
+    retrieveMethod_ = RetrieveMethod_SystemDefault;
+  }
+
+
+  RemoteModalityParameters::RemoteModalityParameters()
+  {
+    Clear();
+  }
+
+  RemoteModalityParameters::RemoteModalityParameters(const Json::Value &serialized)
+  {
+    Unserialize(serialized);
+  }
+
+  RemoteModalityParameters::RemoteModalityParameters(const std::string& aet,
+                                                     const std::string& host,
+                                                     uint16_t port,
+                                                     ModalityManufacturer manufacturer)
+  {
+    Clear();
+    SetApplicationEntityTitle(aet);
+    SetHost(host);
+    SetPortNumber(port);
+    SetManufacturer(manufacturer);
+  }
+
+  const std::string &RemoteModalityParameters::GetApplicationEntityTitle() const
+  {
+    return aet_;
+  }
+
+  void RemoteModalityParameters::SetApplicationEntityTitle(const std::string &aet)
+  {
+    aet_ = aet;
+  }
+
+  const std::string &RemoteModalityParameters::GetHost() const
+  {
+    return host_;
+  }
+
+  void RemoteModalityParameters::SetHost(const std::string &host)
+  {
+    host_ = host;
+  }
+
+  uint16_t RemoteModalityParameters::GetPortNumber() const
+  {
+    return port_;
+  }
+
+
+  static void CheckPortNumber(int value)
+  {
+    if (value <= 0 ||
+        value >= 65535)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "A TCP port number must be in range [1..65534], but found: " +
+                             boost::lexical_cast(value));
+    }
+  }
+
+
+  static uint16_t ReadPortNumber(const Json::Value& value)
+  {
+    int tmp;
+
+    switch (value.type())
+    {
+      case Json::intValue:
+      case Json::uintValue:
+        tmp = value.asInt();
+        break;
+
+      case Json::stringValue:
+        try
+        {
+          tmp = boost::lexical_cast(value.asString());
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    CheckPortNumber(tmp);
+    return static_cast(tmp);
+  }
+
+
+  void RemoteModalityParameters::SetPortNumber(uint16_t port)
+  {
+    CheckPortNumber(port);
+    port_ = port;
+  }
+
+  ModalityManufacturer RemoteModalityParameters::GetManufacturer() const
+  {
+    return manufacturer_;
+  }
+
+  void RemoteModalityParameters::SetManufacturer(ModalityManufacturer manufacturer)
+  {
+    manufacturer_ = manufacturer;
+  }
+
+  void RemoteModalityParameters::SetManufacturer(const std::string &manufacturer)
+  {
+    manufacturer_ = StringToModalityManufacturer(manufacturer);
+  }
+
+  void RemoteModalityParameters::UnserializeArray(const Json::Value& serialized)
+  {
+    assert(serialized.type() == Json::arrayValue);
+
+    if ((serialized.size() != 3 &&
+         serialized.size() != 4) ||
+        serialized[0].type() != Json::stringValue ||
+        serialized[1].type() != Json::stringValue ||
+        (serialized.size() == 4 &&
+         serialized[3].type() != Json::stringValue))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    aet_ = serialized[0].asString();
+    host_ = serialized[1].asString();
+    port_ = ReadPortNumber(serialized[2]);
+
+    if (serialized.size() == 4)
+    {
+      manufacturer_ = StringToModalityManufacturer(serialized[3].asString());
+    }
+    else
+    {
+      manufacturer_ = ModalityManufacturer_Generic;
+    }
+  }
+
+  
+  void RemoteModalityParameters::UnserializeObject(const Json::Value& serialized)
+  {
+    assert(serialized.type() == Json::objectValue);
+
+    aet_ = SerializationToolbox::ReadString(serialized, KEY_AET);
+    host_ = SerializationToolbox::ReadString(serialized, KEY_HOST);
+
+    if (serialized.isMember(KEY_PORT))
+    {
+      port_ = ReadPortNumber(serialized[KEY_PORT]);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    if (serialized.isMember(KEY_MANUFACTURER))
+    {
+      manufacturer_ = StringToModalityManufacturer
+        (SerializationToolbox::ReadString(serialized, KEY_MANUFACTURER));
+    }   
+    else
+    {
+      manufacturer_ = ModalityManufacturer_Generic;
+    }
+
+    if (serialized.isMember(KEY_ALLOW_ECHO))
+    {
+      allowEcho_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_ECHO);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_FIND))
+    {
+      allowFind_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_FIND);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_FIND_WORKLIST))
+    {
+      allowFindWorklist_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_FIND_WORKLIST);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_STORE))
+    {
+      allowStore_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORE);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_GET))
+    {
+      allowGet_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_GET);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_MOVE))
+    {
+      allowMove_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_MOVE);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_N_ACTION))
+    {
+      allowNAction_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_ACTION);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_N_EVENT_REPORT))
+    {
+      allowNEventReport_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_EVENT_REPORT);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_STORAGE_COMMITMENT))
+    {
+      bool allow = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORAGE_COMMITMENT);
+      allowNAction_ = allow;
+      allowNEventReport_ = allow;
+    }
+
+    if (serialized.isMember(KEY_ALLOW_TRANSCODING))
+    {
+      allowTranscoding_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_TRANSCODING);
+    }
+
+    if (serialized.isMember(KEY_USE_DICOM_TLS))
+    {
+      useDicomTls_ = SerializationToolbox::ReadBoolean(serialized, KEY_USE_DICOM_TLS);
+    }
+
+    if (serialized.isMember(KEY_LOCAL_AET))
+    {
+      localAet_ = SerializationToolbox::ReadString(serialized, KEY_LOCAL_AET);
+    }
+
+    if (serialized.isMember(KEY_TIMEOUT))
+    {
+      timeout_ = SerializationToolbox::ReadUnsignedInteger(serialized, KEY_TIMEOUT);
+    }
+
+    if (serialized.isMember(KEY_RETRIEVE_METHOD))
+    {
+      retrieveMethod_ = StringToRetrieveMethod
+        (SerializationToolbox::ReadString(serialized, KEY_RETRIEVE_METHOD));
+    }   
+    else
+    {
+      retrieveMethod_ = RetrieveMethod_SystemDefault;
+    }
+
+  }
+
+
+  bool RemoteModalityParameters::IsRequestAllowed(DicomRequestType type) const
+  {
+    switch (type)
+    {
+    case DicomRequestType_Echo:
+        return allowEcho_;
+
+      case DicomRequestType_Find:
+        return allowFind_;
+
+      case DicomRequestType_FindWorklist:
+        return allowFindWorklist_;
+
+      case DicomRequestType_Get:
+        return allowGet_;
+
+      case DicomRequestType_Move:
+        return allowMove_;
+
+      case DicomRequestType_Store:
+        return allowStore_;
+
+      case DicomRequestType_NAction:
+        return allowNAction_;
+
+      case DicomRequestType_NEventReport:
+        return allowNEventReport_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void RemoteModalityParameters::SetRequestAllowed(DicomRequestType type,
+                                                   bool allowed)
+  {
+    switch (type)
+    {
+      case DicomRequestType_Echo:
+        allowEcho_ = allowed;
+        break;
+
+      case DicomRequestType_Find:
+        allowFind_ = allowed;
+        break;
+
+      case DicomRequestType_FindWorklist:
+        allowFindWorklist_ = allowed;
+        break;
+
+      case DicomRequestType_Get:
+        allowGet_ = allowed;
+        break;
+
+      case DicomRequestType_Move:
+        allowMove_ = allowed;
+        break;
+
+      case DicomRequestType_Store:
+        allowStore_ = allowed;
+        break;
+
+      case DicomRequestType_NAction:
+        allowNAction_ = allowed;
+        break;
+
+      case DicomRequestType_NEventReport:
+        allowNEventReport_ = allowed;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool RemoteModalityParameters::IsAdvancedFormatNeeded() const
+  {
+    return (!allowEcho_ ||
+            !allowStore_ ||
+            !allowFind_ ||
+            !allowFindWorklist_ ||
+            !allowGet_ ||
+            !allowMove_ ||
+            !allowNAction_ ||
+            !allowNEventReport_ ||
+            !allowTranscoding_ ||
+            useDicomTls_ ||
+            HasLocalAet());
+  }
+
+  
+  void RemoteModalityParameters::Serialize(Json::Value& target,
+                                           bool forceAdvancedFormat) const
+  {
+    if (forceAdvancedFormat ||
+        IsAdvancedFormatNeeded())
+    {
+      target = Json::objectValue;
+      target[KEY_AET] = aet_;
+      target[KEY_HOST] = host_;
+      target[KEY_PORT] = port_;
+      target[KEY_MANUFACTURER] = EnumerationToString(manufacturer_);
+      target[KEY_ALLOW_ECHO] = allowEcho_;
+      target[KEY_ALLOW_STORE] = allowStore_;
+      target[KEY_ALLOW_FIND] = allowFind_;
+      target[KEY_ALLOW_FIND_WORKLIST] = allowFindWorklist_;
+      target[KEY_ALLOW_GET] = allowGet_;
+      target[KEY_ALLOW_MOVE] = allowMove_;
+      target[KEY_ALLOW_N_ACTION] = allowNAction_;
+      target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_;
+      target[KEY_ALLOW_TRANSCODING] = allowTranscoding_;
+      target[KEY_USE_DICOM_TLS] = useDicomTls_;
+      target[KEY_LOCAL_AET] = localAet_;
+      target[KEY_TIMEOUT] = timeout_;
+      target[KEY_RETRIEVE_METHOD] = EnumerationToString(retrieveMethod_);
+    }
+    else
+    {
+      target = Json::arrayValue;
+      target.append(GetApplicationEntityTitle());
+      target.append(GetHost());
+      target.append(GetPortNumber());
+      target.append(EnumerationToString(GetManufacturer()));
+    }
+  }
+
+  
+  void RemoteModalityParameters::Unserialize(const Json::Value& serialized)
+  {
+    Clear();
+
+    switch (serialized.type())
+    {
+      case Json::objectValue:
+        UnserializeObject(serialized);
+        break;
+
+      case Json::arrayValue:
+        UnserializeArray(serialized);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+  bool RemoteModalityParameters::IsTranscodingAllowed() const
+  {
+    return allowTranscoding_;
+  }
+
+  void RemoteModalityParameters::SetTranscodingAllowed(bool allowed)
+  {
+    allowTranscoding_ = allowed;
+  }
+
+  bool RemoteModalityParameters::IsDicomTlsEnabled() const
+  {
+    return useDicomTls_;
+  }
+
+  void RemoteModalityParameters::SetDicomTlsEnabled(bool enabled)
+  {
+    useDicomTls_ = enabled;
+  }
+
+  bool RemoteModalityParameters::HasLocalAet() const
+  {
+    return !localAet_.empty();
+  }
+
+  const std::string& RemoteModalityParameters::GetLocalAet() const
+  {
+    if (localAet_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "You should have called HasLocalAet()");
+    }
+    else
+    {
+      return localAet_;
+    }
+  }
+
+  void RemoteModalityParameters::SetLocalAet(const std::string& aet)
+  {
+    if (aet.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      localAet_ = aet;
+    }
+  }
+
+  void RemoteModalityParameters::SetTimeout(uint32_t seconds)
+  {
+    timeout_ = seconds;
+  }
+
+  uint32_t RemoteModalityParameters::GetTimeout() const
+  {
+    return timeout_;
+  }
+
+  bool RemoteModalityParameters::HasTimeout() const
+  {
+    return timeout_ != 0;
+  }
+
+  RetrieveMethod RemoteModalityParameters::GetRetrieveMethod() const
+  {
+    return retrieveMethod_;
+  }
+
+  void RemoteModalityParameters::SetRetrieveMethod(RetrieveMethod retrieveMethod)
+  {
+    retrieveMethod_ = retrieveMethod;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h
new file mode 100644
index 0000000..07620be
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h
@@ -0,0 +1,128 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Enumerations.h"
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC RemoteModalityParameters
+  {
+  private:
+    std::string           aet_;
+    std::string           host_;
+    uint16_t              port_;
+    ModalityManufacturer  manufacturer_;
+    bool                  allowEcho_;
+    bool                  allowStore_;
+    bool                  allowFind_;
+    bool                  allowFindWorklist_;
+    bool                  allowMove_;
+    bool                  allowGet_;
+    bool                  allowNAction_;
+    bool                  allowNEventReport_;
+    bool                  allowTranscoding_;
+    bool                  useDicomTls_;
+    std::string           localAet_;
+    uint32_t              timeout_;
+    RetrieveMethod        retrieveMethod_;   // New in Orthanc 1.12.6
+    
+    void Clear();
+
+    void UnserializeArray(const Json::Value& serialized);
+
+    void UnserializeObject(const Json::Value& serialized);
+
+  public:
+    RemoteModalityParameters();
+
+    explicit RemoteModalityParameters(const Json::Value& serialized);
+
+    RemoteModalityParameters(const std::string& aet,
+                             const std::string& host,
+                             uint16_t port,
+                             ModalityManufacturer manufacturer);
+
+    const std::string& GetApplicationEntityTitle() const;
+
+    void SetApplicationEntityTitle(const std::string& aet);
+
+    const std::string& GetHost() const;
+
+    void SetHost(const std::string& host);
+    
+    uint16_t GetPortNumber() const;
+
+    void SetPortNumber(uint16_t port);
+
+    ModalityManufacturer GetManufacturer() const;
+
+    void SetManufacturer(ModalityManufacturer manufacturer);
+
+    void SetManufacturer(const std::string& manufacturer);
+
+    bool IsRequestAllowed(DicomRequestType type) const;
+
+    void SetRequestAllowed(DicomRequestType type,
+                           bool allowed);
+
+    void Unserialize(const Json::Value& modality);
+
+    bool IsAdvancedFormatNeeded() const;
+
+    void Serialize(Json::Value& target,
+                   bool forceAdvancedFormat) const;
+
+    bool IsTranscodingAllowed() const;
+
+    void SetTranscodingAllowed(bool allowed);
+
+    bool IsDicomTlsEnabled() const;
+
+    void SetDicomTlsEnabled(bool enabled);
+
+    bool HasLocalAet() const;
+
+    const std::string& GetLocalAet() const;
+
+    void SetLocalAet(const std::string& aet);
+
+    // Setting it to "0" will use "DicomAssociationParameters::GetDefaultTimeout()"
+    void SetTimeout(uint32_t seconds);
+
+    uint32_t GetTimeout() const;
+
+    bool HasTimeout() const;    
+
+    RetrieveMethod GetRetrieveMethod() const;
+
+    void SetRetrieveMethod(RetrieveMethod retrieveMethod);
+
+  };
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp
new file mode 100644
index 0000000..4596680
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp
@@ -0,0 +1,129 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "TimeoutDicomConnectionManager.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  static boost::posix_time::ptime GetNow()
+  {
+    return boost::posix_time::microsec_clock::universal_time();
+  }
+
+
+  TimeoutDicomConnectionManager::Lock::Lock(TimeoutDicomConnectionManager& that,
+                                            const std::string& localAet,
+                                            const RemoteModalityParameters& remote) : 
+    that_(that),
+    lock_(that_.mutex_)
+  {
+    // Calling "Touch()" will be done by the "~Lock()" destructor
+    that_.OpenInternal(localAet, remote);
+  }
+
+  
+  TimeoutDicomConnectionManager::Lock::~Lock()
+  {
+    that_.TouchInternal();
+  }
+
+  
+  DicomStoreUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection()
+  {
+    if (that_.connection_.get() == NULL)
+    {
+      // The allocation should have been done by "that_.Open()" in the constructor
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      return *that_.connection_;
+    }
+  }
+
+
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::TouchInternal()
+  {
+    lastUse_ = GetNow();
+  }
+
+
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::OpenInternal(const std::string& localAet,
+                                                   const RemoteModalityParameters& remote)
+  {
+    DicomAssociationParameters other(localAet, remote);
+    
+    if (connection_.get() == NULL ||
+        !connection_->GetParameters().IsEqual(other))
+    {
+      connection_.reset(new DicomStoreUserConnection(other));
+    }
+  }
+
+
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::CloseInternal()
+  {
+    if (connection_.get() != NULL)
+    {
+      CLOG(INFO, DICOM) << "Closing inactive DICOM association with modality: "
+                        << connection_->GetParameters().GetRemoteModality().GetApplicationEntityTitle();
+
+      connection_.reset(NULL);
+    }
+  }
+
+
+  void TimeoutDicomConnectionManager::SetInactivityTimeout(unsigned int milliseconds)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    timeout_ = boost::posix_time::milliseconds(milliseconds);
+    CloseInternal();
+  }
+
+
+  unsigned int TimeoutDicomConnectionManager::GetInactivityTimeout()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return static_cast(timeout_.total_milliseconds());
+  }
+
+
+  void TimeoutDicomConnectionManager::CloseIfInactive()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (connection_.get() != NULL &&
+        (GetNow() - lastUse_) >= timeout_)
+    {
+      CloseInternal();
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h
new file mode 100644
index 0000000..be863e9
--- /dev/null
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h
@@ -0,0 +1,95 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING)
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be 1 to use this file
+#endif
+
+
+#include "../Compatibility.h"
+#include "DicomStoreUserConnection.h"
+
+#include 
+#include 
+
+namespace Orthanc
+{
+  /**
+   * This class corresponds to a singleton to a DICOM SCU connection.
+   **/
+  class TimeoutDicomConnectionManager : public boost::noncopyable
+  {
+  private:
+    boost::mutex                               mutex_;
+    std::unique_ptr  connection_;
+    boost::posix_time::ptime                   lastUse_;
+    boost::posix_time::time_duration           timeout_;
+
+    // Mutex must be locked
+    void TouchInternal();
+
+    // Mutex must be locked
+    void OpenInternal(const std::string& localAet,
+                      const RemoteModalityParameters& remote);
+
+    // Mutex must be locked
+    void CloseInternal();
+
+  public:
+    class Lock : public boost::noncopyable
+    {
+    private:
+      TimeoutDicomConnectionManager&  that_;
+      boost::mutex::scoped_lock       lock_;
+
+    public:
+      Lock(TimeoutDicomConnectionManager& that,
+           const std::string& localAet,
+           const RemoteModalityParameters& remote);
+      
+      ~Lock();
+
+      DicomStoreUserConnection& GetConnection();
+    };
+
+    TimeoutDicomConnectionManager() :
+      timeout_(boost::posix_time::milliseconds(1000))
+    {
+    }
+
+    void SetInactivityTimeout(unsigned int milliseconds);
+
+    unsigned int GetInactivityTimeout();  // In milliseconds
+
+    void Close();
+
+    void CloseIfInactive();
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp
new file mode 100644
index 0000000..765f0a0
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp
@@ -0,0 +1,408 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DcmtkTranscoder.h"
+
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG)
+#  error Macro ORTHANC_ENABLE_DCMTK_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS)
+#  error Macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined
+#endif
+
+
+#include "FromDcmtkBridge.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include 
+#include   // for DJ_RPLossy
+#include    // for DJ_RPLossless
+#include   // for DJLSRepresentationParameter
+
+#include 
+
+
+namespace Orthanc
+{
+  DcmtkTranscoder::DcmtkTranscoder(unsigned int maxConcurrentExecutions) :
+  defaultLossyQuality_(90),
+    maxConcurrentExecutionsSemaphore_(maxConcurrentExecutions)
+  {
+  }
+
+
+  static bool GetBitsStored(uint16_t& bitsStored,
+                            DcmDataset& dataset)
+  {
+    return dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good();
+  }
+
+  
+  void DcmtkTranscoder::SetDefaultLossyQuality(unsigned int quality)
+  {
+    if (quality == 0 ||
+        quality > 100)
+    {
+      throw OrthancException(
+        ErrorCode_ParameterOutOfRange,
+        "The default quality for lossy transcoding must be an integer between 1 and 100, received: " +
+        boost::lexical_cast(quality));
+    }
+    else
+    {
+      LOG(INFO) << "Default quality for lossy transcoding using DCMTK is set to: " << quality;
+      defaultLossyQuality_ = quality;
+    }
+  }
+
+  unsigned int DcmtkTranscoder::GetDefaultLossyQuality() const
+  {
+    return defaultLossyQuality_;
+  }
+
+  bool TryTranscode(std::vector& failureReasons, /* out */
+                    DicomTransferSyntax& selectedSyntax, /* out*/
+                    DcmFileFormat& dicom, /* in/out */
+                    const std::set& allowedSyntaxes,
+                    DicomTransferSyntax trySyntax)
+  {
+    if (allowedSyntaxes.find(trySyntax) != allowedSyntaxes.end())
+    {
+      if (FromDcmtkBridge::Transcode(dicom, trySyntax, NULL))
+      {
+        selectedSyntax = trySyntax;
+        return true;
+      }
+
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(trySyntax));
+    }
+    return false;
+  }
+
+  bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
+                                         std::string& failureReason /* out */,
+                                         DcmFileFormat& dicom, /* in/out */
+                                         const std::set& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid,
+                                         unsigned int lossyQuality) 
+  {
+    std::vector failureReasons;
+
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    DicomTransferSyntax syntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, dicom))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot determine the transfer syntax");
+    }
+
+    uint16_t bitsStored;
+    bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset());
+    
+    if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end())
+    {
+      // No transcoding is needed
+      return true;
+    }
+    
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_LittleEndianImplicit))
+    {
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_LittleEndianExplicit))
+    {
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_BigEndianExplicit))
+    {
+      return true;
+    }
+
+    if (TryTranscode(failureReasons, selectedSyntax, dicom, allowedSyntaxes, DicomTransferSyntax_DeflatedLittleEndianExplicit))
+    {
+      return true;
+    }
+
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end())
+    {
+      if (!allowNewSopInstanceUid)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1) + " without generating new SOPInstanceUID");
+      }
+      else if (hasBitsStored && bitsStored != 8)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1) + " if BitsStored != 8");
+      }
+      else
+      {
+        // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+        DJ_RPLossy parameters(lossyQuality);
+          
+        if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, ¶meters))
+        {
+          selectedSyntax = DicomTransferSyntax_JPEGProcess1;
+          return true;
+        }
+        failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess1));
+      }
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end())
+    {
+      if (!allowNewSopInstanceUid)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4) + " without generating new SOPInstanceUID");
+      }
+      else if (hasBitsStored && bitsStored > 12)
+      {
+        failureReasons.push_back(std::string("Can not transcode to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4) + " if BitsStored != 8");
+      }
+      else
+      {
+        // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+        DJ_RPLossy parameters(lossyQuality);
+        if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, ¶meters))
+        {
+          selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
+          return true;
+        }
+        failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess2_4));
+      }
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+      DJ_RPLossless parameters(6 /* opt_selection_value */,
+                               0 /* opt_point_transform */);
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, ¶meters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGProcess14;
+        return true;
+      }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess14));
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14SV1) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+      DJ_RPLossless parameters(6 /* opt_selection_value */,
+                               0 /* opt_point_transform */);
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14SV1, ¶meters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1;
+        return true;
+      }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGProcess14SV1));
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossless) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpls/apps/dcmcjpls.cc"
+      DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */,
+                                             OFTrue /* opt_useLosslessProcess */);
+
+      /**
+       * WARNING: This call results in a segmentation fault if using
+       * the DCMTK package 3.6.2 from Ubuntu 18.04.
+       **/              
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossless, ¶meters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGLSLossless;
+        return true;
+      }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGLSLossless));
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    if (allowNewSopInstanceUid &&
+        allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossy) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpls/apps/dcmcjpls.cc"
+      DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */,
+                                             OFFalse /* opt_useLosslessProcess */);
+
+      /**
+       * WARNING: This call results in a segmentation fault if using
+       * the DCMTK package 3.6.2 from Ubuntu 18.04.
+       **/              
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, ¶meters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGLSLossy;
+        return true;
+      }
+      failureReasons.push_back(std::string("Internal error while transcoding to ") + GetTransferSyntaxUid(DicomTransferSyntax_JPEGLSLossy));
+    }
+#endif
+
+    Orthanc::Toolbox::JoinStrings(failureReason, failureReasons, ", ");
+    return false;
+  }
+
+  bool DcmtkTranscoder::IsSupported(DicomTransferSyntax syntax)
+  {
+    if (syntax == DicomTransferSyntax_LittleEndianImplicit ||
+        syntax == DicomTransferSyntax_LittleEndianExplicit ||
+        syntax == DicomTransferSyntax_BigEndianExplicit ||
+        syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit)
+    {
+      return true;
+    }
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (syntax == DicomTransferSyntax_JPEGProcess1 ||
+        syntax == DicomTransferSyntax_JPEGProcess2_4 ||
+        syntax == DicomTransferSyntax_JPEGProcess14 ||
+        syntax == DicomTransferSyntax_JPEGProcess14SV1)
+    {
+      return true;
+    }
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    if (syntax == DicomTransferSyntax_JPEGLSLossless ||
+        syntax == DicomTransferSyntax_JPEGLSLossy)
+    {
+      return true;
+    }
+#endif
+    
+    return false;
+  }
+
+  bool DcmtkTranscoder::Transcode(DicomImage& target,
+                                  DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                  const std::set& allowedSyntaxes,
+                                  bool allowNewSopInstanceUid)
+  {
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, defaultLossyQuality_);
+  }
+
+
+  bool DcmtkTranscoder::Transcode(DicomImage& target,
+                                  DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                  const std::set& allowedSyntaxes,
+                                  bool allowNewSopInstanceUid,
+                                  unsigned int lossyQuality)
+  {
+    Semaphore::Locker lock(maxConcurrentExecutionsSemaphore_); // limit the number of concurrent executions
+
+    target.Clear();
+    
+    DicomTransferSyntax sourceSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
+    {
+      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
+      return false;
+    }
+
+    std::string failureReason;
+    std::string s;
+    for (std::set::const_iterator
+            it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
+    {
+      if (!s.empty())
+      {
+        s += ", ";
+      }
+
+      s += GetTransferSyntaxUid(*it);
+    }
+
+    if (s.empty())
+    {
+      s = "";
+    }
+
+    LOG(INFO) << "DCMTK transcoding from " << GetTransferSyntaxUid(sourceSyntax)
+              << " to one of: " << s;
+
+#if !defined(NDEBUG)
+    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
+#endif
+
+    DicomTransferSyntax targetSyntax;
+    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
+    {
+      // No transcoding is needed
+      target.AcquireParsed(source);
+      target.AcquireBuffer(source);
+      return true;
+    }
+    else if (InplaceTranscode(targetSyntax, failureReason, source.GetParsed(),
+                              allowedSyntaxes, allowNewSopInstanceUid, lossyQuality))
+    {   
+      // Sanity check
+      DicomTransferSyntax targetSyntax2;
+      if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, source.GetParsed()) &&
+          targetSyntax == targetSyntax2 &&
+          allowedSyntaxes.find(targetSyntax2) != allowedSyntaxes.end())
+      {
+        target.AcquireParsed(source);
+        source.Clear();
+        
+#if !defined(NDEBUG)
+        // Only run the sanity check in debug mode
+        CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
+                         allowedSyntaxes, allowNewSopInstanceUid);
+#endif
+        
+        return true;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+    else
+    {
+      // Cannot transcode
+      LOG(WARNING) << "DCMTK was unable to transcode from " << GetTransferSyntaxUid(sourceSyntax)
+                   << " to one of: " << s << " " << failureReason;
+      return false;
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h
new file mode 100644
index 0000000..4450bc8
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h
@@ -0,0 +1,74 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING)
+#  error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1
+#  error Transcoding is disabled, cannot compile this file
+#endif
+
+#include "IDicomTranscoder.h"
+#include "../MultiThreading/Semaphore.h"
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DcmtkTranscoder : public IDicomTranscoder
+  {
+  private:
+    unsigned int  defaultLossyQuality_;
+    Semaphore maxConcurrentExecutionsSemaphore_;
+
+    bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
+                          std::string& failureReason /* out */,
+                          DcmFileFormat& dicom,
+                          const std::set& allowedSyntaxes,
+                          bool allowNewSopInstanceUid,
+                          unsigned int lossyQuality);
+    
+  public:
+    explicit DcmtkTranscoder(unsigned int maxConcurrentExecutions);
+
+    void SetDefaultLossyQuality(unsigned int quality);
+
+    unsigned int GetDefaultLossyQuality() const;
+    
+    static bool IsSupported(DicomTransferSyntax syntax);
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) ORTHANC_OVERRIDE;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp
new file mode 100644
index 0000000..83c58f2
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp
@@ -0,0 +1,593 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+
+/***
+    
+    Validation:
+
+    # sudo apt-get install dicom3tools
+    # dciodvfy DICOMDIR 2>&1 | less
+    # dcentvfy DICOMDIR 2>&1 | less
+
+    http://www.dclunie.com/dicom3tools/dciodvfy.html
+
+    DICOMDIR viewer working with Wine under Linux:
+    http://www.microdicom.com/
+
+ ***/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomDirWriter.h"
+
+#include "FromDcmtkBridge.h"
+#include "ToDcmtkBridge.h"
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../TemporaryFile.h"
+#include "../Toolbox.h"
+#include "../SystemToolbox.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "dcmtk/dcmdata/dcvrda.h"     /* for class DcmDate */
+#include "dcmtk/dcmdata/dcvrtm.h"     /* for class DcmTime */
+
+#include 
+
+namespace Orthanc
+{
+  class DicomDirWriter::PImpl
+  {
+  private:
+    bool                       utc_;
+    std::string                fileSetId_;
+    bool                       extendedSopClass_;
+    TemporaryFile              file_;
+    std::unique_ptr dir_;
+
+    typedef std::pair  IndexKey;
+    typedef std::map  Index;
+    Index  index_;
+
+
+    DcmDicomDir& GetDicomDir()
+    {
+      if (dir_.get() == NULL)
+      {
+        dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), 
+                                   fileSetId_.c_str()));
+        //SetTagValue(dir_->getRootRecord(), DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(Encoding_Utf8));
+      }
+
+      return *dir_;
+    }
+
+
+    DcmDirectoryRecord& GetRoot()
+    {
+      return GetDicomDir().getRootRecord();
+    }
+
+
+    static bool GetUtf8TagValue(std::string& result,
+                                DcmItem& source,
+                                Encoding encoding,
+                                bool hasCodeExtensions,
+                                const DcmTagKey& key)
+    {
+      DcmElement* element = NULL;
+      result.clear();
+
+      if (source.findAndGetElement(key, element).good())
+      {
+        char* s = NULL;
+        if (element->isLeaf() &&
+            element->getString(s).good())
+        {
+          if (s != NULL)
+          {
+            result = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
+          }
+          
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+
+    static void SetTagValue(DcmDirectoryRecord& target,
+                            const DcmTagKey& key,
+                            const std::string& valueUtf8)
+    {
+      std::string s = Toolbox::ConvertFromUtf8(valueUtf8, Encoding_Ascii);
+
+      if (!target.putAndInsertString(key, s.c_str()).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+                            
+
+
+    static bool CopyString(DcmDirectoryRecord& target,
+                           DcmDataset& source,
+                           Encoding encoding,
+                           bool hasCodeExtensions,
+                           const DcmTagKey& key,
+                           bool optional,
+                           bool copyEmpty)
+    {
+      if (optional &&
+          !source.tagExistsWithValue(key) &&
+          !(copyEmpty && source.tagExists(key)))
+      {
+        return false;
+      }
+
+      std::string value;
+      bool found = GetUtf8TagValue(value, source, encoding, hasCodeExtensions, key);
+
+      if (!found)
+      {
+        // We don't raise an exception if "!optional", even if this
+        // results in an invalid DICOM file
+        value.clear();
+      }
+
+      SetTagValue(target, key, value);
+      return found;
+    }
+
+
+    static void CopyStringType1(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                bool hasCodeExtensions,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, key, false, false);
+    }
+
+    static void CopyStringType1C(DcmDirectoryRecord& target,
+                                 DcmDataset& source,
+                                 Encoding encoding,
+                                 bool hasCodeExtensions,
+                                 const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, key, true, false);
+    }
+
+    static void CopyStringType2(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                bool hasCodeExtensions,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, key, false, true);
+    }
+
+    static void CopyStringType3(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                bool hasCodeExtensions,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, key, true, true);
+    }
+
+
+  public:
+    PImpl() :
+      utc_(true),   // By default, use UTC (universal time, not local time)
+      fileSetId_("ORTHANC_MEDIA"),
+      extendedSopClass_(false)
+    {
+    }
+    
+    bool IsUtcUsed() const
+    {
+      return utc_;
+    }
+
+
+    void SetUtcUsed(bool utc)
+    {
+      utc_ = utc;
+    }
+    
+    void EnableExtendedSopClass(bool enable)
+    {
+      if (enable)
+      {
+        LOG(WARNING) << "Generating a DICOMDIR with type 3 attributes, "
+                     << "which leads to an Extended SOP Class";
+      }
+      
+      extendedSopClass_ = enable;
+    }
+
+    bool IsExtendedSopClass() const
+    {
+      return extendedSopClass_;
+    }
+
+    void FillPatient(DcmDirectoryRecord& record,
+                     DcmDataset& dicom,
+                     Encoding encoding,
+                     bool hasCodeExtensions)
+    {
+      // cf. "DicomDirInterface::buildPatientRecord()"
+
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_PatientID);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_PatientName);
+    }
+
+    void FillStudy(DcmDirectoryRecord& record,
+                   DcmDataset& dicom,
+                   Encoding encoding,
+                   bool hasCodeExtensions)
+    {
+      // cf. "DicomDirInterface::buildStudyRecord()"
+
+      std::string nowDate, nowTime;
+      SystemToolbox::GetNowDicom(nowDate, nowTime, utc_);
+
+      std::string studyDate;
+      if (!GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_StudyDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_SeriesDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_AcquisitionDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_ContentDate))
+      {
+        studyDate = nowDate;
+      }
+          
+      std::string studyTime;
+      if (!GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_StudyTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_SeriesTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_AcquisitionTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_ContentTime))
+      {
+        studyTime = nowTime;
+      }
+
+      /* copy attribute values from dataset to study record */
+      SetTagValue(record, DCM_StudyDate, studyDate);
+      SetTagValue(record, DCM_StudyTime, studyTime);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_StudyDescription);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_StudyID);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_AccessionNumber);
+    }
+
+    void FillSeries(DcmDirectoryRecord& record,
+                    DcmDataset& dicom,
+                    Encoding encoding,
+                    bool hasCodeExtensions)
+    {
+      // cf. "DicomDirInterface::buildSeriesRecord()"
+
+      /* copy attribute values from dataset to series record */
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_Modality);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_SeriesNumber);
+
+      // Add extended (non-standard) type 3 tags, those are not generated by DCMTK
+      // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part02/sect_7.3.html
+      // https://groups.google.com/d/msg/orthanc-users/Y7LOvZMDeoc/9cp3kDgxAwAJ
+      if (extendedSopClass_)
+      {
+        CopyStringType3(record, dicom, encoding, hasCodeExtensions, DCM_SeriesDescription);
+      }
+    }
+
+    void FillInstance(DcmDirectoryRecord& record,
+                      DcmDataset& dicom,
+                      Encoding encoding,
+                      bool hasCodeExtensions,
+                      DcmMetaInfo& metaInfo,
+                      const char* path)
+    {
+      // cf. "DicomDirInterface::buildImageRecord()"
+
+      /* copy attribute values from dataset to image record */
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_InstanceNumber);
+      //CopyElementType1C(record, dicom, encoding, hasCodeExtensions, DCM_ImageType);
+
+      // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
+
+      std::string sopClassUid, sopInstanceUid, transferSyntaxUid;
+      if (!GetUtf8TagValue(sopClassUid, dicom, encoding, hasCodeExtensions, DCM_SOPClassUID) ||
+          !GetUtf8TagValue(sopInstanceUid, dicom, encoding, hasCodeExtensions, DCM_SOPInstanceUID) ||
+          !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, hasCodeExtensions, DCM_TransferSyntaxUID))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      SetTagValue(record, DCM_ReferencedFileID, path);
+      SetTagValue(record, DCM_ReferencedSOPClassUIDInFile, sopClassUid);
+      SetTagValue(record, DCM_ReferencedSOPInstanceUIDInFile, sopInstanceUid);
+      SetTagValue(record, DCM_ReferencedTransferSyntaxUIDInFile, transferSyntaxUid);
+    }
+
+    
+
+    bool CreateResource(DcmDirectoryRecord*& target,
+                        ResourceType level,
+                        ParsedDicomFile& dicom,
+                        const char* filename,
+                        const char* path)
+    {
+      DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+
+      bool hasCodeExtensions;
+      Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+
+      bool found;
+      std::string id;
+      E_DirRecType type;
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          if (!GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_PatientID))
+          {
+            // Be tolerant about missing patient ID. Fixes issue #124
+            // (GET /studies/ID/media fails for certain dicom file).
+            id = "";
+          }
+
+          found = true;
+          type = ERT_Patient;
+          break;
+
+        case ResourceType_Study:
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
+          type = ERT_Study;
+          break;
+
+        case ResourceType_Series:
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
+          type = ERT_Series;
+          break;
+
+        case ResourceType_Instance:
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SOPInstanceUID);
+          type = ERT_Image;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!found)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      IndexKey key = std::make_pair(level, std::string(id.c_str()));
+      Index::iterator it = index_.find(key);
+
+      if (it != index_.end())
+      {
+        target = it->second;
+        return false; // Already existing
+      }
+
+      std::unique_ptr record(new DcmDirectoryRecord(type, NULL, filename));
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          FillPatient(*record, dataset, encoding, hasCodeExtensions);
+          break;
+
+        case ResourceType_Study:
+          FillStudy(*record, dataset, encoding, hasCodeExtensions);
+          break;
+
+        case ResourceType_Series:
+          FillSeries(*record, dataset, encoding, hasCodeExtensions);
+          break;
+
+        case ResourceType_Instance:
+          FillInstance(*record, dataset, encoding, hasCodeExtensions, *dicom.GetDcmtkObject().getMetaInfo(), path);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      CopyStringType1C(*record, dataset, encoding, hasCodeExtensions, DCM_SpecificCharacterSet);
+
+      target = record.get();
+      GetRoot().insertSub(record.release());
+      index_[key] = target;
+
+      return true;   // Newly created
+    }
+
+    void Write(std::string& s)
+    {
+      if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX, 
+                               EET_UndefinedLength /*encodingType*/, 
+                               EGL_withoutGL /*groupLength*/).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      file_.Read(s);
+    }
+
+    void SetFileSetId(const std::string& id)
+    {
+      dir_.reset(NULL);
+      fileSetId_ = id;
+    }
+  };
+
+
+  DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl)
+  {
+  }
+
+  void DicomDirWriter::SetUtcUsed(bool utc)
+  {
+    pimpl_->SetUtcUsed(utc);
+  }
+  
+  bool DicomDirWriter::IsUtcUsed() const
+  {
+    return pimpl_->IsUtcUsed();
+  }
+
+  void DicomDirWriter::SetFileSetId(const std::string& id)
+  {
+    pimpl_->SetFileSetId(id);
+  }
+
+  void DicomDirWriter::Add(const std::string& directory,
+                           const std::string& filename,
+                           ParsedDicomFile& dicom)
+  {
+    std::string path;
+    if (directory.empty())
+    {
+      path = filename;
+    }
+    else
+    {
+      if (directory[directory.length() - 1] == '/' ||
+          directory[directory.length() - 1] == '\\')
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      path = directory + '\\' + filename;
+    }
+
+    DcmDirectoryRecord* instance;
+    bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, dicom, filename.c_str(), path.c_str());
+    if (isNewInstance)
+    {
+      DcmDirectoryRecord* series;
+      bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, dicom, filename.c_str(), NULL);
+      series->insertSub(instance);
+
+      if (isNewSeries)
+      {
+        DcmDirectoryRecord* study;
+        bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, dicom, filename.c_str(), NULL);
+        study->insertSub(series);
+  
+        if (isNewStudy)
+        {
+          DcmDirectoryRecord* patient;
+          pimpl_->CreateResource(patient, ResourceType_Patient, dicom, filename.c_str(), NULL);
+          patient->insertSub(study);
+        }
+      }
+    }
+  }
+
+  void DicomDirWriter::Encode(std::string& target)
+  {
+    pimpl_->Write(target);
+  }
+
+
+  void DicomDirWriter::EnableExtendedSopClass(bool enable)
+  {
+    pimpl_->EnableExtendedSopClass(enable);
+  }
+
+  
+  bool DicomDirWriter::IsExtendedSopClass() const
+  {
+    return pimpl_->IsExtendedSopClass();
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h
new file mode 100644
index 0000000..ffc1dbf
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h
@@ -0,0 +1,59 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "ParsedDicomFile.h"
+
+#include 
+
+namespace Orthanc
+{
+  class DicomDirWriter : public boost::noncopyable
+  {
+  private:
+    class PImpl;
+    boost::shared_ptr  pimpl_;
+
+  public:
+    DicomDirWriter();
+
+    void SetUtcUsed(bool utc);
+
+    bool IsUtcUsed() const;
+
+    void SetFileSetId(const std::string& id);
+
+    void Add(const std::string& directory,
+             const std::string& filename,
+             ParsedDicomFile& dicom);
+
+    void Encode(std::string& target);
+
+    void EnableExtendedSopClass(bool enable);
+
+    bool IsExtendedSopClass() const;
+  };
+
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp
new file mode 100644
index 0000000..1e134fa
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp
@@ -0,0 +1,1901 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomModification.h"
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "FromDcmtkBridge.h"
+#include "ITagVisitor.h"
+
+#include    // For std::unique_ptr
+
+
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2008 =
+  "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1";
+
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2017c =
+  "Orthanc " ORTHANC_VERSION " - PS 3.15-2017c Table E.1-1 Basic Profile";
+
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2021b =
+  "Orthanc " ORTHANC_VERSION " - PS 3.15-2021b Table E.1-1 Basic Profile";
+
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2023b =
+  "Orthanc " ORTHANC_VERSION " - PS 3.15-2023b Table E.1-1 Basic Profile";
+
+static const std::string ORTHANC_UNSAFE_DEIDENTIFICATION =
+  "Orthanc " ORTHANC_VERSION;
+
+namespace Orthanc
+{
+  namespace
+  {
+    enum TagOperation
+    {
+      TagOperation_Keep,
+      TagOperation_Remove
+    };
+  }
+
+  
+  DicomModification::DicomTagRange::DicomTagRange(uint16_t groupFrom,
+                                                  uint16_t groupTo,
+                                                  uint16_t elementFrom,
+                                                  uint16_t elementTo) :
+    groupFrom_(groupFrom),
+    groupTo_(groupTo),
+    elementFrom_(elementFrom),
+    elementTo_(elementTo)
+  {
+  }
+
+  
+  bool DicomModification::DicomTagRange::Contains(const DicomTag& tag) const
+  {
+    return (tag.GetGroup() >= groupFrom_ &&
+            tag.GetGroup() <= groupTo_ &&
+            tag.GetElement() >= elementFrom_ &&
+            tag.GetElement() <= elementTo_);
+  }
+
+
+  class DicomModification::RelationshipsVisitor : public ITagVisitor
+  {
+  private:
+    DicomModification&  that_;
+
+    // This method is only applicable to first-level tags
+    bool IsManuallyModified(const DicomTag& tag) const
+    {
+      return (that_.IsCleared(tag) ||
+              that_.IsRemoved(tag) ||
+              that_.IsReplaced(tag));
+    }
+
+    bool IsKeptSequence(const std::vector& parentTags,
+                        const std::vector& parentIndexes,
+                        const DicomTag& tag)
+    {
+      for (DicomModification::ListOfPaths::const_iterator
+             it = that_.keepSequences_.begin(); it != that_.keepSequences_.end(); ++it)
+      {
+        if (DicomPath::IsMatch(*it, parentTags, parentIndexes, tag))
+        {
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+    Action GetDefaultAction(const std::vector& parentTags,
+                            const std::vector& parentIndexes,
+                            const DicomTag& tag)
+    {
+      if (parentTags.empty() ||
+          !that_.isAnonymization_)
+      {
+        // Don't interfere with first-level tags or with modification
+        return Action_None;
+      }
+      else if (IsKeptSequence(parentTags, parentIndexes, tag))
+      {
+        return Action_None;
+      }
+      else if (that_.ArePrivateTagsRemoved() &&
+               tag.IsPrivate())
+      {
+        // New in Orthanc 1.9.5
+        // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
+        return Action_Remove;
+      }
+      else if (that_.IsCleared(tag) ||
+               that_.IsRemoved(tag))
+      {
+        // New in Orthanc 1.9.5
+        // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
+        return Action_Remove;
+      }
+      else
+      {
+        return Action_None;
+      }
+    }
+
+  public:
+    explicit RelationshipsVisitor(DicomModification& that) :
+      that_(that)
+    {
+    }
+
+    virtual Action VisitNotSupported(const std::vector& parentTags,
+                                     const std::vector& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr) ORTHANC_OVERRIDE
+    {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
+    }
+
+    virtual Action VisitSequence(const std::vector& parentTags,
+                                 const std::vector& parentIndexes,
+                                 const DicomTag& tag,
+                                 size_t countItems) ORTHANC_OVERRIDE
+    {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
+    }
+
+    virtual Action VisitBinary(const std::vector& parentTags,
+                               const std::vector& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const void* data,
+                               size_t size) ORTHANC_OVERRIDE
+    {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
+    }
+
+    virtual Action VisitIntegers(const std::vector& parentTags,
+                                 const std::vector& parentIndexes,
+                                 const DicomTag& tag,
+                                 ValueRepresentation vr,
+                                 const std::vector& values) ORTHANC_OVERRIDE
+    {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
+    }
+
+    virtual Action VisitDoubles(const std::vector& parentTags,
+                                const std::vector& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const std::vector& value) ORTHANC_OVERRIDE
+    {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
+    }
+
+    virtual Action VisitAttributes(const std::vector& parentTags,
+                                   const std::vector& parentIndexes,
+                                   const DicomTag& tag,
+                                   const std::vector& value) ORTHANC_OVERRIDE
+    {
+      return GetDefaultAction(parentTags, parentIndexes, tag);
+    }
+
+    virtual Action VisitString(std::string& newValue,
+                               const std::vector& parentTags,
+                               const std::vector& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      /**
+       * Note that all the tags in "uids_" have the VR UI (unique
+       * identifier), and are considered as strings.
+       *
+       * Also, the tags "SOP Instance UID", "Series Instance UID" and
+       * "Study Instance UID" are *never* included in "uids_", as they
+       * are separately handed by "MapDicomTags()".
+       **/
+
+      assert(that_.uids_.find(DICOM_TAG_STUDY_INSTANCE_UID) == that_.uids_.end());
+      assert(that_.uids_.find(DICOM_TAG_SERIES_INSTANCE_UID) == that_.uids_.end());
+      assert(that_.uids_.find(DICOM_TAG_SOP_INSTANCE_UID) == that_.uids_.end());
+
+      if (parentTags.empty())
+      {
+        // We are on a first-level tag
+        if (that_.uids_.find(tag) != that_.uids_.end() &&
+            !IsManuallyModified(tag))
+        {
+          if (tag == DICOM_TAG_PATIENT_ID ||
+              tag == DICOM_TAG_PATIENT_NAME)
+          {
+            assert(vr == ValueRepresentation_LongString ||
+                   vr == ValueRepresentation_PersonName);
+            newValue = that_.MapDicomIdentifier(value, ResourceType_Patient);            
+          }
+          else
+          {
+            // This is a first-level UID tag that must be anonymized
+            assert(vr == ValueRepresentation_UniqueIdentifier ||
+                   vr == ValueRepresentation_NotSupported /* for older versions of DCMTK */);
+            newValue = that_.MapDicomIdentifier(value, ResourceType_Instance);
+          }
+          
+          return Action_Replace;
+        }
+        else
+        {
+          return Action_None;
+        }
+      }
+      else
+      {
+        // We are within a sequence
+
+        if (IsKeptSequence(parentTags, parentIndexes, tag))
+        {
+          // New in Orthanc 1.9.4 - Solves issue LSD-629
+          return Action_None;
+        }
+
+        if (that_.isAnonymization_)
+        {
+          // New in Orthanc 1.9.5, similar to "GetDefaultAction()"
+          // https://groups.google.com/g/orthanc-users/c/l1mcYCC2u-k/m/jOdGYuagAgAJ
+          if (that_.ArePrivateTagsRemoved() &&
+              tag.IsPrivate())
+          {
+            return Action_Remove;
+          }
+          else if (that_.IsRemoved(tag))
+          {
+            return Action_Remove;
+          }
+          else if (that_.IsCleared(tag))
+          {
+            // This is different from "GetDefaultAction()", because we know how to clear string tags
+            newValue.clear();
+            return Action_Replace;
+          }
+        }
+
+        if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
+        {
+          newValue = that_.MapDicomIdentifier(value, ResourceType_Study);
+          return Action_Replace;
+        }
+        else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
+        {
+          newValue = that_.MapDicomIdentifier(value, ResourceType_Series);
+          return Action_Replace;
+        }
+        else if (tag == DICOM_TAG_SOP_INSTANCE_UID)
+        {  
+          newValue = that_.MapDicomIdentifier(value, ResourceType_Instance);
+          return Action_Replace;
+        }
+        else if (that_.uids_.find(tag) != that_.uids_.end())
+        {
+          if (tag == DICOM_TAG_PATIENT_ID ||
+              tag == DICOM_TAG_PATIENT_NAME)
+          {
+            newValue = that_.MapDicomIdentifier(value, ResourceType_Patient);
+          }
+          else
+          {
+            assert(vr == ValueRepresentation_UniqueIdentifier ||
+                   vr == ValueRepresentation_NotSupported /* for older versions of DCMTK */);
+
+            if (parentTags.size() == 2 &&
+                parentTags[0] == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE &&
+                parentTags[1] == DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE &&
+                tag == DICOM_TAG_REFERENCED_SOP_INSTANCE_UID)
+            {
+              /**
+               * In RT-STRUCT, this ReferencedSOPInstanceUID is actually
+               * referencing a StudyInstanceUID !! (observed in many
+               * data sets including:
+               * https://wiki.cancerimagingarchive.net/display/Public/Lung+CT+Segmentation+Challenge+2017)
+               * Tested in "test_anonymize_relationships_5". Introduced
+               * in: https://orthanc.uclouvain.be/hg/orthanc/rev/3513
+               **/
+              newValue = that_.MapDicomIdentifier(value, ResourceType_Study);
+            }
+            else
+            {
+              newValue = that_.MapDicomIdentifier(value, ResourceType_Instance);
+            }
+          }
+
+          return Action_Replace;
+        }
+        else
+        {
+          return Action_None;
+        }
+      }
+    }
+
+    void RemoveRelationships(ParsedDicomFile& dicom) const
+    {
+      for (SetOfTags::const_iterator it = that_.uids_.begin(); it != that_.uids_.end(); ++it)
+      {
+        assert(*it != DICOM_TAG_STUDY_INSTANCE_UID &&
+               *it != DICOM_TAG_SERIES_INSTANCE_UID &&
+               *it != DICOM_TAG_SOP_INSTANCE_UID);
+
+        if (!IsManuallyModified(*it))
+        {
+          dicom.Remove(*it);
+        }
+      }
+
+      // The only two sequences with to the "X/Z/U*" rule in the
+      // basic profile. They were already present in Orthanc 1.9.3.
+      if (!IsManuallyModified(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE))
+      {
+        dicom.Remove(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE);
+      }
+      
+      if (!IsManuallyModified(DICOM_TAG_SOURCE_IMAGE_SEQUENCE))
+      {
+        dicom.Remove(DICOM_TAG_SOURCE_IMAGE_SEQUENCE);
+      }
+    }
+  };
+
+
+  void DicomModification::CancelReplacement(const DicomTag& tag)
+  {
+    Replacements::iterator it = replacements_.find(tag);
+    
+    if (it != replacements_.end())
+    {
+      assert(it->second != NULL);
+      delete it->second;
+      replacements_.erase(it);
+    }
+  }
+
+
+  void DicomModification::ReplaceInternal(const DicomTag& tag,
+                                          const Json::Value& value)
+  {
+    Replacements::iterator it = replacements_.find(tag);
+
+    if (it != replacements_.end())
+    {
+      assert(it->second != NULL);
+      delete it->second;
+      it->second = NULL;   // In the case of an exception during the clone
+      it->second = new Json::Value(value);  // Clone
+    }
+    else
+    {
+      replacements_[tag] = new Json::Value(value);  // Clone
+    }
+  }
+
+
+  void DicomModification::ClearReplacements()
+  {
+    for (Replacements::iterator it = replacements_.begin();
+         it != replacements_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+
+    replacements_.clear();
+
+    for (SequenceReplacements::iterator it = sequenceReplacements_.begin();
+         it != sequenceReplacements_.end(); ++it)
+    {
+      assert(*it != NULL);
+      assert((*it)->GetPath().GetPrefixLength() > 0);
+      delete *it;
+    }
+
+    sequenceReplacements_.clear();
+  }
+
+
+  void DicomModification::MarkNotOrthancAnonymization()
+  {
+    Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD);
+
+    if (it != replacements_.end())
+    {
+      assert(it->second != NULL);
+
+      if (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 ||
+          it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c ||
+          it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b ||
+          it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2023b)
+      {
+        ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_UNSAFE_DEIDENTIFICATION);
+      }
+    }
+  }
+
+  void DicomModification::RegisterMappedDicomIdentifier(const std::string& original,
+                                                        const std::string& mapped,
+                                                        ResourceType level)
+  {
+    UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original));
+
+    if (previous == uidMap_.end())
+    {
+      uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped));
+    }
+  }
+
+  std::string DicomModification::MapDicomIdentifier(const std::string& original,
+                                                    ResourceType level)
+  {
+    const std::string stripped = Toolbox::StripSpaces(original);
+    
+    std::string mapped;
+
+    UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, stripped));
+
+    if (previous == uidMap_.end())
+    {
+      if (identifierGenerator_ == NULL)
+      {
+        mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level);
+      }
+      else
+      {
+        if (!identifierGenerator_->Apply(mapped, stripped, level, currentSource_))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Unable to generate an anonymized ID");
+        }
+      }
+
+      uidMap_.insert(std::make_pair(std::make_pair(level, stripped), mapped));
+    }
+    else
+    {
+      mapped = previous->second;
+    }
+
+    return mapped;
+  }
+
+
+  void DicomModification::MapDicomTags(ParsedDicomFile& dicom,
+                                       ResourceType level)
+  {
+    std::unique_ptr tag;
+
+    switch (level)
+    {
+      case ResourceType_Study:
+        tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
+        break;
+
+      case ResourceType_Series:
+        tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
+        break;
+
+      case ResourceType_Instance:
+        tag.reset(new DicomTag(DICOM_TAG_SOP_INSTANCE_UID));
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::string original;
+    if (!const_cast(dicom).GetTagValue(original, *tag))
+    {
+      original = "";
+    }
+
+    std::string mapped = MapDicomIdentifier(original, level);
+
+    dicom.Replace(*tag, mapped, 
+                  false /* don't try and decode data URI scheme for UIDs */, 
+                  DicomReplaceMode_InsertIfAbsent, privateCreator_);
+  }
+
+  
+  DicomModification::DicomModification() :
+    removePrivateTags_(false),
+    keepLabels_(false),
+    level_(ResourceType_Instance),
+    allowManualIdentifiers_(true),
+    keepStudyInstanceUid_(false),
+    keepSeriesInstanceUid_(false),
+    keepSopInstanceUid_(false),
+    updateReferencedRelationships_(true),
+    isAnonymization_(false),
+    //privateCreator_("PrivateCreator"),
+    identifierGenerator_(NULL)
+  {
+  }
+
+  DicomModification::~DicomModification()
+  {
+    ClearReplacements();
+  }
+
+  void DicomModification::Keep(const DicomTag& tag)
+  {
+    keep_.insert(tag);
+    removals_.erase(tag);
+    clearings_.erase(tag);
+    uids_.erase(tag);
+
+    CancelReplacement(tag);
+
+    if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
+    {
+      keepStudyInstanceUid_ = true;
+    }
+    else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
+    {
+      keepSeriesInstanceUid_ = true;
+    }
+    else if (tag == DICOM_TAG_SOP_INSTANCE_UID)
+    {
+      keepSopInstanceUid_ = true;
+    }
+    else if (tag.IsPrivate())
+    {
+      privateTagsToKeep_.insert(tag);
+    }
+
+    MarkNotOrthancAnonymization();
+  }
+
+  void DicomModification::Remove(const DicomTag& tag)
+  {
+    removals_.insert(tag);
+    clearings_.erase(tag);
+    uids_.erase(tag);
+    CancelReplacement(tag);
+    privateTagsToKeep_.erase(tag);
+
+    MarkNotOrthancAnonymization();
+  }
+
+  void DicomModification::Clear(const DicomTag& tag)
+  {
+    removals_.erase(tag);
+    clearings_.insert(tag);
+    uids_.erase(tag);
+    CancelReplacement(tag);
+    privateTagsToKeep_.erase(tag);
+
+    MarkNotOrthancAnonymization();
+  }
+
+  bool DicomModification::IsRemoved(const DicomTag& tag) const
+  {
+    if (removals_.find(tag) != removals_.end())
+    {
+      return true;
+    }
+    else
+    {
+      for (RemovedRanges::const_iterator it = removedRanges_.begin();
+           it != removedRanges_.end(); ++it)
+      {
+        if (it->Contains(tag))
+        {
+          return true;
+        }
+      }
+
+      return false;
+    }
+  }
+
+  bool DicomModification::IsCleared(const DicomTag& tag) const
+  {
+    return clearings_.find(tag) != clearings_.end();
+  }
+
+  void DicomModification::Replace(const DicomTag& tag,
+                                  const Json::Value& value,
+                                  bool safeForAnonymization)
+  {
+    clearings_.erase(tag);
+    removals_.erase(tag);
+    uids_.erase(tag);
+    privateTagsToKeep_.erase(tag);
+    ReplaceInternal(tag, value);
+
+    if (!safeForAnonymization)
+    {
+      MarkNotOrthancAnonymization();
+    }
+  }
+
+
+  bool DicomModification::IsReplaced(const DicomTag& tag) const
+  {
+    return replacements_.find(tag) != replacements_.end();
+  }
+
+  bool DicomModification::IsKept(const DicomTag& tag) const
+  {
+    return keep_.find(tag) != keep_.end();
+  }
+
+  const Json::Value& DicomModification::GetReplacement(const DicomTag& tag) const
+  {
+    Replacements::const_iterator it = replacements_.find(tag);
+
+    if (it == replacements_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      assert(it->second != NULL);
+      return *it->second;
+    } 
+  }
+
+
+  std::string DicomModification::GetReplacementAsString(const DicomTag& tag) const
+  {
+    const Json::Value& json = GetReplacement(tag);
+
+    if (json.type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return json.asString();
+    }    
+  }
+
+
+  void DicomModification::SetRemovePrivateTags(bool removed)
+  {
+    removePrivateTags_ = removed;
+
+    if (!removed)
+    {
+      MarkNotOrthancAnonymization();
+    }
+  }
+
+  bool DicomModification::ArePrivateTagsRemoved() const
+  {
+    return removePrivateTags_;
+  }
+
+  void DicomModification::SetKeepLabels(bool keep)
+  {
+    keepLabels_ = keep;
+  }
+
+  bool DicomModification::AreLabelsKept() const
+  {
+    return keepLabels_;
+  }
+
+  void DicomModification::SetLevel(ResourceType level)
+  {
+    uidMap_.clear();
+    level_ = level;
+
+    if (level != ResourceType_Patient)
+    {
+      MarkNotOrthancAnonymization();
+    }
+  }
+
+  ResourceType DicomModification::GetLevel() const
+  {
+    return level_;
+  }
+
+
+  static void SetupUidsFromOrthancInternal(std::set& uids,
+                                           std::set& removals,
+                                           const DicomTag& tag)
+  {
+    uids.insert(tag);
+    removals.erase(tag);  // Necessary if unserializing a job from 1.9.3
+  }
+  
+
+  void DicomModification::SetupUidsFromOrthanc_1_9_3()
+  {
+    /**
+     * Values below come from the hardcoded UID of Orthanc 1.9.3
+     * in DicomModification::RelationshipsVisitor::VisitString() and
+     * DicomModification::RelationshipsVisitor::RemoveRelationships()
+     * https://orthanc.uclouvain.be/hg/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/DicomParsing/DicomModification.cpp#l117
+     **/
+    uids_.clear();
+
+    // (*) "PatientID" and "PatientName" are handled as UIDs since Orthanc 1.9.4
+    uids_.insert(DICOM_TAG_PATIENT_ID);
+    uids_.insert(DICOM_TAG_PATIENT_NAME);
+    
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0008, 0x0014));  // Instance Creator UID                   <= from SetupAnonymization2008()
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0008, 0x1155));  // Referenced SOP Instance UID            <= from VisitString() + RemoveRelationships()
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0020, 0x0052));  // Frame of Reference UID                 <= from VisitString() + RemoveRelationships()
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0020, 0x0200));  // Synchronization Frame of Reference UID <= from SetupAnonymization2008()
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0040, 0xa124));  // UID                                    <= from SetupAnonymization2008()
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x0088, 0x0140));  // Storage Media File-set UID             <= from SetupAnonymization2008()
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x3006, 0x0024));  // Referenced Frame of Reference UID      <= from VisitString() + RemoveRelationships()
+    SetupUidsFromOrthancInternal(uids_, removals_, DicomTag(0x3006, 0x00c2));  // Related Frame of Reference UID         <= from VisitString() + RemoveRelationships()
+  }
+
+
+  void DicomModification::SetupAnonymization2008()
+  {
+    // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles
+    // https://raw.githubusercontent.com/jodogne/dicom-specification/master/2008/08_15pu.pdf
+
+    SetupUidsFromOrthanc_1_9_3();
+    
+    //uids_.insert(DicomTag(0x0008, 0x0014));  // Instance Creator UID => set in SetupUidsFromOrthanc_1_9_3()
+    //removals_.insert(DicomTag(0x0008, 0x0018));  // SOP Instance UID => set in Apply()
+    removals_.insert(DicomTag(0x0008, 0x0050));  // Accession Number
+    removals_.insert(DicomTag(0x0008, 0x0080));  // Institution Name
+    removals_.insert(DicomTag(0x0008, 0x0081));  // Institution Address
+    removals_.insert(DicomTag(0x0008, 0x0090));  // Referring Physician's Name 
+    removals_.insert(DicomTag(0x0008, 0x0092));  // Referring Physician's Address 
+    removals_.insert(DicomTag(0x0008, 0x0094));  // Referring Physician's Telephone Numbers 
+    removals_.insert(DicomTag(0x0008, 0x1010));  // Station Name 
+    removals_.insert(DicomTag(0x0008, 0x1030));  // Study Description 
+    removals_.insert(DicomTag(0x0008, 0x103e));  // Series Description 
+    removals_.insert(DicomTag(0x0008, 0x1040));  // Institutional Department Name 
+    removals_.insert(DicomTag(0x0008, 0x1048));  // Physician(s) of Record 
+    removals_.insert(DicomTag(0x0008, 0x1050));  // Performing Physicians' Name 
+    removals_.insert(DicomTag(0x0008, 0x1060));  // Name of Physician(s) Reading Study 
+    removals_.insert(DicomTag(0x0008, 0x1070));  // Operators' Name 
+    removals_.insert(DicomTag(0x0008, 0x1080));  // Admitting Diagnoses Description 
+    //uids_.insert(DicomTag(0x0008, 0x1155));      // Referenced SOP Instance UID => set in SetupUidsFromOrthanc_1_9_3()
+    removals_.insert(DicomTag(0x0008, 0x2111));  // Derivation Description 
+    //removals_.insert(DicomTag(0x0010, 0x0010));  // Patient's Name => cf. below (*)
+    //removals_.insert(DicomTag(0x0010, 0x0020));  // Patient ID => cf. below (*)
+    removals_.insert(DicomTag(0x0010, 0x0030));  // Patient's Birth Date 
+    removals_.insert(DicomTag(0x0010, 0x0032));  // Patient's Birth Time 
+    removals_.insert(DicomTag(0x0010, 0x0040));  // Patient's Sex 
+    removals_.insert(DicomTag(0x0010, 0x1000));  // Other Patient Ids 
+    removals_.insert(DicomTag(0x0010, 0x1001));  // Other Patient Names 
+    removals_.insert(DicomTag(0x0010, 0x1010));  // Patient's Age 
+    removals_.insert(DicomTag(0x0010, 0x1020));  // Patient's Size 
+    removals_.insert(DicomTag(0x0010, 0x1030));  // Patient's Weight 
+    removals_.insert(DicomTag(0x0010, 0x1090));  // Medical Record Locator 
+    removals_.insert(DicomTag(0x0010, 0x2160));  // Ethnic Group 
+    removals_.insert(DicomTag(0x0010, 0x2180));  // Occupation 
+    removals_.insert(DicomTag(0x0010, 0x21b0));  // Additional Patient's History 
+    removals_.insert(DicomTag(0x0010, 0x4000));  // Patient Comments 
+    removals_.insert(DicomTag(0x0018, 0x1000));  // Device Serial Number 
+    removals_.insert(DicomTag(0x0018, 0x1030));  // Protocol Name 
+    //removals_.insert(DicomTag(0x0020, 0x000d));  // Study Instance UID => set in Apply()
+    //removals_.insert(DicomTag(0x0020, 0x000e));  // Series Instance UID => set in Apply()
+    removals_.insert(DicomTag(0x0020, 0x0010));  // Study ID 
+    //uids_.insert(DicomTag(0x0020, 0x0052));      // Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
+    //uids_.insert(DicomTag(0x0020, 0x0200));      // Synchronization Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
+    removals_.insert(DicomTag(0x0020, 0x4000));  // Image Comments 
+    removals_.insert(DicomTag(0x0040, 0x0275));  // Request Attributes Sequence 
+    //uids_.insert(DicomTag(0x0040, 0xa124));      // UID => set in SetupUidsFromOrthanc_1_9_3()
+    removals_.insert(DicomTag(0x0040, 0xa730));  // Content Sequence 
+    //uids_.insert(DicomTag(0x0088, 0x0140));      // Storage Media File-set UID => set in SetupUidsFromOrthanc_1_9_3()
+    //uids_.insert(DicomTag(0x3006, 0x0024));      // Referenced Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
+    //uids_.insert(DicomTag(0x3006, 0x00c2));      // Related Frame of Reference UID => set in SetupUidsFromOrthanc_1_9_3()
+
+    // Some more removals (from the experience of DICOM files at the CHU of Liege)
+    removals_.insert(DicomTag(0x0010, 0x1040));  // Patient's Address
+    removals_.insert(DicomTag(0x0032, 0x1032));  // Requesting Physician
+    removals_.insert(DicomTag(0x0010, 0x2154));  // PatientTelephoneNumbers
+    removals_.insert(DicomTag(0x0010, 0x2000));  // Medical Alerts
+
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2008);
+  }
+  
+
+  void DicomModification::SetupAnonymization2017c()
+  {
+    /**
+     * This is Table E.1-1 from PS 3.15-2017c (DICOM Part 15: Security
+     * and System Management Profiles), "basic profile" column. It was
+     * generated automatically by calling:
+     * "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
+     * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2017c/part15.xml"
+     **/
+    
+#include "DicomModification_Anonymization2017c.impl.h"
+    
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2017c);
+  }
+  
+
+  void DicomModification::SetupAnonymization2021b()
+  {
+    /**
+     * This is Table E.1-1 from PS 3.15-2021b (DICOM Part 15: Security
+     * and System Management Profiles), "basic profile" column. It was
+     * generated automatically by calling:
+     * "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
+     * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2021b/part15.xml"
+     **/
+    
+#include "DicomModification_Anonymization2021b.impl.h"
+    
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2021b);
+  }
+  
+
+  void DicomModification::SetupAnonymization2023b()
+  {
+    /**
+     * This is Table E.1-1 from PS 3.15-2023b (DICOM Part 15: Security
+     * and System Management Profiles), "basic profile" column. It was
+     * generated automatically by calling:
+     * "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py
+     * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2023b/part15.xml"
+     *
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1a
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1
+     **/
+    
+#include "DicomModification_Anonymization2023b.impl.h"
+    
+    // Set the DeidentificationMethod tag
+    ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2023b);
+  }
+  
+
+  void DicomModification::SetupAnonymization(DicomVersion version)
+  {
+    isAnonymization_ = true;
+    
+    keep_.clear();
+    removals_.clear();
+    clearings_.clear();
+    removedRanges_.clear();
+    uids_.clear();
+    ClearReplacements();
+    removePrivateTags_ = true;
+    level_ = ResourceType_Patient;
+    uidMap_.clear();
+    privateTagsToKeep_.clear();
+    keepSequences_.clear();
+    removeSequences_.clear();    
+
+    switch (version)
+    {
+      case DicomVersion_2008:
+        SetupAnonymization2008();
+        break;
+
+      case DicomVersion_2017c:
+        SetupAnonymization2017c();
+        break;
+
+      case DicomVersion_2021b:
+        SetupAnonymization2021b();
+        break;
+
+      case DicomVersion_2023b:
+        SetupAnonymization2023b();
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    // Set the PatientIdentityRemoved tag
+    ReplaceInternal(DICOM_TAG_PATIENT_IDENTITY_REMOVED, "YES");
+
+    // (*) Choose a random patient name and ID
+    uids_.insert(DICOM_TAG_PATIENT_ID);
+    uids_.insert(DICOM_TAG_PATIENT_NAME);
+
+    // Sanity check
+    for (SetOfTags::const_iterator it = uids_.begin(); it != uids_.end(); ++it)
+    {
+      ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(*it);
+      if (*it == DICOM_TAG_PATIENT_ID)
+      {
+        if (vr != ValueRepresentation_LongString &&
+            vr != ValueRepresentation_NotSupported /* if no dictionary loaded */)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+      else if (*it == DICOM_TAG_PATIENT_NAME)
+      {
+        if (vr != ValueRepresentation_PersonName &&
+            vr != ValueRepresentation_NotSupported /* if no dictionary loaded */)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+      else if (vr != ValueRepresentation_UniqueIdentifier &&
+               vr != ValueRepresentation_NotSupported /* for older versions of DCMTK */)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }        
+  }
+
+  void DicomModification::Apply(ParsedDicomFile& toModify)
+  {
+    // Check the request
+    assert(ResourceType_Patient + 1 == ResourceType_Study &&
+           ResourceType_Study + 1 == ResourceType_Series &&
+           ResourceType_Series + 1 == ResourceType_Instance);
+
+    if (IsRemoved(DICOM_TAG_PATIENT_ID) ||
+        IsRemoved(DICOM_TAG_STUDY_INSTANCE_UID) ||
+        IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) ||
+        IsRemoved(DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      throw OrthancException(ErrorCode_BadRequest, "It is forbidden to remove one of the main Dicom identifiers");
+    }
+    
+    if (!allowManualIdentifiers_)
+    {
+       // Sanity checks at the patient level
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the StudyInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the SeriesInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the SopInstanceUID cannot be manually modified");
+      }
+
+      // Sanity checks at the study level
+      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the SeriesInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the SopInstanceUID cannot be manually modified");
+      }
+
+      // Sanity checks at the series level
+      if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a series, the SopInstanceUID cannot be manually modified");
+      }
+    }
+
+
+    // (0) Create a summary of the source file, if a custom generator
+    // is provided
+    if (identifierGenerator_ != NULL)
+    {
+      toModify.ExtractDicomSummary(currentSource_, ORTHANC_MAXIMUM_TAG_LENGTH);
+    }
+
+    // (1) Make sure the relationships are updated with the ids that we force too
+    // i.e: an RT-STRUCT is referencing its own StudyInstanceUID
+    if (isAnonymization_ && updateReferencedRelationships_)
+    {
+      if (IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        std::string original;
+        std::string replacement = GetReplacementAsString(DICOM_TAG_STUDY_INSTANCE_UID);
+        const_cast(toModify).GetTagValue(original, DICOM_TAG_STUDY_INSTANCE_UID);
+        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Study);
+      }
+
+      if (IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        std::string original;
+        std::string replacement = GetReplacementAsString(DICOM_TAG_SERIES_INSTANCE_UID);
+        const_cast(toModify).GetTagValue(original, DICOM_TAG_SERIES_INSTANCE_UID);
+        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Series);
+      }
+
+      if (IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        std::string original;
+        std::string replacement = GetReplacementAsString(DICOM_TAG_SOP_INSTANCE_UID);
+        const_cast(toModify).GetTagValue(original, DICOM_TAG_SOP_INSTANCE_UID);
+        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Instance);
+      }
+    }
+
+
+    // (2) Remove the private tags, if need be
+    if (removePrivateTags_)
+    {
+      toModify.RemovePrivateTags(privateTagsToKeep_);
+    }
+
+    // (3) Clear the tags specified by the user
+    for (SetOfTags::const_iterator it = clearings_.begin(); 
+         it != clearings_.end(); ++it)
+    {
+      toModify.Clear(*it, true /* only clear if the tag exists in the original file */);
+    }
+
+    // (4) Remove the tags specified by the user
+    for (SetOfTags::const_iterator it = removals_.begin(); 
+         it != removals_.end(); ++it)
+    {
+      toModify.Remove(*it);
+    }
+
+    // (5) Replace the tags
+    for (Replacements::const_iterator it = replacements_.begin(); 
+         it != replacements_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      toModify.Replace(it->first, *it->second, true /* decode data URI scheme */,
+                       DicomReplaceMode_InsertIfAbsent, privateCreator_);
+    }
+
+    // (6) Update the DICOM identifiers
+    if (level_ <= ResourceType_Study &&
+        !IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      if (keepStudyInstanceUid_)
+      {
+        LOG(WARNING) << "Modifying a study while keeping its original StudyInstanceUID: This should be avoided!";
+      }
+      else
+      {
+        MapDicomTags(toModify, ResourceType_Study);
+      }
+    }
+
+    if (level_ <= ResourceType_Series &&
+        !IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      if (keepSeriesInstanceUid_)
+      {
+        LOG(WARNING) << "Modifying a series while keeping its original SeriesInstanceUID: This should be avoided!";
+      }
+      else
+      {
+        MapDicomTags(toModify, ResourceType_Series);
+      }
+    }
+
+    if (level_ <= ResourceType_Instance &&  // Always true
+        !IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      if (keepSopInstanceUid_)
+      {
+        LOG(WARNING) << "Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!";
+      }
+      else
+      {
+        MapDicomTags(toModify, ResourceType_Instance);
+      }
+    }
+
+    // (7) Update the "referenced" relationships in the case of an anonymization
+    if (isAnonymization_)
+    {
+      RelationshipsVisitor visitor(*this);
+
+      if (updateReferencedRelationships_)
+      {
+        const_cast(toModify).Apply(visitor);
+      }
+      else
+      {
+        visitor.RemoveRelationships(toModify);
+      }
+    }
+
+    // (8) New in Orthanc 1.9.4: Apply modifications to subsequences
+    for (ListOfPaths::const_iterator it = removeSequences_.begin();
+         it != removeSequences_.end(); ++it)
+    {
+      assert(it->GetPrefixLength() > 0);
+      toModify.RemovePath(*it);
+    }
+
+    for (SequenceReplacements::const_iterator it = sequenceReplacements_.begin();
+         it != sequenceReplacements_.end(); ++it)
+    {
+      assert(*it != NULL);
+      assert((*it)->GetPath().GetPrefixLength() > 0);
+      toModify.ReplacePath((*it)->GetPath(), (*it)->GetValue(), true /* decode data URI scheme */,
+                           DicomReplaceMode_InsertIfAbsent, privateCreator_);
+    }
+  }
+
+  void DicomModification::SetAllowManualIdentifiers(bool check)
+  {
+    allowManualIdentifiers_ = check;
+  }
+
+  bool DicomModification::AreAllowManualIdentifiers() const
+  {
+    return allowManualIdentifiers_;
+  }
+
+
+  static bool IsDatabaseKey(const DicomTag& tag)
+  {
+    return (tag == DICOM_TAG_PATIENT_ID ||
+            tag == DICOM_TAG_STUDY_INSTANCE_UID ||
+            tag == DICOM_TAG_SERIES_INSTANCE_UID ||
+            tag == DICOM_TAG_SOP_INSTANCE_UID);
+  }
+
+
+  static void ParseListOfTags(DicomModification& target,
+                              const Json::Value& query,
+                              TagOperation operation,
+                              bool force)
+  {
+    if (!query.isArray())
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
+    {
+      if (query[i].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+      
+      std::string name = query[i].asString();
+
+      const DicomPath path(DicomPath::Parse(name));
+
+      if (path.GetPrefixLength() == 0 &&
+          !force &&
+          IsDatabaseKey(path.GetFinalTag()))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Marking tag \"" + name + "\" as to be " +
+                               (operation == TagOperation_Keep ? "kept" : "removed") +
+                               " requires the \"Force\" option to be set to true");
+      }
+
+      switch (operation)
+      {
+        case TagOperation_Keep:
+          target.Keep(path);
+          LOG(TRACE) << "Keep: " << name << " = " << path.Format();
+          break;
+
+        case TagOperation_Remove:
+          target.Remove(path);
+          LOG(TRACE) << "Remove: " << name << " = " << path.Format();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  static void ParseReplacements(DicomModification& target,
+                                const Json::Value& replacements,
+                                bool force)
+  {
+    if (!replacements.isObject())
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    Json::Value::Members members = replacements.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      const Json::Value& value = replacements[name];
+
+      const DicomPath path(DicomPath::Parse(name));
+
+      if (path.GetPrefixLength() == 0 &&
+          !force &&
+          IsDatabaseKey(path.GetFinalTag()))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Marking tag \"" + name + "\" as to be replaced " +
+                               "requires the \"Force\" option to be set to true");
+      }
+        
+      target.Replace(path, value, false /* not safe for anonymization */);
+
+      LOG(TRACE) << "Replace: " << name << " = " << path.Format() 
+                 << " by: " << value.toStyledString();
+    }
+  }
+
+
+  static bool GetBooleanValue(const std::string& member,
+                              const Json::Value& json,
+                              bool defaultValue)
+  {
+    if (!json.isMember(member))
+    {
+      return defaultValue;
+    }
+    else if (json[member].type() == Json::booleanValue)
+    {
+      return json[member].asBool();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Member \"" + member + "\" should be a Boolean value");
+    }
+  }
+
+
+  void DicomModification::ParseModifyRequest(const Json::Value& request)
+  {
+    if (!request.isObject())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    bool force = GetBooleanValue("Force", request, false);
+      
+    if (GetBooleanValue("RemovePrivateTags", request, false))
+    {
+      SetRemovePrivateTags(true);
+    }
+
+    if (GetBooleanValue("KeepLabels", request, false))
+    {
+      SetKeepLabels(true);
+    }
+
+    if (request.isMember("Remove"))
+    {
+      ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
+    }
+
+    if (request.isMember("Replace"))
+    {
+      ParseReplacements(*this, request["Replace"], force);
+    }
+
+    // The "Keep" operation only makes sense for the tags
+    // StudyInstanceUID, SeriesInstanceUID and SOPInstanceUID. Avoid
+    // this feature as much as possible, as this breaks the DICOM
+    // model of the real world, except if you know exactly what
+    // you're doing!
+    if (request.isMember("Keep"))
+    {
+      ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force);
+    }
+
+    // New in Orthanc 1.6.0
+    if (request.isMember("PrivateCreator"))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
+    }
+
+    if (!force)
+    {
+      /**
+       * Sanity checks about the manual replacement of DICOM
+       * identifiers. Those checks were part of
+       * "DicomModification::Apply()" in Orthanc <= 1.11.2, and
+       * couldn't be disabled even if using the "Force" flag. Check
+       * out:
+       * https://groups.google.com/g/orthanc-users/c/xMUUZAnBa5g/m/WCEu-U2NBQAJ
+       **/
+      bool isReplacedPatientId = (IsReplaced(DICOM_TAG_PATIENT_ID) ||
+                                  uids_.find(DICOM_TAG_PATIENT_ID) != uids_.end());
+    
+      if (level_ == ResourceType_Patient && !isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, her PatientID is required to be modified.");
+      }
+
+      if (level_ == ResourceType_Study && isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the parent PatientID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Series && isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a series, the parent PatientID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a series, the parent StudyInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Instance && isReplacedPatientId)
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying an instance, the parent PatientID cannot be manually modified");
+      }
+      
+      if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying an instance, the parent StudyInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified");
+      }
+    }
+  }
+
+
+  void DicomModification::ParseAnonymizationRequest(bool& patientNameOverridden,
+                                                    const Json::Value& request)
+  {
+    if (!request.isObject())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    bool force = GetBooleanValue("Force", request, false);
+      
+    // DicomVersion version = DicomVersion_2008;   // For Orthanc <= 1.2.0
+    // DicomVersion version = DicomVersion_2017c;  // For Orthanc between 1.3.0 and 1.9.3
+    // DicomVersion version = DicomVersion_2021b;  // For Orthanc >= 1.9.4
+    DicomVersion version = DicomVersion_2023b;     // For Orthanc >= 1.12.1
+    
+    if (request.isMember("DicomVersion"))
+    {
+      if (request["DicomVersion"].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        version = StringToDicomVersion(request["DicomVersion"].asString());
+      }
+    }
+        
+    SetupAnonymization(version);
+
+    if (GetBooleanValue("KeepPrivateTags", request, false))
+    {
+      SetRemovePrivateTags(false);
+    }
+
+    if (GetBooleanValue("KeepLabels", request, false))
+    {
+      SetKeepLabels(true);
+    }
+
+    if (request.isMember("Remove"))
+    {
+      ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force);
+    }
+
+    if (request.isMember("Replace"))
+    {
+      ParseReplacements(*this, request["Replace"], force);
+    }
+
+    if (request.isMember("Keep"))
+    {
+      ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force);
+    }
+
+    patientNameOverridden = (uids_.find(DICOM_TAG_PATIENT_NAME) == uids_.end());
+    
+    // New in Orthanc 1.6.0
+    if (request.isMember("PrivateCreator"))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
+    }
+  }
+
+  void DicomModification::SetDicomIdentifierGenerator(DicomModification::IDicomIdentifierGenerator &generator)
+  {
+    identifierGenerator_ = &generator;
+  }
+
+
+
+
+  static const char* REMOVE_PRIVATE_TAGS = "RemovePrivateTags";
+  static const char* LEVEL = "Level";
+  static const char* ALLOW_MANUAL_IDENTIFIERS = "AllowManualIdentifiers";
+  static const char* KEEP_STUDY_INSTANCE_UID = "KeepStudyInstanceUID";
+  static const char* KEEP_SERIES_INSTANCE_UID = "KeepSeriesInstanceUID";
+  static const char* KEEP_SOP_INSTANCE_UID = "KeepSOPInstanceUID";
+  static const char* UPDATE_REFERENCED_RELATIONSHIPS = "UpdateReferencedRelationships";
+  static const char* IS_ANONYMIZATION = "IsAnonymization";
+  static const char* REMOVALS = "Removals";
+  static const char* CLEARINGS = "Clearings";
+  static const char* PRIVATE_TAGS_TO_KEEP = "PrivateTagsToKeep";
+  static const char* REPLACEMENTS = "Replacements";
+  static const char* MAP_PATIENTS = "MapPatients";
+  static const char* MAP_STUDIES = "MapStudies";
+  static const char* MAP_SERIES = "MapSeries";
+  static const char* MAP_INSTANCES = "MapInstances";
+  static const char* PRIVATE_CREATOR = "PrivateCreator";    // New in Orthanc 1.6.0
+  static const char* UIDS = "Uids";                         // New in Orthanc 1.9.4
+  static const char* REMOVED_RANGES = "RemovedRanges";      // New in Orthanc 1.9.4
+  static const char* KEEP_SEQUENCES = "KeepSequences";      // New in Orthanc 1.9.4
+  static const char* REMOVE_SEQUENCES = "RemoveSequences";  // New in Orthanc 1.9.4
+  static const char* SEQUENCE_REPLACEMENTS = "SequenceReplacements";  // New in Orthanc 1.9.4
+  
+  void DicomModification::Serialize(Json::Value& value) const
+  {
+    if (identifierGenerator_ != NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot serialize a DicomModification with a custom identifier generator");
+    }
+
+    value = Json::objectValue;
+    value[REMOVE_PRIVATE_TAGS] = removePrivateTags_;
+    value[LEVEL] = EnumerationToString(level_);
+    value[ALLOW_MANUAL_IDENTIFIERS] = allowManualIdentifiers_;
+    value[KEEP_STUDY_INSTANCE_UID] = keepStudyInstanceUid_;
+    value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_;
+    value[KEEP_SOP_INSTANCE_UID] = keepSopInstanceUid_;
+    value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_;
+    value[IS_ANONYMIZATION] = isAnonymization_;
+    value[PRIVATE_CREATOR] = privateCreator_;
+
+    SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS);
+    SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS);
+    SerializationToolbox::WriteSetOfTags(value, privateTagsToKeep_, PRIVATE_TAGS_TO_KEEP);
+
+    Json::Value& tmp = value[REPLACEMENTS];
+
+    tmp = Json::objectValue;
+
+    for (Replacements::const_iterator it = replacements_.begin();
+         it != replacements_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      tmp[it->first.Format()] = *it->second;
+    }
+
+    Json::Value& mapPatients = value[MAP_PATIENTS];
+    Json::Value& mapStudies = value[MAP_STUDIES];
+    Json::Value& mapSeries = value[MAP_SERIES];
+    Json::Value& mapInstances = value[MAP_INSTANCES];
+
+    mapPatients = Json::objectValue;
+    mapStudies = Json::objectValue;
+    mapSeries = Json::objectValue;
+    mapInstances = Json::objectValue;
+
+    for (UidMap::const_iterator it = uidMap_.begin(); it != uidMap_.end(); ++it)
+    {
+      Json::Value* tmp2 = NULL;
+
+      switch (it->first.first)
+      {
+        case ResourceType_Patient:
+          tmp2 = &mapPatients;
+          break;
+
+        case ResourceType_Study:
+          tmp2 = &mapStudies;
+          break;
+
+        case ResourceType_Series:
+          tmp2 = &mapSeries;
+          break;
+
+        case ResourceType_Instance:
+          tmp2 = &mapInstances;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      assert(tmp2 != NULL);
+      (*tmp2) [it->first.second] = it->second;
+    }
+
+    // New in Orthanc 1.9.4
+    SerializationToolbox::WriteSetOfTags(value, uids_, UIDS);
+
+    // New in Orthanc 1.9.4
+    Json::Value ranges = Json::arrayValue;
+      
+    for (RemovedRanges::const_iterator it = removedRanges_.begin(); it != removedRanges_.end(); ++it)
+    {
+      Json::Value item = Json::arrayValue;
+      item.append(it->GetGroupFrom());
+      item.append(it->GetGroupTo());
+      item.append(it->GetElementFrom());
+      item.append(it->GetElementTo());
+      ranges.append(item);
+    }
+
+    value[REMOVED_RANGES] = ranges;
+
+    // New in Orthanc 1.9.4
+    Json::Value lst = Json::arrayValue;
+    for (ListOfPaths::const_iterator it = keepSequences_.begin(); it != keepSequences_.end(); ++it)
+    {
+      lst.append(it->Format());
+    }
+
+    value[KEEP_SEQUENCES] = lst;
+
+    // New in Orthanc 1.9.4
+    lst = Json::arrayValue;
+    for (ListOfPaths::const_iterator it = removeSequences_.begin(); it != removeSequences_.end(); ++it)
+    {
+      assert(it->GetPrefixLength() > 0);
+      lst.append(it->Format());
+    }
+
+    value[REMOVE_SEQUENCES] = lst;
+
+    // New in Orthanc 1.9.4
+    lst = Json::objectValue;
+    for (SequenceReplacements::const_iterator it = sequenceReplacements_.begin(); it != sequenceReplacements_.end(); ++it)
+    {
+      assert(*it != NULL);
+      assert((*it)->GetPath().GetPrefixLength() > 0);
+      lst[(*it)->GetPath().Format()] = (*it)->GetValue();
+    }
+
+    value[SEQUENCE_REPLACEMENTS] = lst;
+  }
+
+  void DicomModification::UnserializeUidMap(ResourceType level,
+                                            const Json::Value& serialized,
+                                            const char* field)
+  {
+    if (!serialized.isMember(field) ||
+        serialized[field].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members names = serialized[field].getMemberNames();
+    
+    for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
+    {
+      const Json::Value& value = serialized[field][*it];
+
+      if (value.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        uidMap_[std::make_pair(level, *it)] = value.asString();
+      }
+    }
+  }
+
+  
+  DicomModification::DicomModification(const Json::Value& serialized) :
+    identifierGenerator_(NULL)
+  {
+    removePrivateTags_ = SerializationToolbox::ReadBoolean(serialized, REMOVE_PRIVATE_TAGS);
+    level_ = StringToResourceType(SerializationToolbox::ReadString(serialized, LEVEL).c_str());
+    allowManualIdentifiers_ = SerializationToolbox::ReadBoolean(serialized, ALLOW_MANUAL_IDENTIFIERS);
+    keepStudyInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_STUDY_INSTANCE_UID);
+    keepSeriesInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SERIES_INSTANCE_UID);
+    updateReferencedRelationships_ = SerializationToolbox::ReadBoolean
+      (serialized, UPDATE_REFERENCED_RELATIONSHIPS);
+    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
+
+    if (serialized.isMember(KEEP_SOP_INSTANCE_UID))
+    {
+      keepSopInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOP_INSTANCE_UID);
+    }
+    else
+    {
+      /**
+       * Compatibility with jobs serialized using Orthanc between
+       * 1.5.0 and 1.6.1. This compatibility was broken between 1.7.0
+       * and 1.9.3: Indeed, an exception was thrown in "ReadBoolean()"
+       * if "KEEP_SOP_INSTANCE_UID" was absent, because of changeset:
+       * https://orthanc.uclouvain.be/hg/orthanc/rev/3860
+       **/
+      keepSopInstanceUid_ = false;
+    }
+
+    if (serialized.isMember(PRIVATE_CREATOR))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(serialized, PRIVATE_CREATOR);
+    }
+
+    SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
+    SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS);
+    SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP);
+
+    if (!serialized.isMember(REPLACEMENTS) ||
+        serialized[REPLACEMENTS].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members names = serialized[REPLACEMENTS].getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
+    {
+      DicomTag tag(0, 0);
+      if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        const Json::Value& value = serialized[REPLACEMENTS][*it];
+        replacements_.insert(std::make_pair(tag, new Json::Value(value)));
+      }
+    }
+
+    UnserializeUidMap(ResourceType_Patient, serialized, MAP_PATIENTS);
+    UnserializeUidMap(ResourceType_Study, serialized, MAP_STUDIES);
+    UnserializeUidMap(ResourceType_Series, serialized, MAP_SERIES);
+    UnserializeUidMap(ResourceType_Instance, serialized, MAP_INSTANCES);
+
+    // New in Orthanc 1.9.4
+    if (serialized.isMember(UIDS))  // Backward compatibility with Orthanc <= 1.9.3
+    {
+      SerializationToolbox::ReadSetOfTags(uids_, serialized, UIDS);
+    }
+    else
+    {
+      SetupUidsFromOrthanc_1_9_3();
+    }
+
+    // New in Orthanc 1.9.4
+    removedRanges_.clear();
+    if (serialized.isMember(REMOVED_RANGES))  // Backward compatibility with Orthanc <= 1.9.3
+    {
+      const Json::Value& ranges = serialized[REMOVED_RANGES];
+      
+      if (ranges.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        for (Json::Value::ArrayIndex i = 0; i < ranges.size(); i++)
+        {
+          if (ranges[i].type() != Json::arrayValue ||
+              ranges[i].size() != 4 ||
+              !ranges[i][0].isUInt() ||
+              !ranges[i][1].isUInt() ||
+              !ranges[i][2].isUInt() ||
+              !ranges[i][3].isUInt())
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            Json::LargestUInt groupFrom = ranges[i][0].asUInt();
+            Json::LargestUInt groupTo = ranges[i][1].asUInt();
+            Json::LargestUInt elementFrom = ranges[i][2].asUInt();
+            Json::LargestUInt elementTo = ranges[i][3].asUInt();
+
+            if (groupFrom > groupTo ||
+                elementFrom > elementTo ||
+                groupTo > 0xffffu ||
+                elementTo > 0xffffu)
+            {
+              throw OrthancException(ErrorCode_BadFileFormat);
+            }
+            else
+            {
+              removedRanges_.push_back(DicomTagRange(groupFrom, groupTo, elementFrom, elementTo));
+            }
+          }
+        }
+      }
+    }
+
+    // New in Orthanc 1.9.4
+    if (serialized.isMember(KEEP_SEQUENCES))
+    {
+      const Json::Value& keep = serialized[KEEP_SEQUENCES];
+      
+      if (keep.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        for (Json::Value::ArrayIndex i = 0; i < keep.size(); i++)
+        {
+          if (keep[i].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            keepSequences_.push_back(DicomPath::Parse(keep[i].asString()));
+          }
+        }
+      }
+    }
+
+    // New in Orthanc 1.9.4
+    if (serialized.isMember(REMOVE_SEQUENCES))
+    {
+      const Json::Value& remove = serialized[REMOVE_SEQUENCES];
+      
+      if (remove.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        for (Json::Value::ArrayIndex i = 0; i < remove.size(); i++)
+        {
+          if (remove[i].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+          else
+          {
+            removeSequences_.push_back(DicomPath::Parse(remove[i].asString()));
+          }
+        }
+      }
+    }
+
+    // New in Orthanc 1.9.4
+    if (serialized.isMember(SEQUENCE_REPLACEMENTS))
+    {
+      const Json::Value& replace = serialized[SEQUENCE_REPLACEMENTS];
+      
+      if (replace.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        Json::Value::Members members = replace.getMemberNames();
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          sequenceReplacements_.push_back(
+            new SequenceReplacement(DicomPath::Parse(members[i]), replace[members[i]]));
+        }
+      }
+    }
+  }
+
+
+  void DicomModification::SetPrivateCreator(const std::string &privateCreator)
+  {
+    privateCreator_ = privateCreator;
+  }
+
+  const std::string &DicomModification::GetPrivateCreator() const
+  {
+    return privateCreator_;
+  }
+
+
+  void DicomModification::Keep(const DicomPath& path)
+  {
+    if (path.GetPrefixLength() == 0)
+    {
+      Keep(path.GetFinalTag());
+    }
+
+    keepSequences_.push_back(path);
+    MarkNotOrthancAnonymization();
+  }
+  
+
+  void DicomModification::Remove(const DicomPath& path)
+  {
+    if (path.GetPrefixLength() == 0)
+    {
+      Remove(path.GetFinalTag());
+    }
+    else
+    {
+      removeSequences_.push_back(path);
+      MarkNotOrthancAnonymization();
+    }
+  }
+  
+
+  void DicomModification::Replace(const DicomPath& path,
+                                  const Json::Value& value,
+                                  bool safeForAnonymization)
+  {
+    if (path.GetPrefixLength() == 0)
+    {
+      Replace(path.GetFinalTag(), value, safeForAnonymization);
+    }
+    else
+    {
+      sequenceReplacements_.push_back(new SequenceReplacement(path, value));
+
+      if (!safeForAnonymization)
+      {
+        MarkNotOrthancAnonymization();
+      }
+    }
+  }
+
+
+  bool DicomModification::IsAlteredTag(const DicomTag& tag) const
+  {
+    return (uids_.find(tag) != uids_.end() ||
+            IsCleared(tag) ||
+            IsRemoved(tag) ||
+            IsReplaced(tag) ||
+            (tag.IsPrivate() &&
+             ArePrivateTagsRemoved() &&
+             privateTagsToKeep_.find(tag) == privateTagsToKeep_.end()) ||
+            (isAnonymization_ && (
+              tag == DICOM_TAG_PATIENT_NAME ||
+              tag == DICOM_TAG_PATIENT_ID)) ||
+            (tag == DICOM_TAG_STUDY_INSTANCE_UID &&
+             !keepStudyInstanceUid_) ||
+            (tag == DICOM_TAG_SERIES_INSTANCE_UID &&
+             !keepSeriesInstanceUid_) ||
+            (tag == DICOM_TAG_SOP_INSTANCE_UID &&
+             !keepSopInstanceUid_));
+  }
+
+  void DicomModification::GetReplacedTags(std::set& target) const
+  {
+    target.clear();
+    for (Replacements::const_iterator it = replacements_.begin(); it != replacements_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DicomModification.h b/OrthancFramework/Sources/DicomParsing/DicomModification.h
new file mode 100644
index 0000000..6f057ef
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h
@@ -0,0 +1,272 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "ParsedDicomFile.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomModification : public boost::noncopyable
+  {
+    /**
+     * Process:
+     * (1) Remove private tags
+     * (2) Remove tags specified by the user
+     * (3) Replace tags
+     **/
+
+  public:
+    class IDicomIdentifierGenerator : public boost::noncopyable
+    {
+    public:
+      virtual ~IDicomIdentifierGenerator()
+      {
+      }
+
+      virtual bool Apply(std::string& target,
+                         const std::string& sourceIdentifier,
+                         ResourceType level,
+                         const DicomMap& sourceDicom) = 0;                       
+    };
+
+  private:
+    class RelationshipsVisitor;
+
+    class DicomTagRange
+    {
+    private:
+      uint16_t   groupFrom_;
+      uint16_t   groupTo_;
+      uint16_t   elementFrom_;
+      uint16_t   elementTo_;
+
+    public:
+      DicomTagRange(uint16_t groupFrom,
+                    uint16_t groupTo,
+                    uint16_t elementFrom,
+                    uint16_t elementTo);
+
+      uint16_t GetGroupFrom() const
+      {
+        return groupFrom_;
+      }
+
+      uint16_t GetGroupTo() const
+      {
+        return groupTo_;
+      }
+
+      uint16_t GetElementFrom() const
+      {
+        return elementFrom_;
+      }
+
+      uint16_t GetElementTo() const
+      {
+        return elementTo_;
+      }
+
+      bool Contains(const DicomTag& tag) const;
+    };
+
+    class SequenceReplacement : public boost::noncopyable
+    {
+    private:
+      DicomPath    path_;
+      Json::Value  value_;
+
+    public:
+      SequenceReplacement(const DicomPath& path,
+                          const Json::Value& value) :
+        path_(path),
+        value_(value)
+      {
+      }
+
+      const DicomPath& GetPath() const
+      {
+        return path_;
+      }
+
+      const Json::Value& GetValue() const
+      {
+        return value_;
+      }
+    };
+    
+    typedef std::set                SetOfTags;
+    typedef std::map  Replacements;
+    typedef std::list          RemovedRanges;
+    typedef std::list              ListOfPaths;
+    typedef std::list   SequenceReplacements;
+
+    typedef std::map< std::pair, std::string>  UidMap;
+    
+    SetOfTags removals_;
+    SetOfTags clearings_;
+    SetOfTags keep_;
+    Replacements replacements_;
+    bool removePrivateTags_;
+    bool keepLabels_;
+    ResourceType level_;
+    UidMap uidMap_;
+    SetOfTags privateTagsToKeep_;
+    bool allowManualIdentifiers_;
+    bool keepStudyInstanceUid_;
+    bool keepSeriesInstanceUid_;
+    bool keepSopInstanceUid_;
+    bool updateReferencedRelationships_;
+    bool isAnonymization_;
+    DicomMap currentSource_;
+    std::string privateCreator_;
+
+    IDicomIdentifierGenerator* identifierGenerator_;
+
+    // New in Orthanc 1.9.4
+    SetOfTags            uids_;
+    RemovedRanges        removedRanges_;
+    ListOfPaths          keepSequences_;         // Can *possibly* be a path whose prefix is empty
+    ListOfPaths          removeSequences_;       // Must *never* be a path whose prefix is empty
+    SequenceReplacements sequenceReplacements_;  // Must *never* be a path whose prefix is empty
+
+    std::string MapDicomIdentifier(const std::string& original,
+                                   ResourceType level);
+
+    void RegisterMappedDicomIdentifier(const std::string& original,
+                                       const std::string& mapped,
+                                       ResourceType level);
+
+    void MapDicomTags(ParsedDicomFile& dicom,
+                      ResourceType level);
+
+    void MarkNotOrthancAnonymization();
+
+    void ClearReplacements();
+
+    void CancelReplacement(const DicomTag& tag);
+
+    void ReplaceInternal(const DicomTag& tag,
+                         const Json::Value& value);
+
+    void SetupUidsFromOrthanc_1_9_3();
+
+    void SetupAnonymization2008();
+
+    void SetupAnonymization2017c();
+
+    void SetupAnonymization2021b();
+
+    void SetupAnonymization2023b();
+
+    void UnserializeUidMap(ResourceType level,
+                           const Json::Value& serialized,
+                           const char* field);
+
+  public:
+    DicomModification();
+
+    explicit DicomModification(const Json::Value& serialized);
+
+    ~DicomModification();
+
+    void Keep(const DicomTag& tag);
+
+    void Remove(const DicomTag& tag);
+
+    // Replace the DICOM tag as a NULL/empty value (e.g. for anonymization)
+    void Clear(const DicomTag& tag);
+
+    bool IsRemoved(const DicomTag& tag) const;
+
+    bool IsCleared(const DicomTag& tag) const;
+
+    // "safeForAnonymization" tells Orthanc that this replacement does
+    // not break the anonymization process it implements (for internal use only)
+    void Replace(const DicomTag& tag,
+                 const Json::Value& value,   // Encoded using UTF-8
+                 bool safeForAnonymization);
+
+    bool IsReplaced(const DicomTag& tag) const;
+
+    void GetReplacedTags(std::set& target) const;
+
+    const Json::Value& GetReplacement(const DicomTag& tag) const;
+
+    std::string GetReplacementAsString(const DicomTag& tag) const;
+
+    bool IsKept(const DicomTag& tag) const;
+
+    void SetRemovePrivateTags(bool removed);
+
+    bool ArePrivateTagsRemoved() const;
+
+    void SetKeepLabels(bool keep);
+
+    bool AreLabelsKept() const;
+
+    void SetLevel(ResourceType level);
+
+    ResourceType GetLevel() const;
+
+    void SetupAnonymization(DicomVersion version);
+
+    void Apply(ParsedDicomFile& toModify);
+
+    void SetAllowManualIdentifiers(bool check);
+
+    bool AreAllowManualIdentifiers() const;
+
+    void ParseModifyRequest(const Json::Value& request);
+
+    // "patientNameOverridden" is set to "true" iff. the PatientName
+    // (0010,0010) tag is manually replaced, removed, cleared or kept
+    void ParseAnonymizationRequest(bool& patientNameOverridden /* out */,
+                                   const Json::Value& request);
+
+    void SetDicomIdentifierGenerator(IDicomIdentifierGenerator& generator);
+
+    void Serialize(Json::Value& value) const;
+
+    void SetPrivateCreator(const std::string& privateCreator);
+
+    const std::string& GetPrivateCreator() const;
+
+    // New in Orthanc 1.9.4
+    void Keep(const DicomPath& path);
+
+    // New in Orthanc 1.9.4
+    void Remove(const DicomPath& path);
+
+    // New in Orthanc 1.9.4
+    void Replace(const DicomPath& path,
+                 const Json::Value& value,   // Encoded using UTF-8
+                 bool safeForAnonymization);
+
+    bool IsAlteredTag(const DicomTag& tag) const;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2017c.impl.h b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2017c.impl.h
new file mode 100644
index 0000000..cba7527
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2017c.impl.h
@@ -0,0 +1,277 @@
+// RelationshipsVisitor handles (0x0008, 0x1140)  /* X/Z/U* */   // Referenced Image Sequence
+// RelationshipsVisitor handles (0x0008, 0x2112)  /* X/Z/U* */   // Source Image Sequence
+// Tag (0x0008, 0x0018) is set in Apply()         /* U */        // SOP Instance UID
+// Tag (0x0010, 0x0010) is set below (*)          /* Z */        // Patient's Name
+// Tag (0x0010, 0x0020) is set below (*)          /* Z */        // Patient ID
+// Tag (0x0020, 0x000d) is set in Apply()         /* U */        // Study Instance UID
+// Tag (0x0020, 0x000e) is set in Apply()         /* U */        // Series Instance UID
+clearings_.insert(DicomTag(0x0008, 0x0020));                     // Study Date
+clearings_.insert(DicomTag(0x0008, 0x0023));  /* Z/D */          // Content Date
+clearings_.insert(DicomTag(0x0008, 0x0030));                     // Study Time
+clearings_.insert(DicomTag(0x0008, 0x0033));  /* Z/D */          // Content Time
+clearings_.insert(DicomTag(0x0008, 0x0050));                     // Accession Number
+clearings_.insert(DicomTag(0x0008, 0x0090));                     // Referring Physician's Name
+clearings_.insert(DicomTag(0x0008, 0x009c));                     // Consulting Physician's Name
+clearings_.insert(DicomTag(0x0010, 0x0030));                     // Patient's Birth Date
+clearings_.insert(DicomTag(0x0010, 0x0040));                     // Patient's Sex
+clearings_.insert(DicomTag(0x0018, 0x0010));  /* Z/D */          // Contrast Bolus Agent
+clearings_.insert(DicomTag(0x0020, 0x0010));                     // Study ID
+clearings_.insert(DicomTag(0x0040, 0x1101));  /* D */            // Person Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0x2016));                     // Placer Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0x2017));                     // Filler Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0xa073));  /* D */            // Verifying Observer Sequence
+clearings_.insert(DicomTag(0x0040, 0xa075));  /* D */            // Verifying Observer Name
+clearings_.insert(DicomTag(0x0040, 0xa088));                     // Verifying Observer Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0xa123));  /* D */            // Person Name
+clearings_.insert(DicomTag(0x0070, 0x0001));  /* D */            // Graphic Annotation Sequence
+clearings_.insert(DicomTag(0x0070, 0x0084));                     // Content Creator's Name
+removals_.insert(DicomTag(0x0000, 0x1000));                      // Affected SOP Instance UID
+removals_.insert(DicomTag(0x0008, 0x0015));                      // Instance Coercion DateTime
+removals_.insert(DicomTag(0x0008, 0x0021));   /* X/D */          // Series Date
+removals_.insert(DicomTag(0x0008, 0x0022));   /* X/Z */          // Acquisition Date
+removals_.insert(DicomTag(0x0008, 0x0024));                      // Overlay Date
+removals_.insert(DicomTag(0x0008, 0x0025));                      // Curve Date
+removals_.insert(DicomTag(0x0008, 0x002a));   /* X/D */          // Acquisition DateTime
+removals_.insert(DicomTag(0x0008, 0x0031));   /* X/D */          // Series Time
+removals_.insert(DicomTag(0x0008, 0x0032));   /* X/Z */          // Acquisition Time
+removals_.insert(DicomTag(0x0008, 0x0034));                      // Overlay Time
+removals_.insert(DicomTag(0x0008, 0x0035));                      // Curve Time
+removals_.insert(DicomTag(0x0008, 0x0080));   /* X/Z/D */        // Institution Name
+removals_.insert(DicomTag(0x0008, 0x0081));                      // Institution Address
+removals_.insert(DicomTag(0x0008, 0x0082));   /* X/Z/D */        // Institution Code Sequence
+removals_.insert(DicomTag(0x0008, 0x0092));                      // Referring Physician's Address
+removals_.insert(DicomTag(0x0008, 0x0094));                      // Referring Physician's Telephone Numbers
+removals_.insert(DicomTag(0x0008, 0x0096));                      // Referring Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x009d));                      // Consulting Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x0201));                      // Timezone Offset From UTC
+removals_.insert(DicomTag(0x0008, 0x1010));   /* X/Z/D */        // Station Name
+removals_.insert(DicomTag(0x0008, 0x1030));                      // Study Description
+removals_.insert(DicomTag(0x0008, 0x103e));                      // Series Description
+removals_.insert(DicomTag(0x0008, 0x1040));                      // Institutional Department Name
+removals_.insert(DicomTag(0x0008, 0x1048));                      // Physician(s) of Record
+removals_.insert(DicomTag(0x0008, 0x1049));                      // Physician(s) of Record Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1050));                      // Performing Physicians' Name
+removals_.insert(DicomTag(0x0008, 0x1052));                      // Performing Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1060));                      // Name of Physician(s) Reading Study
+removals_.insert(DicomTag(0x0008, 0x1062));                      // Physician(s) Reading Study Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1070));   /* X/Z/D */        // Operators' Name
+removals_.insert(DicomTag(0x0008, 0x1072));   /* X/D */          // Operators' Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1080));                      // Admitting Diagnoses Description
+removals_.insert(DicomTag(0x0008, 0x1084));                      // Admitting Diagnoses Code Sequence
+removals_.insert(DicomTag(0x0008, 0x1110));   /* X/Z */          // Referenced Study Sequence
+removals_.insert(DicomTag(0x0008, 0x1111));   /* X/Z/D */        // Referenced Performed Procedure Step Sequence
+removals_.insert(DicomTag(0x0008, 0x1120));                      // Referenced Patient Sequence
+removals_.insert(DicomTag(0x0008, 0x2111));                      // Derivation Description
+removals_.insert(DicomTag(0x0008, 0x4000));                      // Identifying Comments
+removals_.insert(DicomTag(0x0010, 0x0021));                      // Issuer of Patient ID
+removals_.insert(DicomTag(0x0010, 0x0032));                      // Patient's Birth Time
+removals_.insert(DicomTag(0x0010, 0x0050));                      // Patient's Insurance Plan Code Sequence
+removals_.insert(DicomTag(0x0010, 0x0101));                      // Patient's Primary Language Code Sequence
+removals_.insert(DicomTag(0x0010, 0x0102));                      // Patient's Primary Language Modifier Code Sequence
+removals_.insert(DicomTag(0x0010, 0x1000));                      // Other Patient IDs
+removals_.insert(DicomTag(0x0010, 0x1001));                      // Other Patient Names
+removals_.insert(DicomTag(0x0010, 0x1002));                      // Other Patient IDs Sequence
+removals_.insert(DicomTag(0x0010, 0x1005));                      // Patient's Birth Name
+removals_.insert(DicomTag(0x0010, 0x1010));                      // Patient's Age
+removals_.insert(DicomTag(0x0010, 0x1020));                      // Patient's Size
+removals_.insert(DicomTag(0x0010, 0x1030));                      // Patient's Weight
+removals_.insert(DicomTag(0x0010, 0x1040));                      // Patient Address
+removals_.insert(DicomTag(0x0010, 0x1050));                      // Insurance Plan Identification
+removals_.insert(DicomTag(0x0010, 0x1060));                      // Patient's Mother's Birth Name
+removals_.insert(DicomTag(0x0010, 0x1080));                      // Military Rank
+removals_.insert(DicomTag(0x0010, 0x1081));                      // Branch of Service
+removals_.insert(DicomTag(0x0010, 0x1090));                      // Medical Record Locator
+removals_.insert(DicomTag(0x0010, 0x1100));                      // Referenced Patient Photo Sequence
+removals_.insert(DicomTag(0x0010, 0x2000));                      // Medical Alerts
+removals_.insert(DicomTag(0x0010, 0x2110));                      // Allergies
+removals_.insert(DicomTag(0x0010, 0x2150));                      // Country of Residence
+removals_.insert(DicomTag(0x0010, 0x2152));                      // Region of Residence
+removals_.insert(DicomTag(0x0010, 0x2154));                      // Patient's Telephone Numbers
+removals_.insert(DicomTag(0x0010, 0x2155));                      // Patient's Telecom Information
+removals_.insert(DicomTag(0x0010, 0x2160));                      // Ethnic Group
+removals_.insert(DicomTag(0x0010, 0x2180));                      // Occupation
+removals_.insert(DicomTag(0x0010, 0x21a0));                      // Smoking Status
+removals_.insert(DicomTag(0x0010, 0x21b0));                      // Additional Patient's History
+removals_.insert(DicomTag(0x0010, 0x21c0));                      // Pregnancy Status
+removals_.insert(DicomTag(0x0010, 0x21d0));                      // Last Menstrual Date
+removals_.insert(DicomTag(0x0010, 0x21f0));                      // Patient's Religious Preference
+removals_.insert(DicomTag(0x0010, 0x2203));   /* X/Z */          // Patient Sex Neutered
+removals_.insert(DicomTag(0x0010, 0x2297));                      // Responsible Person
+removals_.insert(DicomTag(0x0010, 0x2299));                      // Responsible Organization
+removals_.insert(DicomTag(0x0010, 0x4000));                      // Patient Comments
+removals_.insert(DicomTag(0x0018, 0x1000));   /* X/Z/D */        // Device Serial Number
+removals_.insert(DicomTag(0x0018, 0x1004));                      // Plate ID
+removals_.insert(DicomTag(0x0018, 0x1005));                      // Generator ID
+removals_.insert(DicomTag(0x0018, 0x1007));                      // Cassette ID
+removals_.insert(DicomTag(0x0018, 0x1008));                      // Gantry ID
+removals_.insert(DicomTag(0x0018, 0x1030));   /* X/D */          // Protocol Name
+removals_.insert(DicomTag(0x0018, 0x1400));   /* X/D */          // Acquisition Device Processing Description
+removals_.insert(DicomTag(0x0018, 0x4000));                      // Acquisition Comments
+removals_.insert(DicomTag(0x0018, 0x700a));   /* X/D */          // Detector ID
+removals_.insert(DicomTag(0x0018, 0x9424));                      // Acquisition Protocol Description
+removals_.insert(DicomTag(0x0018, 0x9516));   /* X/D */          // Start Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0x9517));   /* X/D */          // End Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0xa003));                      // Contribution Description
+removals_.insert(DicomTag(0x0020, 0x3401));                      // Modifying Device ID
+removals_.insert(DicomTag(0x0020, 0x3404));                      // Modifying Device Manufacturer
+removals_.insert(DicomTag(0x0020, 0x3406));                      // Modified Image Description
+removals_.insert(DicomTag(0x0020, 0x4000));                      // Image Comments
+removals_.insert(DicomTag(0x0020, 0x9158));                      // Frame Comments
+removals_.insert(DicomTag(0x0028, 0x4000));                      // Image Presentation Comments
+removals_.insert(DicomTag(0x0032, 0x0012));                      // Study ID Issuer
+removals_.insert(DicomTag(0x0032, 0x1020));                      // Scheduled Study Location
+removals_.insert(DicomTag(0x0032, 0x1021));                      // Scheduled Study Location AE Title
+removals_.insert(DicomTag(0x0032, 0x1030));                      // Reason for Study
+removals_.insert(DicomTag(0x0032, 0x1032));                      // Requesting Physician
+removals_.insert(DicomTag(0x0032, 0x1033));                      // Requesting Service
+removals_.insert(DicomTag(0x0032, 0x1060));   /* X/Z */          // Requested Procedure Description
+removals_.insert(DicomTag(0x0032, 0x1070));                      // Requested Contrast Agent
+removals_.insert(DicomTag(0x0032, 0x4000));                      // Study Comments
+removals_.insert(DicomTag(0x0038, 0x0004));                      // Referenced Patient Alias Sequence
+removals_.insert(DicomTag(0x0038, 0x0010));                      // Admission ID
+removals_.insert(DicomTag(0x0038, 0x0011));                      // Issuer of Admission ID
+removals_.insert(DicomTag(0x0038, 0x001e));                      // Scheduled Patient Institution Residence
+removals_.insert(DicomTag(0x0038, 0x0020));                      // Admitting Date
+removals_.insert(DicomTag(0x0038, 0x0021));                      // Admitting Time
+removals_.insert(DicomTag(0x0038, 0x0040));                      // Discharge Diagnosis Description
+removals_.insert(DicomTag(0x0038, 0x0050));                      // Special Needs
+removals_.insert(DicomTag(0x0038, 0x0060));                      // Service Episode ID
+removals_.insert(DicomTag(0x0038, 0x0061));                      // Issuer of Service Episode ID
+removals_.insert(DicomTag(0x0038, 0x0062));                      // Service Episode Description
+removals_.insert(DicomTag(0x0038, 0x0300));                      // Current Patient Location
+removals_.insert(DicomTag(0x0038, 0x0400));                      // Patient's Institution Residence
+removals_.insert(DicomTag(0x0038, 0x0500));                      // Patient State
+removals_.insert(DicomTag(0x0038, 0x4000));                      // Visit Comments
+removals_.insert(DicomTag(0x0040, 0x0001));                      // Scheduled Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0002));                      // Scheduled Procedure Step Start Date
+removals_.insert(DicomTag(0x0040, 0x0003));                      // Scheduled Procedure Step Start Time
+removals_.insert(DicomTag(0x0040, 0x0004));                      // Scheduled Procedure Step End Date
+removals_.insert(DicomTag(0x0040, 0x0005));                      // Scheduled Procedure Step End Time
+removals_.insert(DicomTag(0x0040, 0x0006));                      // Scheduled Performing Physician Name
+removals_.insert(DicomTag(0x0040, 0x0007));                      // Scheduled Procedure Step Description
+removals_.insert(DicomTag(0x0040, 0x000b));                      // Scheduled Performing Physician Identification Sequence
+removals_.insert(DicomTag(0x0040, 0x0010));                      // Scheduled Station Name
+removals_.insert(DicomTag(0x0040, 0x0011));                      // Scheduled Procedure Step Location
+removals_.insert(DicomTag(0x0040, 0x0012));                      // Pre-Medication
+removals_.insert(DicomTag(0x0040, 0x0241));                      // Performed Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0242));                      // Performed Station Name
+removals_.insert(DicomTag(0x0040, 0x0243));                      // Performed Location
+removals_.insert(DicomTag(0x0040, 0x0244));                      // Performed Procedure Step Start Date
+removals_.insert(DicomTag(0x0040, 0x0245));                      // Performed Procedure Step Start Time
+removals_.insert(DicomTag(0x0040, 0x0250));                      // Performed Procedure Step End Date
+removals_.insert(DicomTag(0x0040, 0x0251));                      // Performed Procedure Step End Time
+removals_.insert(DicomTag(0x0040, 0x0253));                      // Performed Procedure Step ID
+removals_.insert(DicomTag(0x0040, 0x0254));                      // Performed Procedure Step Description
+removals_.insert(DicomTag(0x0040, 0x0275));                      // Request Attributes Sequence
+removals_.insert(DicomTag(0x0040, 0x0280));                      // Comments on the Performed Procedure Step
+removals_.insert(DicomTag(0x0040, 0x0555));                      // Acquisition Context Sequence
+removals_.insert(DicomTag(0x0040, 0x1001));                      // Requested Procedure ID
+removals_.insert(DicomTag(0x0040, 0x1004));                      // Patient Transport Arrangements
+removals_.insert(DicomTag(0x0040, 0x1005));                      // Requested Procedure Location
+removals_.insert(DicomTag(0x0040, 0x1010));                      // Names of Intended Recipient of Results
+removals_.insert(DicomTag(0x0040, 0x1011));                      // Intended Recipients of Results Identification Sequence
+removals_.insert(DicomTag(0x0040, 0x1102));                      // Person Address
+removals_.insert(DicomTag(0x0040, 0x1103));                      // Person's Telephone Numbers
+removals_.insert(DicomTag(0x0040, 0x1104));                      // Person's Telecom Information
+removals_.insert(DicomTag(0x0040, 0x1400));                      // Requested Procedure Comments
+removals_.insert(DicomTag(0x0040, 0x2001));                      // Reason for the Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2008));                      // Order Entered By
+removals_.insert(DicomTag(0x0040, 0x2009));                      // Order Enterer Location
+removals_.insert(DicomTag(0x0040, 0x2010));                      // Order Callback Phone Number
+removals_.insert(DicomTag(0x0040, 0x2011));                      // Order Callback Telecom Information
+removals_.insert(DicomTag(0x0040, 0x2400));                      // Imaging Service Request Comments
+removals_.insert(DicomTag(0x0040, 0x3001));                      // Confidentiality Constraint on Patient Data Description
+removals_.insert(DicomTag(0x0040, 0x4005));                      // Scheduled Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4010));                      // Scheduled Procedure Step Modification DateTime
+removals_.insert(DicomTag(0x0040, 0x4011));                      // Expected Completion DateTime
+removals_.insert(DicomTag(0x0040, 0x4025));                      // Scheduled Station Name Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4027));                      // Scheduled Station Geographic Location Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4028));                      // Performed Station Name Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4030));                      // Performed Station Geographic Location Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4034));                      // Scheduled Human Performers Sequence
+removals_.insert(DicomTag(0x0040, 0x4035));                      // Actual Human Performers Sequence
+removals_.insert(DicomTag(0x0040, 0x4036));                      // Human Performers Organization
+removals_.insert(DicomTag(0x0040, 0x4037));                      // Human Performers Name
+removals_.insert(DicomTag(0x0040, 0x4050));                      // Performed Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4051));                      // Performed Procedure Step End DateTime
+removals_.insert(DicomTag(0x0040, 0x4052));                      // Procedure Step Cancellation DateTime
+removals_.insert(DicomTag(0x0040, 0xa027));                      // Verifying Organization
+removals_.insert(DicomTag(0x0040, 0xa078));                      // Author Observer Sequence
+removals_.insert(DicomTag(0x0040, 0xa07a));                      // Participant Sequence
+removals_.insert(DicomTag(0x0040, 0xa07c));                      // Custodial Organization Sequence
+removals_.insert(DicomTag(0x0040, 0xa192));                      // Observation Date (Trial)
+removals_.insert(DicomTag(0x0040, 0xa193));                      // Observation Time (Trial)
+removals_.insert(DicomTag(0x0040, 0xa307));                      // Current Observer (Trial)
+removals_.insert(DicomTag(0x0040, 0xa352));                      // Verbal Source (Trial)
+removals_.insert(DicomTag(0x0040, 0xa353));                      // Address (Trial)
+removals_.insert(DicomTag(0x0040, 0xa354));                      // Telephone Number (Trial)
+removals_.insert(DicomTag(0x0040, 0xa358));                      // Verbal Source Identifier Code Sequence (Trial)
+removals_.insert(DicomTag(0x0040, 0xa730));                      // Content Sequence
+removals_.insert(DicomTag(0x0070, 0x0086));                      // Content Creator's Identification Code Sequence
+removals_.insert(DicomTag(0x0088, 0x0200));                      // Icon Image Sequence(see Note 12)
+removals_.insert(DicomTag(0x0088, 0x0904));                      // Topic Title
+removals_.insert(DicomTag(0x0088, 0x0906));                      // Topic Subject
+removals_.insert(DicomTag(0x0088, 0x0910));                      // Topic Author
+removals_.insert(DicomTag(0x0088, 0x0912));                      // Topic Keywords
+removals_.insert(DicomTag(0x0400, 0x0100));                      // Digital Signature UID
+removals_.insert(DicomTag(0x0400, 0x0402));                      // Referenced Digital Signature Sequence
+removals_.insert(DicomTag(0x0400, 0x0403));                      // Referenced SOP Instance MAC Sequence
+removals_.insert(DicomTag(0x0400, 0x0404));                      // MAC
+removals_.insert(DicomTag(0x0400, 0x0550));                      // Modified Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0561));                      // Original Attributes Sequence
+removals_.insert(DicomTag(0x2030, 0x0020));                      // Text String
+removals_.insert(DicomTag(0x3008, 0x0105));                      // Source Serial Number
+removals_.insert(DicomTag(0x300c, 0x0113));                      // Reason for Omission Description
+removals_.insert(DicomTag(0x300e, 0x0008));   /* X/Z */          // Reviewer Name
+removals_.insert(DicomTag(0x4000, 0x0010));                      // Arbitrary
+removals_.insert(DicomTag(0x4000, 0x4000));                      // Text Comments
+removals_.insert(DicomTag(0x4008, 0x0042));                      // Results ID Issuer
+removals_.insert(DicomTag(0x4008, 0x0102));                      // Interpretation Recorder
+removals_.insert(DicomTag(0x4008, 0x010a));                      // Interpretation Transcriber
+removals_.insert(DicomTag(0x4008, 0x010b));                      // Interpretation Text
+removals_.insert(DicomTag(0x4008, 0x010c));                      // Interpretation Author
+removals_.insert(DicomTag(0x4008, 0x0111));                      // Interpretation Approver Sequence
+removals_.insert(DicomTag(0x4008, 0x0114));                      // Physician Approving Interpretation
+removals_.insert(DicomTag(0x4008, 0x0115));                      // Interpretation Diagnosis Description
+removals_.insert(DicomTag(0x4008, 0x0118));                      // Results Distribution List Sequence
+removals_.insert(DicomTag(0x4008, 0x0119));                      // Distribution Name
+removals_.insert(DicomTag(0x4008, 0x011a));                      // Distribution Address
+removals_.insert(DicomTag(0x4008, 0x0202));                      // Interpretation ID Issuer
+removals_.insert(DicomTag(0x4008, 0x0300));                      // Impressions
+removals_.insert(DicomTag(0x4008, 0x4000));                      // Results Comments
+removals_.insert(DicomTag(0xfffa, 0xfffa));                      // Digital Signatures Sequence
+removals_.insert(DicomTag(0xfffc, 0xfffc));                      // Data Set Trailing Padding
+removedRanges_.push_back(DicomTagRange(0x5000, 0x50ff, 0x0000, 0xffff));  // Curve Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x3000, 0x3000));  // Overlay Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x4000, 0x4000));  // Overlay Comments
+uids_.insert(DicomTag(0x0000, 0x1001));                          // Requested SOP Instance UID
+uids_.insert(DicomTag(0x0002, 0x0003));                          // Media Storage SOP Instance UID
+uids_.insert(DicomTag(0x0004, 0x1511));                          // Referenced SOP Instance UID in File
+uids_.insert(DicomTag(0x0008, 0x0014));                          // Instance Creator UID
+uids_.insert(DicomTag(0x0008, 0x0058));                          // Failed SOP Instance UID List
+uids_.insert(DicomTag(0x0008, 0x1155));                          // Referenced SOP Instance UID
+uids_.insert(DicomTag(0x0008, 0x1195));                          // Transaction UID
+uids_.insert(DicomTag(0x0008, 0x3010));                          // Irradiation Event UID
+uids_.insert(DicomTag(0x0018, 0x1002));                          // Device UID
+uids_.insert(DicomTag(0x0018, 0x2042));                          // Target UID
+uids_.insert(DicomTag(0x0020, 0x0052));                          // Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x0200));                          // Synchronization Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x9161));                          // Concatenation UID
+uids_.insert(DicomTag(0x0020, 0x9164));                          // Dimension Organization UID
+uids_.insert(DicomTag(0x0028, 0x1199));                          // Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x0028, 0x1214));                          // Large Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x0040, 0x4023));                          // Referenced General Purpose Scheduled Procedure Step Transaction UID
+uids_.insert(DicomTag(0x0040, 0xa124));                          // UID
+uids_.insert(DicomTag(0x0040, 0xa171));                          // Observation UID
+uids_.insert(DicomTag(0x0040, 0xa172));                          // Referenced Observation UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xa402));                          // Observation Subject UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xdb0c));                          // Template Extension Organization UID
+uids_.insert(DicomTag(0x0040, 0xdb0d));                          // Template Extension Creator UID
+uids_.insert(DicomTag(0x0062, 0x0021));                          // Tracking UID
+uids_.insert(DicomTag(0x0070, 0x031a));                          // Fiducial UID
+uids_.insert(DicomTag(0x0070, 0x1101));                          // Presentation Display Collection UID
+uids_.insert(DicomTag(0x0070, 0x1102));                          // Presentation Sequence Collection UID
+uids_.insert(DicomTag(0x0088, 0x0140));                          // Storage Media File-set UID
+uids_.insert(DicomTag(0x3006, 0x0024));                          // Referenced Frame of Reference UID
+uids_.insert(DicomTag(0x3006, 0x00c2));                          // Related Frame of Reference UID
+uids_.insert(DicomTag(0x300a, 0x0013));                          // Dose Reference UID
diff --git a/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2021b.impl.h b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2021b.impl.h
new file mode 100644
index 0000000..207aa98
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2021b.impl.h
@@ -0,0 +1,459 @@
+// RelationshipsVisitor handles (0x0008, 0x1140)  /* X/Z/U* */   // Referenced Image Sequence
+// RelationshipsVisitor handles (0x0008, 0x2112)  /* X/Z/U* */   // Source Image Sequence
+// Tag (0x0008, 0x0018) is set in Apply()         /* U */        // SOP Instance UID
+// Tag (0x0010, 0x0010) is set below (*)          /* Z */        // Patient's Name
+// Tag (0x0010, 0x0020) is set below (*)          /* Z */        // Patient ID
+// Tag (0x0020, 0x000d) is set in Apply()         /* U */        // Study Instance UID
+// Tag (0x0020, 0x000e) is set in Apply()         /* U */        // Series Instance UID
+clearings_.insert(DicomTag(0x0008, 0x0020));                     // Study Date
+clearings_.insert(DicomTag(0x0008, 0x0023));  /* Z/D */          // Content Date
+clearings_.insert(DicomTag(0x0008, 0x0030));                     // Study Time
+clearings_.insert(DicomTag(0x0008, 0x0033));  /* Z/D */          // Content Time
+clearings_.insert(DicomTag(0x0008, 0x0050));                     // Accession Number
+clearings_.insert(DicomTag(0x0008, 0x0090));                     // Referring Physician's Name
+clearings_.insert(DicomTag(0x0008, 0x009c));                     // Consulting Physician's Name
+clearings_.insert(DicomTag(0x0010, 0x0030));                     // Patient's Birth Date
+clearings_.insert(DicomTag(0x0010, 0x0040));                     // Patient's Sex
+clearings_.insert(DicomTag(0x0012, 0x0010));  /* D */            // Clinical Trial Sponsor Name
+clearings_.insert(DicomTag(0x0012, 0x0020));  /* D */            // Clinical Trial Protocol ID
+clearings_.insert(DicomTag(0x0012, 0x0021));                     // Clinical Trial Protocol Name
+clearings_.insert(DicomTag(0x0012, 0x0030));                     // Clinical Trial Site ID
+clearings_.insert(DicomTag(0x0012, 0x0031));                     // Clinical Trial Site Name
+clearings_.insert(DicomTag(0x0012, 0x0040));  /* D */            // Clinical Trial Subject ID
+clearings_.insert(DicomTag(0x0012, 0x0042));  /* D */            // Clinical Trial Subject Reading ID
+clearings_.insert(DicomTag(0x0012, 0x0050));                     // Clinical Trial Time Point ID
+clearings_.insert(DicomTag(0x0012, 0x0060));                     // Clinical Trial Coordinating Center Name
+clearings_.insert(DicomTag(0x0012, 0x0081));  /* D */            // Clinical Trial Protocol Ethics Committee Name
+clearings_.insert(DicomTag(0x0018, 0x0010));  /* Z/D */          // Contrast/Bolus Agent
+clearings_.insert(DicomTag(0x0018, 0x11bb));  /* D */            // Acquisition Field Of View Label
+clearings_.insert(DicomTag(0x0018, 0x9367));  /* D */            // X-Ray Source ID
+clearings_.insert(DicomTag(0x0018, 0x9369));  /* D */            // Source Start DateTime
+clearings_.insert(DicomTag(0x0018, 0x936a));  /* D */            // Source End DateTime
+clearings_.insert(DicomTag(0x0018, 0x9371));  /* D */            // X-Ray Detector ID
+clearings_.insert(DicomTag(0x0020, 0x0010));                     // Study ID
+clearings_.insert(DicomTag(0x0034, 0x0001));  /* D */            // Flow Identifier Sequence
+clearings_.insert(DicomTag(0x0034, 0x0002));  /* D */            // Flow Identifier
+clearings_.insert(DicomTag(0x0034, 0x0005));  /* D */            // Source Identifier
+clearings_.insert(DicomTag(0x0034, 0x0007));  /* D */            // Frame Origin Timestamp
+clearings_.insert(DicomTag(0x003a, 0x0314));  /* D */            // Impedance Measurement DateTime
+clearings_.insert(DicomTag(0x0040, 0x0512));  /* D */            // Container Identifier
+clearings_.insert(DicomTag(0x0040, 0x0513));                     // Issuer of the Container Identifier Sequence
+clearings_.insert(DicomTag(0x0040, 0x0551));  /* D */            // Specimen Identifier
+clearings_.insert(DicomTag(0x0040, 0x0562));                     // Issuer of the Specimen Identifier Sequence
+clearings_.insert(DicomTag(0x0040, 0x0610));                     // Specimen Preparation Sequence
+clearings_.insert(DicomTag(0x0040, 0x1101));  /* D */            // Person Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0x2016));                     // Placer Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0x2017));                     // Filler Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0xa027));  /* D */            // Verifying Organization
+clearings_.insert(DicomTag(0x0040, 0xa073));  /* D */            // Verifying Observer Sequence
+clearings_.insert(DicomTag(0x0040, 0xa075));  /* D */            // Verifying Observer Name
+clearings_.insert(DicomTag(0x0040, 0xa088));                     // Verifying Observer Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0xa123));  /* D */            // Person Name
+clearings_.insert(DicomTag(0x0040, 0xa730));  /* D */            // Content Sequence
+clearings_.insert(DicomTag(0x0070, 0x0001));  /* D */            // Graphic Annotation Sequence
+clearings_.insert(DicomTag(0x0070, 0x0084));  /* Z/D */          // Content Creator's Name
+clearings_.insert(DicomTag(0x3006, 0x0002));  /* D */            // Structure Set Label
+clearings_.insert(DicomTag(0x3006, 0x0008));                     // Structure Set Date
+clearings_.insert(DicomTag(0x3006, 0x0009));                     // Structure Set Time
+clearings_.insert(DicomTag(0x3006, 0x0026));                     // ROI Name
+clearings_.insert(DicomTag(0x3006, 0x00a6));                     // ROI Interpreter
+clearings_.insert(DicomTag(0x300a, 0x0002));  /* D */            // RT Plan Label
+clearings_.insert(DicomTag(0x300a, 0x0608));  /* D */            // Treatment Position Group Label
+clearings_.insert(DicomTag(0x300a, 0x0611));                     // RT Accessory Holder Slot ID
+clearings_.insert(DicomTag(0x300a, 0x0615));                     // RT Accessory Device Slot ID
+clearings_.insert(DicomTag(0x300a, 0x0619));  /* D */            // Radiation Dose Identification Label
+clearings_.insert(DicomTag(0x300a, 0x0623));  /* D */            // Radiation Dose In-Vivo Measurement Label
+clearings_.insert(DicomTag(0x300a, 0x062a));  /* D */            // RT Tolerance Set Label
+clearings_.insert(DicomTag(0x300a, 0x067c));  /* D */            // Radiation Generation Mode Label
+clearings_.insert(DicomTag(0x300a, 0x067d));                     // Radiation Generation Mode Description
+clearings_.insert(DicomTag(0x300a, 0x0734));  /* D */            // Treatment Tolerance Violation Description
+clearings_.insert(DicomTag(0x300a, 0x0736));  /* D */            // Treatment Tolerance Violation DateTime
+clearings_.insert(DicomTag(0x300a, 0x073a));  /* D */            // Recorded RT Control Point DateTime
+clearings_.insert(DicomTag(0x300a, 0x0741));  /* D */            // Interlock DateTime
+clearings_.insert(DicomTag(0x300a, 0x0742));  /* D */            // Interlock Description
+clearings_.insert(DicomTag(0x300a, 0x0760));  /* D */            // Override DateTime
+clearings_.insert(DicomTag(0x300a, 0x0783));  /* D */            // Interlock Origin Description
+clearings_.insert(DicomTag(0x3010, 0x000f));                     // Conceptual Volume Combination Description
+clearings_.insert(DicomTag(0x3010, 0x0017));                     // Conceptual Volume Description
+clearings_.insert(DicomTag(0x3010, 0x001b));                     // Device Alternate Identifier
+clearings_.insert(DicomTag(0x3010, 0x002d));  /* D */            // Device Label
+clearings_.insert(DicomTag(0x3010, 0x0033));  /* D */            // User Content Label
+clearings_.insert(DicomTag(0x3010, 0x0034));  /* D */            // User Content Long Label
+clearings_.insert(DicomTag(0x3010, 0x0035));  /* D */            // Entity Label
+clearings_.insert(DicomTag(0x3010, 0x0038));  /* D */            // Entity Long Label
+clearings_.insert(DicomTag(0x3010, 0x0043));                     // Manufacturer's Device Identifier
+clearings_.insert(DicomTag(0x3010, 0x0054));  /* D */            // RT Prescription Label
+clearings_.insert(DicomTag(0x3010, 0x005a));                     // RT Physician Intent Narrative
+clearings_.insert(DicomTag(0x3010, 0x005c));                     // Reason for Superseding
+clearings_.insert(DicomTag(0x3010, 0x0077));  /* D */            // Treatment Site
+clearings_.insert(DicomTag(0x3010, 0x007a));                     // Treatment Technique Notes
+clearings_.insert(DicomTag(0x3010, 0x007b));                     // Prescription Notes
+clearings_.insert(DicomTag(0x3010, 0x007f));                     // Fractionation Notes
+clearings_.insert(DicomTag(0x3010, 0x0081));                     // Prescription Notes Sequence
+removals_.insert(DicomTag(0x0000, 0x1000));                      // Affected SOP Instance UID
+removals_.insert(DicomTag(0x0008, 0x0015));                      // Instance Coercion DateTime
+removals_.insert(DicomTag(0x0008, 0x0021));   /* X/D */          // Series Date
+removals_.insert(DicomTag(0x0008, 0x0022));   /* X/Z */          // Acquisition Date
+removals_.insert(DicomTag(0x0008, 0x0024));                      // Overlay Date
+removals_.insert(DicomTag(0x0008, 0x0025));                      // Curve Date
+removals_.insert(DicomTag(0x0008, 0x002a));   /* X/Z/D */        // Acquisition DateTime
+removals_.insert(DicomTag(0x0008, 0x0031));   /* X/D */          // Series Time
+removals_.insert(DicomTag(0x0008, 0x0032));   /* X/Z */          // Acquisition Time
+removals_.insert(DicomTag(0x0008, 0x0034));                      // Overlay Time
+removals_.insert(DicomTag(0x0008, 0x0035));                      // Curve Time
+removals_.insert(DicomTag(0x0008, 0x0080));   /* X/Z/D */        // Institution Name
+removals_.insert(DicomTag(0x0008, 0x0081));                      // Institution Address
+removals_.insert(DicomTag(0x0008, 0x0082));   /* X/Z/D */        // Institution Code Sequence
+removals_.insert(DicomTag(0x0008, 0x0092));                      // Referring Physician's Address
+removals_.insert(DicomTag(0x0008, 0x0094));                      // Referring Physician's Telephone Numbers
+removals_.insert(DicomTag(0x0008, 0x0096));                      // Referring Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x009d));                      // Consulting Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x0201));                      // Timezone Offset From UTC
+removals_.insert(DicomTag(0x0008, 0x1010));   /* X/Z/D */        // Station Name
+removals_.insert(DicomTag(0x0008, 0x1030));                      // Study Description
+removals_.insert(DicomTag(0x0008, 0x103e));                      // Series Description
+removals_.insert(DicomTag(0x0008, 0x1040));                      // Institutional Department Name
+removals_.insert(DicomTag(0x0008, 0x1041));                      // Institutional Department Type Code Sequence
+removals_.insert(DicomTag(0x0008, 0x1048));                      // Physician(s) of Record
+removals_.insert(DicomTag(0x0008, 0x1049));                      // Physician(s) of Record Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1050));                      // Performing Physician's Name
+removals_.insert(DicomTag(0x0008, 0x1052));                      // Performing Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1060));                      // Name of Physician(s) Reading Study
+removals_.insert(DicomTag(0x0008, 0x1062));                      // Physician(s) Reading Study Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1070));   /* X/Z/D */        // Operators' Name
+removals_.insert(DicomTag(0x0008, 0x1072));   /* X/D */          // Operator Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1080));                      // Admitting Diagnoses Description
+removals_.insert(DicomTag(0x0008, 0x1084));                      // Admitting Diagnoses Code Sequence
+removals_.insert(DicomTag(0x0008, 0x1110));   /* X/Z */          // Referenced Study Sequence
+removals_.insert(DicomTag(0x0008, 0x1111));   /* X/Z/D */        // Referenced Performed Procedure Step Sequence
+removals_.insert(DicomTag(0x0008, 0x1120));                      // Referenced Patient Sequence
+removals_.insert(DicomTag(0x0008, 0x2111));                      // Derivation Description
+removals_.insert(DicomTag(0x0008, 0x4000));                      // Identifying Comments
+removals_.insert(DicomTag(0x0010, 0x0021));                      // Issuer of Patient ID
+removals_.insert(DicomTag(0x0010, 0x0032));                      // Patient's Birth Time
+removals_.insert(DicomTag(0x0010, 0x0050));                      // Patient's Insurance Plan Code Sequence
+removals_.insert(DicomTag(0x0010, 0x0101));                      // Patient's Primary Language Code Sequence
+removals_.insert(DicomTag(0x0010, 0x0102));                      // Patient's Primary Language Modifier Code Sequence
+removals_.insert(DicomTag(0x0010, 0x1000));                      // Other Patient IDs
+removals_.insert(DicomTag(0x0010, 0x1001));                      // Other Patient Names
+removals_.insert(DicomTag(0x0010, 0x1002));                      // Other Patient IDs Sequence
+removals_.insert(DicomTag(0x0010, 0x1005));                      // Patient's Birth Name
+removals_.insert(DicomTag(0x0010, 0x1010));                      // Patient's Age
+removals_.insert(DicomTag(0x0010, 0x1020));                      // Patient's Size
+removals_.insert(DicomTag(0x0010, 0x1030));                      // Patient's Weight
+removals_.insert(DicomTag(0x0010, 0x1040));                      // Patient's Address
+removals_.insert(DicomTag(0x0010, 0x1050));                      // Insurance Plan Identification
+removals_.insert(DicomTag(0x0010, 0x1060));                      // Patient's Mother's Birth Name
+removals_.insert(DicomTag(0x0010, 0x1080));                      // Military Rank
+removals_.insert(DicomTag(0x0010, 0x1081));                      // Branch of Service
+removals_.insert(DicomTag(0x0010, 0x1090));                      // Medical Record Locator
+removals_.insert(DicomTag(0x0010, 0x1100));                      // Referenced Patient Photo Sequence
+removals_.insert(DicomTag(0x0010, 0x2000));                      // Medical Alerts
+removals_.insert(DicomTag(0x0010, 0x2110));                      // Allergies
+removals_.insert(DicomTag(0x0010, 0x2150));                      // Country of Residence
+removals_.insert(DicomTag(0x0010, 0x2152));                      // Region of Residence
+removals_.insert(DicomTag(0x0010, 0x2154));                      // Patient's Telephone Numbers
+removals_.insert(DicomTag(0x0010, 0x2155));                      // Patient's Telecom Information
+removals_.insert(DicomTag(0x0010, 0x2160));                      // Ethnic Group
+removals_.insert(DicomTag(0x0010, 0x2180));                      // Occupation
+removals_.insert(DicomTag(0x0010, 0x21a0));                      // Smoking Status
+removals_.insert(DicomTag(0x0010, 0x21b0));                      // Additional Patient History
+removals_.insert(DicomTag(0x0010, 0x21c0));                      // Pregnancy Status
+removals_.insert(DicomTag(0x0010, 0x21d0));                      // Last Menstrual Date
+removals_.insert(DicomTag(0x0010, 0x21f0));                      // Patient's Religious Preference
+removals_.insert(DicomTag(0x0010, 0x2203));   /* X/Z */          // Patient's Sex Neutered
+removals_.insert(DicomTag(0x0010, 0x2297));                      // Responsible Person
+removals_.insert(DicomTag(0x0010, 0x2299));                      // Responsible Organization
+removals_.insert(DicomTag(0x0010, 0x4000));                      // Patient Comments
+removals_.insert(DicomTag(0x0012, 0x0051));                      // Clinical Trial Time Point Description
+removals_.insert(DicomTag(0x0012, 0x0071));                      // Clinical Trial Series ID
+removals_.insert(DicomTag(0x0012, 0x0072));                      // Clinical Trial Series Description
+removals_.insert(DicomTag(0x0012, 0x0082));                      // Clinical Trial Protocol Ethics Committee Approval Number
+removals_.insert(DicomTag(0x0016, 0x002b));                      // Maker Note
+removals_.insert(DicomTag(0x0016, 0x004b));                      // Device Setting Description
+removals_.insert(DicomTag(0x0016, 0x004d));                      // Camera Owner Name
+removals_.insert(DicomTag(0x0016, 0x004e));                      // Lens Specification
+removals_.insert(DicomTag(0x0016, 0x004f));                      // Lens Make
+removals_.insert(DicomTag(0x0016, 0x0050));                      // Lens Model
+removals_.insert(DicomTag(0x0016, 0x0051));                      // Lens Serial Number
+removals_.insert(DicomTag(0x0016, 0x0070));                      // GPS Version ID
+removals_.insert(DicomTag(0x0016, 0x0071));                      // GPS Latitude Ref
+removals_.insert(DicomTag(0x0016, 0x0072));                      // GPS Latitude
+removals_.insert(DicomTag(0x0016, 0x0073));                      // GPS Longitude Ref
+removals_.insert(DicomTag(0x0016, 0x0074));                      // GPS Longitude
+removals_.insert(DicomTag(0x0016, 0x0075));                      // GPS Altitude Ref
+removals_.insert(DicomTag(0x0016, 0x0076));                      // GPS Altitude
+removals_.insert(DicomTag(0x0016, 0x0077));                      // GPS Time Stamp
+removals_.insert(DicomTag(0x0016, 0x0078));                      // GPS Satellites
+removals_.insert(DicomTag(0x0016, 0x0079));                      // GPS Status
+removals_.insert(DicomTag(0x0016, 0x007a));                      // GPS Measure Mode
+removals_.insert(DicomTag(0x0016, 0x007b));                      // GPS DOP
+removals_.insert(DicomTag(0x0016, 0x007c));                      // GPS Speed Ref
+removals_.insert(DicomTag(0x0016, 0x007d));                      // GPS Speed
+removals_.insert(DicomTag(0x0016, 0x007e));                      // GPS Track Ref
+removals_.insert(DicomTag(0x0016, 0x007f));                      // GPS Track
+removals_.insert(DicomTag(0x0016, 0x0080));                      // GPS Img Direction Ref
+removals_.insert(DicomTag(0x0016, 0x0081));                      // GPS Img Direction
+removals_.insert(DicomTag(0x0016, 0x0082));                      // GPS Map Datum
+removals_.insert(DicomTag(0x0016, 0x0083));                      // GPS Dest Latitude Ref
+removals_.insert(DicomTag(0x0016, 0x0084));                      // GPS Dest Latitude
+removals_.insert(DicomTag(0x0016, 0x0085));                      // GPS Dest Longitude Ref
+removals_.insert(DicomTag(0x0016, 0x0086));                      // GPS Dest Longitude
+removals_.insert(DicomTag(0x0016, 0x0087));                      // GPS Dest Bearing Ref
+removals_.insert(DicomTag(0x0016, 0x0088));                      // GPS Dest Bearing
+removals_.insert(DicomTag(0x0016, 0x0089));                      // GPS Dest Distance Ref
+removals_.insert(DicomTag(0x0016, 0x008a));                      // GPS Dest Distance
+removals_.insert(DicomTag(0x0016, 0x008b));                      // GPS Processing Method
+removals_.insert(DicomTag(0x0016, 0x008c));                      // GPS Area Information
+removals_.insert(DicomTag(0x0016, 0x008d));                      // GPS Date Stamp
+removals_.insert(DicomTag(0x0016, 0x008e));                      // GPS Differential
+removals_.insert(DicomTag(0x0018, 0x1000));   /* X/Z/D */        // Device Serial Number
+removals_.insert(DicomTag(0x0018, 0x1004));                      // Plate ID
+removals_.insert(DicomTag(0x0018, 0x1005));                      // Generator ID
+removals_.insert(DicomTag(0x0018, 0x1007));                      // Cassette ID
+removals_.insert(DicomTag(0x0018, 0x1008));                      // Gantry ID
+removals_.insert(DicomTag(0x0018, 0x1009));                      // Unique Device Identifier
+removals_.insert(DicomTag(0x0018, 0x100a));                      // UDI Sequence
+removals_.insert(DicomTag(0x0018, 0x1030));   /* X/D */          // Protocol Name
+removals_.insert(DicomTag(0x0018, 0x1400));   /* X/D */          // Acquisition Device Processing Description
+removals_.insert(DicomTag(0x0018, 0x4000));                      // Acquisition Comments
+removals_.insert(DicomTag(0x0018, 0x5011));                      // Transducer Identification Sequence
+removals_.insert(DicomTag(0x0018, 0x700a));   /* X/D */          // Detector ID
+removals_.insert(DicomTag(0x0018, 0x9185));                      // Respiratory Motion Compensation Technique Description
+removals_.insert(DicomTag(0x0018, 0x9373));                      // X-Ray Detector Label
+removals_.insert(DicomTag(0x0018, 0x937b));                      // Multi-energy Acquisition Description
+removals_.insert(DicomTag(0x0018, 0x937f));                      // Decomposition Description
+removals_.insert(DicomTag(0x0018, 0x9424));                      // Acquisition Protocol Description
+removals_.insert(DicomTag(0x0018, 0x9516));   /* X/D */          // Start Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0x9517));   /* X/D */          // End Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0x9937));                      // Requested Series Description
+removals_.insert(DicomTag(0x0018, 0xa003));                      // Contribution Description
+removals_.insert(DicomTag(0x0020, 0x3401));                      // Modifying Device ID
+removals_.insert(DicomTag(0x0020, 0x3406));                      // Modified Image Description
+removals_.insert(DicomTag(0x0020, 0x4000));                      // Image Comments
+removals_.insert(DicomTag(0x0020, 0x9158));                      // Frame Comments
+removals_.insert(DicomTag(0x0028, 0x4000));                      // Image Presentation Comments
+removals_.insert(DicomTag(0x0032, 0x0012));                      // Study ID Issuer
+removals_.insert(DicomTag(0x0032, 0x1020));                      // Scheduled Study Location
+removals_.insert(DicomTag(0x0032, 0x1021));                      // Scheduled Study Location AE Title
+removals_.insert(DicomTag(0x0032, 0x1030));                      // Reason for Study
+removals_.insert(DicomTag(0x0032, 0x1032));                      // Requesting Physician
+removals_.insert(DicomTag(0x0032, 0x1033));                      // Requesting Service
+removals_.insert(DicomTag(0x0032, 0x1060));   /* X/Z */          // Requested Procedure Description
+removals_.insert(DicomTag(0x0032, 0x1066));                      // Reason for Visit
+removals_.insert(DicomTag(0x0032, 0x1067));                      // Reason for Visit Code Sequence
+removals_.insert(DicomTag(0x0032, 0x1070));                      // Requested Contrast Agent
+removals_.insert(DicomTag(0x0032, 0x4000));                      // Study Comments
+removals_.insert(DicomTag(0x0038, 0x0004));                      // Referenced Patient Alias Sequence
+removals_.insert(DicomTag(0x0038, 0x0010));                      // Admission ID
+removals_.insert(DicomTag(0x0038, 0x0011));                      // Issuer of Admission ID
+removals_.insert(DicomTag(0x0038, 0x0014));                      // Issuer of Admission ID Sequence
+removals_.insert(DicomTag(0x0038, 0x001e));                      // Scheduled Patient Institution Residence
+removals_.insert(DicomTag(0x0038, 0x0020));                      // Admitting Date
+removals_.insert(DicomTag(0x0038, 0x0021));                      // Admitting Time
+removals_.insert(DicomTag(0x0038, 0x0040));                      // Discharge Diagnosis Description
+removals_.insert(DicomTag(0x0038, 0x0050));                      // Special Needs
+removals_.insert(DicomTag(0x0038, 0x0060));                      // Service Episode ID
+removals_.insert(DicomTag(0x0038, 0x0061));                      // Issuer of Service Episode ID
+removals_.insert(DicomTag(0x0038, 0x0062));                      // Service Episode Description
+removals_.insert(DicomTag(0x0038, 0x0064));                      // Issuer of Service Episode ID Sequence
+removals_.insert(DicomTag(0x0038, 0x0300));                      // Current Patient Location
+removals_.insert(DicomTag(0x0038, 0x0400));                      // Patient's Institution Residence
+removals_.insert(DicomTag(0x0038, 0x0500));                      // Patient State
+removals_.insert(DicomTag(0x0038, 0x4000));                      // Visit Comments
+removals_.insert(DicomTag(0x0040, 0x0001));                      // Scheduled Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0002));                      // Scheduled Procedure Step Start Date
+removals_.insert(DicomTag(0x0040, 0x0003));                      // Scheduled Procedure Step Start Time
+removals_.insert(DicomTag(0x0040, 0x0004));                      // Scheduled Procedure Step End Date
+removals_.insert(DicomTag(0x0040, 0x0005));                      // Scheduled Procedure Step End Time
+removals_.insert(DicomTag(0x0040, 0x0006));                      // Scheduled Performing Physician's Name
+removals_.insert(DicomTag(0x0040, 0x0007));                      // Scheduled Procedure Step Description
+removals_.insert(DicomTag(0x0040, 0x0009));                      // Scheduled Procedure Step ID
+removals_.insert(DicomTag(0x0040, 0x000b));                      // Scheduled Performing Physician Identification Sequence
+removals_.insert(DicomTag(0x0040, 0x0010));                      // Scheduled Station Name
+removals_.insert(DicomTag(0x0040, 0x0011));                      // Scheduled Procedure Step Location
+removals_.insert(DicomTag(0x0040, 0x0012));                      // Pre-Medication
+removals_.insert(DicomTag(0x0040, 0x0241));                      // Performed Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0242));                      // Performed Station Name
+removals_.insert(DicomTag(0x0040, 0x0243));                      // Performed Location
+removals_.insert(DicomTag(0x0040, 0x0244));                      // Performed Procedure Step Start Date
+removals_.insert(DicomTag(0x0040, 0x0245));                      // Performed Procedure Step Start Time
+removals_.insert(DicomTag(0x0040, 0x0250));                      // Performed Procedure Step End Date
+removals_.insert(DicomTag(0x0040, 0x0251));                      // Performed Procedure Step End Time
+removals_.insert(DicomTag(0x0040, 0x0253));                      // Performed Procedure Step ID
+removals_.insert(DicomTag(0x0040, 0x0254));                      // Performed Procedure Step Description
+removals_.insert(DicomTag(0x0040, 0x0275));                      // Request Attributes Sequence
+removals_.insert(DicomTag(0x0040, 0x0280));                      // Comments on the Performed Procedure Step
+removals_.insert(DicomTag(0x0040, 0x0310));                      // Comments on Radiation Dose
+removals_.insert(DicomTag(0x0040, 0x050a));                      // Specimen Accession Number
+removals_.insert(DicomTag(0x0040, 0x051a));                      // Container Description
+removals_.insert(DicomTag(0x0040, 0x0555));   /* X/Z */          // Acquisition Context Sequence
+removals_.insert(DicomTag(0x0040, 0x0600));                      // Specimen Short Description
+removals_.insert(DicomTag(0x0040, 0x0602));                      // Specimen Detailed Description
+removals_.insert(DicomTag(0x0040, 0x06fa));                      // Slide Identifier
+removals_.insert(DicomTag(0x0040, 0x1001));                      // Requested Procedure ID
+removals_.insert(DicomTag(0x0040, 0x1002));                      // Reason for the Requested Procedure
+removals_.insert(DicomTag(0x0040, 0x1004));                      // Patient Transport Arrangements
+removals_.insert(DicomTag(0x0040, 0x1005));                      // Requested Procedure Location
+removals_.insert(DicomTag(0x0040, 0x100a));                      // Reason for Requested Procedure Code Sequence
+removals_.insert(DicomTag(0x0040, 0x1010));                      // Names of Intended Recipients of Results
+removals_.insert(DicomTag(0x0040, 0x1011));                      // Intended Recipients of Results Identification Sequence
+removals_.insert(DicomTag(0x0040, 0x1102));                      // Person's Address
+removals_.insert(DicomTag(0x0040, 0x1103));                      // Person's Telephone Numbers
+removals_.insert(DicomTag(0x0040, 0x1104));                      // Person's Telecom Information
+removals_.insert(DicomTag(0x0040, 0x1400));                      // Requested Procedure Comments
+removals_.insert(DicomTag(0x0040, 0x2001));                      // Reason for the Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2008));                      // Order Entered By
+removals_.insert(DicomTag(0x0040, 0x2009));                      // Order Enterer's Location
+removals_.insert(DicomTag(0x0040, 0x2010));                      // Order Callback Phone Number
+removals_.insert(DicomTag(0x0040, 0x2011));                      // Order Callback Telecom Information
+removals_.insert(DicomTag(0x0040, 0x2400));                      // Imaging Service Request Comments
+removals_.insert(DicomTag(0x0040, 0x3001));                      // Confidentiality Constraint on Patient Data Description
+removals_.insert(DicomTag(0x0040, 0x4005));                      // Scheduled Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4008));                      // Scheduled Procedure Step Expiration DateTime
+removals_.insert(DicomTag(0x0040, 0x4010));                      // Scheduled Procedure Step Modification DateTime
+removals_.insert(DicomTag(0x0040, 0x4011));                      // Expected Completion DateTime
+removals_.insert(DicomTag(0x0040, 0x4025));                      // Scheduled Station Name Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4027));                      // Scheduled Station Geographic Location Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4028));                      // Performed Station Name Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4030));                      // Performed Station Geographic Location Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4034));                      // Scheduled Human Performers Sequence
+removals_.insert(DicomTag(0x0040, 0x4035));                      // Actual Human Performers Sequence
+removals_.insert(DicomTag(0x0040, 0x4036));                      // Human Performer's Organization
+removals_.insert(DicomTag(0x0040, 0x4037));                      // Human Performer's Name
+removals_.insert(DicomTag(0x0040, 0x4050));                      // Performed Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4051));                      // Performed Procedure Step End DateTime
+removals_.insert(DicomTag(0x0040, 0x4052));                      // Procedure Step Cancellation DateTime
+removals_.insert(DicomTag(0x0040, 0xa078));                      // Author Observer Sequence
+removals_.insert(DicomTag(0x0040, 0xa07a));                      // Participant Sequence
+removals_.insert(DicomTag(0x0040, 0xa07c));                      // Custodial Organization Sequence
+removals_.insert(DicomTag(0x0040, 0xa192));                      // Observation Date (Trial)
+removals_.insert(DicomTag(0x0040, 0xa193));                      // Observation Time (Trial)
+removals_.insert(DicomTag(0x0040, 0xa307));                      // Current Observer (Trial)
+removals_.insert(DicomTag(0x0040, 0xa352));                      // Verbal Source (Trial)
+removals_.insert(DicomTag(0x0040, 0xa353));                      // Address (Trial)
+removals_.insert(DicomTag(0x0040, 0xa354));                      // Telephone Number (Trial)
+removals_.insert(DicomTag(0x0040, 0xa358));                      // Verbal Source Identifier Code Sequence (Trial)
+removals_.insert(DicomTag(0x0050, 0x001b));                      // Container Component ID
+removals_.insert(DicomTag(0x0050, 0x0020));                      // Device Description
+removals_.insert(DicomTag(0x0050, 0x0021));                      // Long Device Description
+removals_.insert(DicomTag(0x0070, 0x0086));                      // Content Creator's Identification Code Sequence
+removals_.insert(DicomTag(0x0088, 0x0200));                      // Icon Image Sequence
+removals_.insert(DicomTag(0x0088, 0x0904));                      // Topic Title
+removals_.insert(DicomTag(0x0088, 0x0906));                      // Topic Subject
+removals_.insert(DicomTag(0x0088, 0x0910));                      // Topic Author
+removals_.insert(DicomTag(0x0088, 0x0912));                      // Topic Keywords
+removals_.insert(DicomTag(0x0400, 0x0402));                      // Referenced Digital Signature Sequence
+removals_.insert(DicomTag(0x0400, 0x0403));                      // Referenced SOP Instance MAC Sequence
+removals_.insert(DicomTag(0x0400, 0x0404));                      // MAC
+removals_.insert(DicomTag(0x0400, 0x0550));                      // Modified Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0551));                      // Nonconforming Modified Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0552));                      // Nonconforming Data Element Value
+removals_.insert(DicomTag(0x0400, 0x0561));                      // Original Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0600));                      // Instance Origin Status
+removals_.insert(DicomTag(0x2030, 0x0020));                      // Text String
+removals_.insert(DicomTag(0x2200, 0x0002));   /* X/Z */          // Label Text
+removals_.insert(DicomTag(0x2200, 0x0005));   /* X/Z */          // Barcode Value
+removals_.insert(DicomTag(0x3006, 0x0004));                      // Structure Set Name
+removals_.insert(DicomTag(0x3006, 0x0006));                      // Structure Set Description
+removals_.insert(DicomTag(0x3006, 0x0028));                      // ROI Description
+removals_.insert(DicomTag(0x3006, 0x0038));                      // ROI Generation Description
+removals_.insert(DicomTag(0x3006, 0x0085));                      // ROI Observation Label
+removals_.insert(DicomTag(0x3006, 0x0088));                      // ROI Observation Description
+removals_.insert(DicomTag(0x3008, 0x0054));   /* X/D */          // First Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0056));   /* X/D */          // Most Recent Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0105));   /* X/Z */          // Source Serial Number
+removals_.insert(DicomTag(0x3008, 0x0250));   /* X/D */          // Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0251));   /* X/D */          // Treatment Time
+removals_.insert(DicomTag(0x300a, 0x0003));                      // RT Plan Name
+removals_.insert(DicomTag(0x300a, 0x0004));                      // RT Plan Description
+removals_.insert(DicomTag(0x300a, 0x0006));   /* X/D */          // RT Plan Date
+removals_.insert(DicomTag(0x300a, 0x0007));   /* X/D */          // RT Plan Time
+removals_.insert(DicomTag(0x300a, 0x000e));                      // Prescription Description
+removals_.insert(DicomTag(0x300a, 0x0016));                      // Dose Reference Description
+removals_.insert(DicomTag(0x300a, 0x0072));                      // Fraction Group Description
+removals_.insert(DicomTag(0x300a, 0x00b2));   /* X/Z */          // Treatment Machine Name
+removals_.insert(DicomTag(0x300a, 0x00c3));                      // Beam Description
+removals_.insert(DicomTag(0x300a, 0x00dd));                      // Bolus Description
+removals_.insert(DicomTag(0x300a, 0x0196));                      // Fixation Device Description
+removals_.insert(DicomTag(0x300a, 0x01a6));                      // Shielding Device Description
+removals_.insert(DicomTag(0x300a, 0x01b2));                      // Setup Technique Description
+removals_.insert(DicomTag(0x300a, 0x0216));                      // Source Manufacturer
+removals_.insert(DicomTag(0x300a, 0x02eb));                      // Compensator Description
+removals_.insert(DicomTag(0x300a, 0x0676));                      // Equipment Frame of Reference Description
+removals_.insert(DicomTag(0x300c, 0x0113));                      // Reason for Omission Description
+removals_.insert(DicomTag(0x300e, 0x0008));   /* X/Z */          // Reviewer Name
+removals_.insert(DicomTag(0x3010, 0x0036));                      // Entity Name
+removals_.insert(DicomTag(0x3010, 0x0037));                      // Entity Description
+removals_.insert(DicomTag(0x3010, 0x004c));   /* X/D */          // Intended Phase Start Date
+removals_.insert(DicomTag(0x3010, 0x004d));   /* X/D */          // Intended Phase End Date
+removals_.insert(DicomTag(0x3010, 0x0056));   /* X/D */          // RT Treatment Approach Label
+removals_.insert(DicomTag(0x3010, 0x0061));                      // Prior Treatment Dose Description
+removals_.insert(DicomTag(0x4000, 0x0010));                      // Arbitrary
+removals_.insert(DicomTag(0x4000, 0x4000));                      // Text Comments
+removals_.insert(DicomTag(0x4008, 0x0042));                      // Results ID Issuer
+removals_.insert(DicomTag(0x4008, 0x0102));                      // Interpretation Recorder
+removals_.insert(DicomTag(0x4008, 0x010a));                      // Interpretation Transcriber
+removals_.insert(DicomTag(0x4008, 0x010b));                      // Interpretation Text
+removals_.insert(DicomTag(0x4008, 0x010c));                      // Interpretation Author
+removals_.insert(DicomTag(0x4008, 0x0111));                      // Interpretation Approver Sequence
+removals_.insert(DicomTag(0x4008, 0x0114));                      // Physician Approving Interpretation
+removals_.insert(DicomTag(0x4008, 0x0115));                      // Interpretation Diagnosis Description
+removals_.insert(DicomTag(0x4008, 0x0118));                      // Results Distribution List Sequence
+removals_.insert(DicomTag(0x4008, 0x0119));                      // Distribution Name
+removals_.insert(DicomTag(0x4008, 0x011a));                      // Distribution Address
+removals_.insert(DicomTag(0x4008, 0x0202));                      // Interpretation ID Issuer
+removals_.insert(DicomTag(0x4008, 0x0300));                      // Impressions
+removals_.insert(DicomTag(0x4008, 0x4000));                      // Results Comments
+removals_.insert(DicomTag(0xfffa, 0xfffa));                      // Digital Signatures Sequence
+removals_.insert(DicomTag(0xfffc, 0xfffc));                      // Data Set Trailing Padding
+removedRanges_.push_back(DicomTagRange(0x5000, 0x50ff, 0x0000, 0xffff));  // Curve Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x3000, 0x3000));  // Overlay Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x4000, 0x4000));  // Overlay Comments
+uids_.insert(DicomTag(0x0000, 0x1001));                          // Requested SOP Instance UID
+uids_.insert(DicomTag(0x0002, 0x0003));                          // Media Storage SOP Instance UID
+uids_.insert(DicomTag(0x0004, 0x1511));                          // Referenced SOP Instance UID in File
+uids_.insert(DicomTag(0x0008, 0x0014));                          // Instance Creator UID
+uids_.insert(DicomTag(0x0008, 0x0058));                          // Failed SOP Instance UID List
+uids_.insert(DicomTag(0x0008, 0x1155));                          // Referenced SOP Instance UID
+uids_.insert(DicomTag(0x0008, 0x1195));                          // Transaction UID
+uids_.insert(DicomTag(0x0008, 0x3010));                          // Irradiation Event UID
+uids_.insert(DicomTag(0x0018, 0x1002));                          // Device UID
+uids_.insert(DicomTag(0x0018, 0x100b));                          // Manufacturer's Device Class UID
+uids_.insert(DicomTag(0x0018, 0x2042));                          // Target UID
+uids_.insert(DicomTag(0x0020, 0x0052));                          // Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x0200));                          // Synchronization Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x9161));                          // Concatenation UID
+uids_.insert(DicomTag(0x0020, 0x9164));                          // Dimension Organization UID
+uids_.insert(DicomTag(0x0028, 0x1199));                          // Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x0028, 0x1214));                          // Large Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x003a, 0x0310));                          // Multiplex Group UID
+uids_.insert(DicomTag(0x0040, 0x0554));                          // Specimen UID
+uids_.insert(DicomTag(0x0040, 0x4023));                          // Referenced General Purpose Scheduled Procedure Step Transaction UID
+uids_.insert(DicomTag(0x0040, 0xa124));                          // UID
+uids_.insert(DicomTag(0x0040, 0xa171));                          // Observation UID
+uids_.insert(DicomTag(0x0040, 0xa172));                          // Referenced Observation UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xa402));                          // Observation Subject UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xdb0c));                          // Template Extension Organization UID
+uids_.insert(DicomTag(0x0040, 0xdb0d));                          // Template Extension Creator UID
+uids_.insert(DicomTag(0x0062, 0x0021));                          // Tracking UID
+uids_.insert(DicomTag(0x0070, 0x031a));                          // Fiducial UID
+uids_.insert(DicomTag(0x0070, 0x1101));                          // Presentation Display Collection UID
+uids_.insert(DicomTag(0x0070, 0x1102));                          // Presentation Sequence Collection UID
+uids_.insert(DicomTag(0x0088, 0x0140));                          // Storage Media File-set UID
+uids_.insert(DicomTag(0x0400, 0x0100));                          // Digital Signature UID
+uids_.insert(DicomTag(0x3006, 0x0024));                          // Referenced Frame of Reference UID
+uids_.insert(DicomTag(0x3006, 0x00c2));                          // Related Frame of Reference UID
+uids_.insert(DicomTag(0x300a, 0x0013));                          // Dose Reference UID
+uids_.insert(DicomTag(0x300a, 0x0083));                          // Referenced Dose Reference UID
+uids_.insert(DicomTag(0x300a, 0x0609));                          // Treatment Position Group UID
+uids_.insert(DicomTag(0x300a, 0x0650));                          // Patient Setup UID
+uids_.insert(DicomTag(0x300a, 0x0700));                          // Treatment Session UID
+uids_.insert(DicomTag(0x3010, 0x0006));                          // Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x000b));                          // Referenced Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0013));                          // Constituent Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0015));                          // Source Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0031));                          // Referenced Fiducials UID
+uids_.insert(DicomTag(0x3010, 0x003b));                          // RT Treatment Phase UID
+uids_.insert(DicomTag(0x3010, 0x006e));                          // Dosimetric Objective UID
+uids_.insert(DicomTag(0x3010, 0x006f));                          // Referenced Dosimetric Objective UID
diff --git a/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2023b.impl.h b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2023b.impl.h
new file mode 100644
index 0000000..c037dab
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2023b.impl.h
@@ -0,0 +1,610 @@
+// RelationshipsVisitor handles (0x0008, 0x1140)  /* X/Z/U* */   // Referenced Image Sequence
+// RelationshipsVisitor handles (0x0008, 0x2112)  /* X/Z/U* */   // Source Image Sequence
+// Tag (0x0008, 0x0018) is set in Apply()         /* U */        // SOP Instance UID
+// Tag (0x0010, 0x0010) is set below (*)          /* Z */        // Patient's Name
+// Tag (0x0010, 0x0020) is set below (*)          /* Z */        // Patient ID
+// Tag (0x0020, 0x000d) is set in Apply()         /* U */        // Study Instance UID
+// Tag (0x0020, 0x000e) is set in Apply()         /* U */        // Series Instance UID
+clearings_.insert(DicomTag(0x0008, 0x0020));                     // Study Date
+clearings_.insert(DicomTag(0x0008, 0x0023));  /* Z/D */          // Content Date
+clearings_.insert(DicomTag(0x0008, 0x0030));                     // Study Time
+clearings_.insert(DicomTag(0x0008, 0x0033));  /* Z/D */          // Content Time
+clearings_.insert(DicomTag(0x0008, 0x0050));                     // Accession Number
+clearings_.insert(DicomTag(0x0008, 0x0090));                     // Referring Physician's Name
+clearings_.insert(DicomTag(0x0008, 0x009c));                     // Consulting Physician's Name
+clearings_.insert(DicomTag(0x0008, 0x0106));  /* D */            // Context Group Version
+clearings_.insert(DicomTag(0x0008, 0x0107));  /* D */            // Context Group Local Version
+clearings_.insert(DicomTag(0x0010, 0x0030));                     // Patient's Birth Date
+clearings_.insert(DicomTag(0x0010, 0x0040));                     // Patient's Sex
+clearings_.insert(DicomTag(0x0012, 0x0010));  /* D */            // Clinical Trial Sponsor Name
+clearings_.insert(DicomTag(0x0012, 0x0020));  /* D */            // Clinical Trial Protocol ID
+clearings_.insert(DicomTag(0x0012, 0x0021));                     // Clinical Trial Protocol Name
+clearings_.insert(DicomTag(0x0012, 0x0030));                     // Clinical Trial Site ID
+clearings_.insert(DicomTag(0x0012, 0x0031));                     // Clinical Trial Site Name
+clearings_.insert(DicomTag(0x0012, 0x0040));  /* D */            // Clinical Trial Subject ID
+clearings_.insert(DicomTag(0x0012, 0x0042));  /* D */            // Clinical Trial Subject Reading ID
+clearings_.insert(DicomTag(0x0012, 0x0050));                     // Clinical Trial Time Point ID
+clearings_.insert(DicomTag(0x0012, 0x0060));                     // Clinical Trial Coordinating Center Name
+clearings_.insert(DicomTag(0x0012, 0x0081));  /* D */            // Clinical Trial Protocol Ethics Committee Name
+clearings_.insert(DicomTag(0x0018, 0x0010));  /* Z/D */          // Contrast/Bolus Agent
+clearings_.insert(DicomTag(0x0018, 0x11bb));  /* D */            // Acquisition Field Of View Label
+clearings_.insert(DicomTag(0x0018, 0x1203));                     // Calibration DateTime
+clearings_.insert(DicomTag(0x0018, 0x9074));  /* D */            // Frame Acquisition DateTime
+clearings_.insert(DicomTag(0x0018, 0x9151));  /* D */            // Frame Reference DateTime
+clearings_.insert(DicomTag(0x0018, 0x9367));  /* D */            // X-Ray Source ID
+clearings_.insert(DicomTag(0x0018, 0x9369));  /* D */            // Source Start DateTime
+clearings_.insert(DicomTag(0x0018, 0x936a));  /* D */            // Source End DateTime
+clearings_.insert(DicomTag(0x0018, 0x9371));  /* D */            // X-Ray Detector ID
+clearings_.insert(DicomTag(0x0018, 0x9623));  /* D */            // Functional Sync Pulse
+clearings_.insert(DicomTag(0x0018, 0x9701));  /* D */            // Decay Correction DateTime
+clearings_.insert(DicomTag(0x0018, 0x9804));  /* D */            // Exclusion Start DateTime
+clearings_.insert(DicomTag(0x0018, 0x9919));  /* Z/D */          // Instruction Performed DateTime
+clearings_.insert(DicomTag(0x0020, 0x0010));                     // Study ID
+clearings_.insert(DicomTag(0x0034, 0x0001));  /* D */            // Flow Identifier Sequence
+clearings_.insert(DicomTag(0x0034, 0x0002));  /* D */            // Flow Identifier
+clearings_.insert(DicomTag(0x0034, 0x0005));  /* D */            // Source Identifier
+clearings_.insert(DicomTag(0x0034, 0x0007));  /* D */            // Frame Origin Timestamp
+clearings_.insert(DicomTag(0x003a, 0x0314));  /* D */            // Impedance Measurement DateTime
+clearings_.insert(DicomTag(0x0040, 0x0512));  /* D */            // Container Identifier
+clearings_.insert(DicomTag(0x0040, 0x0513));                     // Issuer of the Container Identifier Sequence
+clearings_.insert(DicomTag(0x0040, 0x0551));  /* D */            // Specimen Identifier
+clearings_.insert(DicomTag(0x0040, 0x0562));                     // Issuer of the Specimen Identifier Sequence
+clearings_.insert(DicomTag(0x0040, 0x0610));                     // Specimen Preparation Sequence
+clearings_.insert(DicomTag(0x0040, 0x1101));  /* D */            // Person Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0x2016));                     // Placer Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0x2017));                     // Filler Order Number / Imaging Service Request
+clearings_.insert(DicomTag(0x0040, 0xa027));  /* D */            // Verifying Organization
+clearings_.insert(DicomTag(0x0040, 0xa030));  /* D */            // Verification DateTime
+clearings_.insert(DicomTag(0x0040, 0xa073));  /* D */            // Verifying Observer Sequence
+clearings_.insert(DicomTag(0x0040, 0xa075));  /* D */            // Verifying Observer Name
+clearings_.insert(DicomTag(0x0040, 0xa082));                     // Participation DateTime
+clearings_.insert(DicomTag(0x0040, 0xa088));                     // Verifying Observer Identification Code Sequence
+clearings_.insert(DicomTag(0x0040, 0xa120));  /* D */            // DateTime
+clearings_.insert(DicomTag(0x0040, 0xa121));  /* D */            // Date
+clearings_.insert(DicomTag(0x0040, 0xa122));  /* D */            // Time
+clearings_.insert(DicomTag(0x0040, 0xa123));  /* D */            // Person Name
+clearings_.insert(DicomTag(0x0040, 0xa13a));  /* D */            // Referenced DateTime
+clearings_.insert(DicomTag(0x0040, 0xa730));  /* D */            // Content Sequence
+clearings_.insert(DicomTag(0x0042, 0x0011));  /* D */            // Encapsulated Document
+clearings_.insert(DicomTag(0x0044, 0x0104));  /* D */            // Assertion DateTime
+clearings_.insert(DicomTag(0x0068, 0x6226));  /* D */            // Effective DateTime
+clearings_.insert(DicomTag(0x0068, 0x6270));  /* D */            // Information Issue DateTime
+clearings_.insert(DicomTag(0x006a, 0x0003));  /* D */            // Annotation Group UID
+clearings_.insert(DicomTag(0x006a, 0x0005));  /* D */            // Annotation Group Label
+clearings_.insert(DicomTag(0x0070, 0x0001));  /* D */            // Graphic Annotation Sequence
+clearings_.insert(DicomTag(0x0070, 0x0084));  /* Z/D */          // Content Creator's Name
+clearings_.insert(DicomTag(0x0072, 0x000a));  /* D */            // Hanging Protocol Creation DateTime
+clearings_.insert(DicomTag(0x0072, 0x005e));  /* D */            // Selector AE Value
+clearings_.insert(DicomTag(0x0072, 0x005f));  /* D */            // Selector AS Value
+clearings_.insert(DicomTag(0x0072, 0x0061));  /* D */            // Selector DA Value
+clearings_.insert(DicomTag(0x0072, 0x0063));  /* D */            // Selector DT Value
+clearings_.insert(DicomTag(0x0072, 0x0065));  /* D */            // Selector OB Value
+clearings_.insert(DicomTag(0x0072, 0x0066));  /* D */            // Selector LO Value
+clearings_.insert(DicomTag(0x0072, 0x0068));  /* D */            // Selector LT Value
+clearings_.insert(DicomTag(0x0072, 0x006a));  /* D */            // Selector PN Value
+clearings_.insert(DicomTag(0x0072, 0x006b));  /* D */            // Selector TM Value
+clearings_.insert(DicomTag(0x0072, 0x006c));  /* D */            // Selector SH Value
+clearings_.insert(DicomTag(0x0072, 0x006d));  /* D */            // Selector UN Value
+clearings_.insert(DicomTag(0x0072, 0x006e));  /* D */            // Selector ST Value
+clearings_.insert(DicomTag(0x0072, 0x0070));  /* D */            // Selector UT Value
+clearings_.insert(DicomTag(0x0072, 0x0071));  /* D */            // Selector UR Value
+clearings_.insert(DicomTag(0x0400, 0x0105));  /* D */            // Digital Signature DateTime
+clearings_.insert(DicomTag(0x0400, 0x0115));  /* D */            // Certificate of Signer
+clearings_.insert(DicomTag(0x0400, 0x0562));  /* D */            // Attribute Modification DateTime
+clearings_.insert(DicomTag(0x0400, 0x0563));  /* D */            // Modifying System
+clearings_.insert(DicomTag(0x0400, 0x0564));                     // Source of Previous Values
+clearings_.insert(DicomTag(0x0400, 0x0565));  /* D */            // Reason for the Attribute Modification
+clearings_.insert(DicomTag(0x2100, 0x0140));  /* D */            // Destination AE
+clearings_.insert(DicomTag(0x3006, 0x0002));  /* D */            // Structure Set Label
+clearings_.insert(DicomTag(0x3006, 0x0008));                     // Structure Set Date
+clearings_.insert(DicomTag(0x3006, 0x0009));                     // Structure Set Time
+clearings_.insert(DicomTag(0x3006, 0x0026));                     // ROI Name
+clearings_.insert(DicomTag(0x3006, 0x00a6));                     // ROI Interpreter
+clearings_.insert(DicomTag(0x3008, 0x0024));  /* D */            // Treatment Control Point Date
+clearings_.insert(DicomTag(0x3008, 0x0025));  /* D */            // Treatment Control Point Time
+clearings_.insert(DicomTag(0x3008, 0x0162));  /* D */            // Safe Position Exit Date
+clearings_.insert(DicomTag(0x3008, 0x0164));  /* D */            // Safe Position Exit Time
+clearings_.insert(DicomTag(0x3008, 0x0166));  /* D */            // Safe Position Return Date
+clearings_.insert(DicomTag(0x3008, 0x0168));  /* D */            // Safe Position Return Time
+clearings_.insert(DicomTag(0x300a, 0x0002));  /* D */            // RT Plan Label
+clearings_.insert(DicomTag(0x300a, 0x022c));  /* D */            // Source Strength Reference Date
+clearings_.insert(DicomTag(0x300a, 0x022e));  /* D */            // Source Strength Reference Time
+clearings_.insert(DicomTag(0x300a, 0x0608));  /* D */            // Treatment Position Group Label
+clearings_.insert(DicomTag(0x300a, 0x0611));                     // RT Accessory Holder Slot ID
+clearings_.insert(DicomTag(0x300a, 0x0615));                     // RT Accessory Device Slot ID
+clearings_.insert(DicomTag(0x300a, 0x0619));  /* D */            // Radiation Dose Identification Label
+clearings_.insert(DicomTag(0x300a, 0x0623));  /* D */            // Radiation Dose In-Vivo Measurement Label
+clearings_.insert(DicomTag(0x300a, 0x062a));  /* D */            // RT Tolerance Set Label
+clearings_.insert(DicomTag(0x300a, 0x067c));  /* D */            // Radiation Generation Mode Label
+clearings_.insert(DicomTag(0x300a, 0x067d));                     // Radiation Generation Mode Description
+clearings_.insert(DicomTag(0x300a, 0x0734));  /* D */            // Treatment Tolerance Violation Description
+clearings_.insert(DicomTag(0x300a, 0x0736));  /* D */            // Treatment Tolerance Violation DateTime
+clearings_.insert(DicomTag(0x300a, 0x073a));  /* D */            // Recorded RT Control Point DateTime
+clearings_.insert(DicomTag(0x300a, 0x0741));  /* D */            // Interlock DateTime
+clearings_.insert(DicomTag(0x300a, 0x0742));  /* D */            // Interlock Description
+clearings_.insert(DicomTag(0x300a, 0x0760));  /* D */            // Override DateTime
+clearings_.insert(DicomTag(0x300a, 0x0783));  /* D */            // Interlock Origin Description
+clearings_.insert(DicomTag(0x300c, 0x0127));  /* D */            // Beam Hold Transition DateTime
+clearings_.insert(DicomTag(0x300e, 0x0004));                     // Review Date
+clearings_.insert(DicomTag(0x300e, 0x0005));                     // Review Time
+clearings_.insert(DicomTag(0x3010, 0x000f));                     // Conceptual Volume Combination Description
+clearings_.insert(DicomTag(0x3010, 0x0017));                     // Conceptual Volume Description
+clearings_.insert(DicomTag(0x3010, 0x001b));                     // Device Alternate Identifier
+clearings_.insert(DicomTag(0x3010, 0x002d));  /* D */            // Device Label
+clearings_.insert(DicomTag(0x3010, 0x0033));  /* D */            // User Content Label
+clearings_.insert(DicomTag(0x3010, 0x0034));  /* D */            // User Content Long Label
+clearings_.insert(DicomTag(0x3010, 0x0035));  /* D */            // Entity Label
+clearings_.insert(DicomTag(0x3010, 0x0038));  /* D */            // Entity Long Label
+clearings_.insert(DicomTag(0x3010, 0x0043));                     // Manufacturer's Device Identifier
+clearings_.insert(DicomTag(0x3010, 0x0054));  /* D */            // RT Prescription Label
+clearings_.insert(DicomTag(0x3010, 0x005a));                     // RT Physician Intent Narrative
+clearings_.insert(DicomTag(0x3010, 0x005c));                     // Reason for Superseding
+clearings_.insert(DicomTag(0x3010, 0x007a));                     // Treatment Technique Notes
+clearings_.insert(DicomTag(0x3010, 0x007b));                     // Prescription Notes
+clearings_.insert(DicomTag(0x3010, 0x007f));                     // Fractionation Notes
+clearings_.insert(DicomTag(0x3010, 0x0081));                     // Prescription Notes Sequence
+removals_.insert(DicomTag(0x0000, 0x1000));                      // Affected SOP Instance UID
+removals_.insert(DicomTag(0x0008, 0x0012));   /* X/D */          // Instance Creation Date
+removals_.insert(DicomTag(0x0008, 0x0013));   /* X/Z/D */        // Instance Creation Time
+removals_.insert(DicomTag(0x0008, 0x0015));                      // Instance Coercion DateTime
+removals_.insert(DicomTag(0x0008, 0x0021));   /* X/D */          // Series Date
+removals_.insert(DicomTag(0x0008, 0x0022));   /* X/Z */          // Acquisition Date
+removals_.insert(DicomTag(0x0008, 0x0024));                      // Overlay Date
+removals_.insert(DicomTag(0x0008, 0x0025));                      // Curve Date
+removals_.insert(DicomTag(0x0008, 0x002a));   /* X/Z/D */        // Acquisition DateTime
+removals_.insert(DicomTag(0x0008, 0x0031));   /* X/D */          // Series Time
+removals_.insert(DicomTag(0x0008, 0x0032));   /* X/Z */          // Acquisition Time
+removals_.insert(DicomTag(0x0008, 0x0034));                      // Overlay Time
+removals_.insert(DicomTag(0x0008, 0x0035));                      // Curve Time
+removals_.insert(DicomTag(0x0008, 0x0054));                      // Retrieve AE Title
+removals_.insert(DicomTag(0x0008, 0x0055));                      // Station AE Title
+removals_.insert(DicomTag(0x0008, 0x0080));   /* X/Z/D */        // Institution Name
+removals_.insert(DicomTag(0x0008, 0x0081));                      // Institution Address
+removals_.insert(DicomTag(0x0008, 0x0082));   /* X/Z/D */        // Institution Code Sequence
+removals_.insert(DicomTag(0x0008, 0x0092));                      // Referring Physician's Address
+removals_.insert(DicomTag(0x0008, 0x0094));                      // Referring Physician's Telephone Numbers
+removals_.insert(DicomTag(0x0008, 0x0096));                      // Referring Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x009d));                      // Consulting Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x0201));                      // Timezone Offset From UTC
+removals_.insert(DicomTag(0x0008, 0x1000));                      // Network ID
+removals_.insert(DicomTag(0x0008, 0x1010));   /* X/Z/D */        // Station Name
+removals_.insert(DicomTag(0x0008, 0x1030));                      // Study Description
+removals_.insert(DicomTag(0x0008, 0x103e));                      // Series Description
+removals_.insert(DicomTag(0x0008, 0x1040));                      // Institutional Department Name
+removals_.insert(DicomTag(0x0008, 0x1041));                      // Institutional Department Type Code Sequence
+removals_.insert(DicomTag(0x0008, 0x1048));                      // Physician(s) of Record
+removals_.insert(DicomTag(0x0008, 0x1049));                      // Physician(s) of Record Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1050));                      // Performing Physician's Name
+removals_.insert(DicomTag(0x0008, 0x1052));                      // Performing Physician Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1060));                      // Name of Physician(s) Reading Study
+removals_.insert(DicomTag(0x0008, 0x1062));                      // Physician(s) Reading Study Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1070));   /* X/Z/D */        // Operators' Name
+removals_.insert(DicomTag(0x0008, 0x1072));   /* X/D */          // Operator Identification Sequence
+removals_.insert(DicomTag(0x0008, 0x1080));                      // Admitting Diagnoses Description
+removals_.insert(DicomTag(0x0008, 0x1084));                      // Admitting Diagnoses Code Sequence
+removals_.insert(DicomTag(0x0008, 0x1088));                      // Pyramid Description
+removals_.insert(DicomTag(0x0008, 0x1110));   /* X/Z */          // Referenced Study Sequence
+removals_.insert(DicomTag(0x0008, 0x1111));   /* X/Z/D */        // Referenced Performed Procedure Step Sequence
+removals_.insert(DicomTag(0x0008, 0x1120));                      // Referenced Patient Sequence
+removals_.insert(DicomTag(0x0008, 0x2111));                      // Derivation Description
+removals_.insert(DicomTag(0x0008, 0x4000));                      // Identifying Comments
+removals_.insert(DicomTag(0x0010, 0x0021));                      // Issuer of Patient ID
+removals_.insert(DicomTag(0x0010, 0x0032));                      // Patient's Birth Time
+removals_.insert(DicomTag(0x0010, 0x0050));                      // Patient's Insurance Plan Code Sequence
+removals_.insert(DicomTag(0x0010, 0x0101));                      // Patient's Primary Language Code Sequence
+removals_.insert(DicomTag(0x0010, 0x0102));                      // Patient's Primary Language Modifier Code Sequence
+removals_.insert(DicomTag(0x0010, 0x1000));                      // Other Patient IDs
+removals_.insert(DicomTag(0x0010, 0x1001));                      // Other Patient Names
+removals_.insert(DicomTag(0x0010, 0x1002));                      // Other Patient IDs Sequence
+removals_.insert(DicomTag(0x0010, 0x1005));                      // Patient's Birth Name
+removals_.insert(DicomTag(0x0010, 0x1010));                      // Patient's Age
+removals_.insert(DicomTag(0x0010, 0x1020));                      // Patient's Size
+removals_.insert(DicomTag(0x0010, 0x1030));                      // Patient's Weight
+removals_.insert(DicomTag(0x0010, 0x1040));                      // Patient's Address
+removals_.insert(DicomTag(0x0010, 0x1050));                      // Insurance Plan Identification
+removals_.insert(DicomTag(0x0010, 0x1060));                      // Patient's Mother's Birth Name
+removals_.insert(DicomTag(0x0010, 0x1080));                      // Military Rank
+removals_.insert(DicomTag(0x0010, 0x1081));                      // Branch of Service
+removals_.insert(DicomTag(0x0010, 0x1090));                      // Medical Record Locator
+removals_.insert(DicomTag(0x0010, 0x1100));                      // Referenced Patient Photo Sequence
+removals_.insert(DicomTag(0x0010, 0x2000));                      // Medical Alerts
+removals_.insert(DicomTag(0x0010, 0x2110));                      // Allergies
+removals_.insert(DicomTag(0x0010, 0x2150));                      // Country of Residence
+removals_.insert(DicomTag(0x0010, 0x2152));                      // Region of Residence
+removals_.insert(DicomTag(0x0010, 0x2154));                      // Patient's Telephone Numbers
+removals_.insert(DicomTag(0x0010, 0x2155));                      // Patient's Telecom Information
+removals_.insert(DicomTag(0x0010, 0x2160));                      // Ethnic Group
+removals_.insert(DicomTag(0x0010, 0x2180));                      // Occupation
+removals_.insert(DicomTag(0x0010, 0x21a0));                      // Smoking Status
+removals_.insert(DicomTag(0x0010, 0x21b0));                      // Additional Patient History
+removals_.insert(DicomTag(0x0010, 0x21c0));                      // Pregnancy Status
+removals_.insert(DicomTag(0x0010, 0x21d0));                      // Last Menstrual Date
+removals_.insert(DicomTag(0x0010, 0x21f0));                      // Patient's Religious Preference
+removals_.insert(DicomTag(0x0010, 0x2203));   /* X/Z */          // Patient's Sex Neutered
+removals_.insert(DicomTag(0x0010, 0x2297));                      // Responsible Person
+removals_.insert(DicomTag(0x0010, 0x2299));                      // Responsible Organization
+removals_.insert(DicomTag(0x0010, 0x4000));                      // Patient Comments
+removals_.insert(DicomTag(0x0012, 0x0051));                      // Clinical Trial Time Point Description
+removals_.insert(DicomTag(0x0012, 0x0071));                      // Clinical Trial Series ID
+removals_.insert(DicomTag(0x0012, 0x0072));                      // Clinical Trial Series Description
+removals_.insert(DicomTag(0x0012, 0x0082));                      // Clinical Trial Protocol Ethics Committee Approval Number
+removals_.insert(DicomTag(0x0012, 0x0086));                      // Ethics Committee Approval Effectiveness Start Date
+removals_.insert(DicomTag(0x0012, 0x0087));                      // Ethics Committee Approval Effectiveness End Date
+removals_.insert(DicomTag(0x0014, 0x407c));                      // Calibration Time
+removals_.insert(DicomTag(0x0014, 0x407e));                      // Calibration Date
+removals_.insert(DicomTag(0x0016, 0x002b));                      // Maker Note
+removals_.insert(DicomTag(0x0016, 0x004b));                      // Device Setting Description
+removals_.insert(DicomTag(0x0016, 0x004d));                      // Camera Owner Name
+removals_.insert(DicomTag(0x0016, 0x004e));                      // Lens Specification
+removals_.insert(DicomTag(0x0016, 0x004f));                      // Lens Make
+removals_.insert(DicomTag(0x0016, 0x0050));                      // Lens Model
+removals_.insert(DicomTag(0x0016, 0x0051));                      // Lens Serial Number
+removals_.insert(DicomTag(0x0016, 0x0070));                      // GPS Version ID
+removals_.insert(DicomTag(0x0016, 0x0071));                      // GPS Latitude Ref
+removals_.insert(DicomTag(0x0016, 0x0072));                      // GPS Latitude
+removals_.insert(DicomTag(0x0016, 0x0073));                      // GPS Longitude Ref
+removals_.insert(DicomTag(0x0016, 0x0074));                      // GPS Longitude
+removals_.insert(DicomTag(0x0016, 0x0075));                      // GPS Altitude Ref
+removals_.insert(DicomTag(0x0016, 0x0076));                      // GPS Altitude
+removals_.insert(DicomTag(0x0016, 0x0077));                      // GPS Time Stamp
+removals_.insert(DicomTag(0x0016, 0x0078));                      // GPS Satellites
+removals_.insert(DicomTag(0x0016, 0x0079));                      // GPS Status
+removals_.insert(DicomTag(0x0016, 0x007a));                      // GPS Measure Mode
+removals_.insert(DicomTag(0x0016, 0x007b));                      // GPS DOP
+removals_.insert(DicomTag(0x0016, 0x007c));                      // GPS Speed Ref
+removals_.insert(DicomTag(0x0016, 0x007d));                      // GPS Speed
+removals_.insert(DicomTag(0x0016, 0x007e));                      // GPS Track Ref
+removals_.insert(DicomTag(0x0016, 0x007f));                      // GPS Track
+removals_.insert(DicomTag(0x0016, 0x0080));                      // GPS Img Direction Ref
+removals_.insert(DicomTag(0x0016, 0x0081));                      // GPS Img Direction
+removals_.insert(DicomTag(0x0016, 0x0082));                      // GPS Map Datum
+removals_.insert(DicomTag(0x0016, 0x0083));                      // GPS Dest Latitude Ref
+removals_.insert(DicomTag(0x0016, 0x0084));                      // GPS Dest Latitude
+removals_.insert(DicomTag(0x0016, 0x0085));                      // GPS Dest Longitude Ref
+removals_.insert(DicomTag(0x0016, 0x0086));                      // GPS Dest Longitude
+removals_.insert(DicomTag(0x0016, 0x0087));                      // GPS Dest Bearing Ref
+removals_.insert(DicomTag(0x0016, 0x0088));                      // GPS Dest Bearing
+removals_.insert(DicomTag(0x0016, 0x0089));                      // GPS Dest Distance Ref
+removals_.insert(DicomTag(0x0016, 0x008a));                      // GPS Dest Distance
+removals_.insert(DicomTag(0x0016, 0x008b));                      // GPS Processing Method
+removals_.insert(DicomTag(0x0016, 0x008c));                      // GPS Area Information
+removals_.insert(DicomTag(0x0016, 0x008d));                      // GPS Date Stamp
+removals_.insert(DicomTag(0x0016, 0x008e));                      // GPS Differential
+removals_.insert(DicomTag(0x0018, 0x0027));                      // Intervention Drug Stop Time
+removals_.insert(DicomTag(0x0018, 0x0035));                      // Intervention Drug Start Time
+removals_.insert(DicomTag(0x0018, 0x1000));   /* X/Z/D */        // Device Serial Number
+removals_.insert(DicomTag(0x0018, 0x1004));                      // Plate ID
+removals_.insert(DicomTag(0x0018, 0x1005));                      // Generator ID
+removals_.insert(DicomTag(0x0018, 0x1007));                      // Cassette ID
+removals_.insert(DicomTag(0x0018, 0x1008));                      // Gantry ID
+removals_.insert(DicomTag(0x0018, 0x1009));                      // Unique Device Identifier
+removals_.insert(DicomTag(0x0018, 0x100a));                      // UDI Sequence
+removals_.insert(DicomTag(0x0018, 0x1012));                      // Date of Secondary Capture
+removals_.insert(DicomTag(0x0018, 0x1014));                      // Time of Secondary Capture
+removals_.insert(DicomTag(0x0018, 0x1030));   /* X/D */          // Protocol Name
+removals_.insert(DicomTag(0x0018, 0x1042));                      // Contrast/Bolus Start Time
+removals_.insert(DicomTag(0x0018, 0x1043));                      // Contrast/Bolus Stop Time
+removals_.insert(DicomTag(0x0018, 0x1072));                      // Radiopharmaceutical Start Time
+removals_.insert(DicomTag(0x0018, 0x1073));                      // Radiopharmaceutical Stop Time
+removals_.insert(DicomTag(0x0018, 0x1078));                      // Radiopharmaceutical Start DateTime
+removals_.insert(DicomTag(0x0018, 0x1079));                      // Radiopharmaceutical Stop DateTime
+removals_.insert(DicomTag(0x0018, 0x1200));                      // Date of Last Calibration
+removals_.insert(DicomTag(0x0018, 0x1201));                      // Time of Last Calibration
+removals_.insert(DicomTag(0x0018, 0x1202));                      // DateTime of Last Calibration
+removals_.insert(DicomTag(0x0018, 0x1400));   /* X/D */          // Acquisition Device Processing Description
+removals_.insert(DicomTag(0x0018, 0x4000));                      // Acquisition Comments
+removals_.insert(DicomTag(0x0018, 0x5011));                      // Transducer Identification Sequence
+removals_.insert(DicomTag(0x0018, 0x700a));   /* X/D */          // Detector ID
+removals_.insert(DicomTag(0x0018, 0x700c));   /* X/D */          // Date of Last Detector Calibration
+removals_.insert(DicomTag(0x0018, 0x700e));   /* X/D */          // Time of Last Detector Calibration
+removals_.insert(DicomTag(0x0018, 0x9185));                      // Respiratory Motion Compensation Technique Description
+removals_.insert(DicomTag(0x0018, 0x9373));                      // X-Ray Detector Label
+removals_.insert(DicomTag(0x0018, 0x937b));                      // Multi-energy Acquisition Description
+removals_.insert(DicomTag(0x0018, 0x937f));                      // Decomposition Description
+removals_.insert(DicomTag(0x0018, 0x9424));                      // Acquisition Protocol Description
+removals_.insert(DicomTag(0x0018, 0x9516));   /* X/D */          // Start Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0x9517));   /* X/D */          // End Acquisition DateTime
+removals_.insert(DicomTag(0x0018, 0x9937));                      // Requested Series Description
+removals_.insert(DicomTag(0x0018, 0xa002));                      // Contribution DateTime
+removals_.insert(DicomTag(0x0018, 0xa003));                      // Contribution Description
+removals_.insert(DicomTag(0x0020, 0x0027));                      // Pyramid Label
+removals_.insert(DicomTag(0x0020, 0x3401));                      // Modifying Device ID
+removals_.insert(DicomTag(0x0020, 0x3403));                      // Modified Image Date
+removals_.insert(DicomTag(0x0020, 0x3405));                      // Modified Image Time
+removals_.insert(DicomTag(0x0020, 0x3406));                      // Modified Image Description
+removals_.insert(DicomTag(0x0020, 0x4000));                      // Image Comments
+removals_.insert(DicomTag(0x0020, 0x9158));                      // Frame Comments
+removals_.insert(DicomTag(0x0028, 0x4000));                      // Image Presentation Comments
+removals_.insert(DicomTag(0x0032, 0x0012));                      // Study ID Issuer
+removals_.insert(DicomTag(0x0032, 0x0032));                      // Study Verified Date
+removals_.insert(DicomTag(0x0032, 0x0033));                      // Study Verified Time
+removals_.insert(DicomTag(0x0032, 0x0034));                      // Study Read Date
+removals_.insert(DicomTag(0x0032, 0x0035));                      // Study Read Time
+removals_.insert(DicomTag(0x0032, 0x1000));                      // Scheduled Study Start Date
+removals_.insert(DicomTag(0x0032, 0x1001));                      // Scheduled Study Start Time
+removals_.insert(DicomTag(0x0032, 0x1010));                      // Scheduled Study Stop Date
+removals_.insert(DicomTag(0x0032, 0x1011));                      // Scheduled Study Stop Time
+removals_.insert(DicomTag(0x0032, 0x1020));                      // Scheduled Study Location
+removals_.insert(DicomTag(0x0032, 0x1021));                      // Scheduled Study Location AE Title
+removals_.insert(DicomTag(0x0032, 0x1021));                      // Scheduled Study Location AE Title
+removals_.insert(DicomTag(0x0032, 0x1030));                      // Reason for Study
+removals_.insert(DicomTag(0x0032, 0x1032));                      // Requesting Physician
+removals_.insert(DicomTag(0x0032, 0x1033));                      // Requesting Service
+removals_.insert(DicomTag(0x0032, 0x1040));                      // Study Arrival Date
+removals_.insert(DicomTag(0x0032, 0x1041));                      // Study Arrival Time
+removals_.insert(DicomTag(0x0032, 0x1050));                      // Study Completion Date
+removals_.insert(DicomTag(0x0032, 0x1051));                      // Study Completion Time
+removals_.insert(DicomTag(0x0032, 0x1060));   /* X/Z */          // Requested Procedure Description
+removals_.insert(DicomTag(0x0032, 0x1066));                      // Reason for Visit
+removals_.insert(DicomTag(0x0032, 0x1067));                      // Reason for Visit Code Sequence
+removals_.insert(DicomTag(0x0032, 0x1070));                      // Requested Contrast Agent
+removals_.insert(DicomTag(0x0032, 0x4000));                      // Study Comments
+removals_.insert(DicomTag(0x0038, 0x0004));                      // Referenced Patient Alias Sequence
+removals_.insert(DicomTag(0x0038, 0x0010));                      // Admission ID
+removals_.insert(DicomTag(0x0038, 0x0011));                      // Issuer of Admission ID
+removals_.insert(DicomTag(0x0038, 0x0014));                      // Issuer of Admission ID Sequence
+removals_.insert(DicomTag(0x0038, 0x001a));                      // Scheduled Admission Date
+removals_.insert(DicomTag(0x0038, 0x001b));                      // Scheduled Admission Time
+removals_.insert(DicomTag(0x0038, 0x001c));                      // Scheduled Discharge Date
+removals_.insert(DicomTag(0x0038, 0x001d));                      // Scheduled Discharge Time
+removals_.insert(DicomTag(0x0038, 0x001e));                      // Scheduled Patient Institution Residence
+removals_.insert(DicomTag(0x0038, 0x0020));                      // Admitting Date
+removals_.insert(DicomTag(0x0038, 0x0021));                      // Admitting Time
+removals_.insert(DicomTag(0x0038, 0x0030));                      // Discharge Date
+removals_.insert(DicomTag(0x0038, 0x0032));                      // Discharge Time
+removals_.insert(DicomTag(0x0038, 0x0040));                      // Discharge Diagnosis Description
+removals_.insert(DicomTag(0x0038, 0x0050));                      // Special Needs
+removals_.insert(DicomTag(0x0038, 0x0060));                      // Service Episode ID
+removals_.insert(DicomTag(0x0038, 0x0061));                      // Issuer of Service Episode ID
+removals_.insert(DicomTag(0x0038, 0x0062));                      // Service Episode Description
+removals_.insert(DicomTag(0x0038, 0x0064));                      // Issuer of Service Episode ID Sequence
+removals_.insert(DicomTag(0x0038, 0x0300));                      // Current Patient Location
+removals_.insert(DicomTag(0x0038, 0x0400));                      // Patient's Institution Residence
+removals_.insert(DicomTag(0x0038, 0x0500));                      // Patient State
+removals_.insert(DicomTag(0x0038, 0x4000));                      // Visit Comments
+removals_.insert(DicomTag(0x003a, 0x0329));                      // Waveform Filter Description
+removals_.insert(DicomTag(0x003a, 0x032b));                      // Filter Lookup Table Description
+removals_.insert(DicomTag(0x0040, 0x0001));                      // Scheduled Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0001));                      // Scheduled Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0002));                      // Scheduled Procedure Step Start Date
+removals_.insert(DicomTag(0x0040, 0x0003));                      // Scheduled Procedure Step Start Time
+removals_.insert(DicomTag(0x0040, 0x0004));                      // Scheduled Procedure Step End Date
+removals_.insert(DicomTag(0x0040, 0x0005));                      // Scheduled Procedure Step End Time
+removals_.insert(DicomTag(0x0040, 0x0006));                      // Scheduled Performing Physician's Name
+removals_.insert(DicomTag(0x0040, 0x0007));                      // Scheduled Procedure Step Description
+removals_.insert(DicomTag(0x0040, 0x0009));                      // Scheduled Procedure Step ID
+removals_.insert(DicomTag(0x0040, 0x000b));                      // Scheduled Performing Physician Identification Sequence
+removals_.insert(DicomTag(0x0040, 0x0010));                      // Scheduled Station Name
+removals_.insert(DicomTag(0x0040, 0x0011));                      // Scheduled Procedure Step Location
+removals_.insert(DicomTag(0x0040, 0x0012));                      // Pre-Medication
+removals_.insert(DicomTag(0x0040, 0x0241));                      // Performed Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0241));                      // Performed Station AE Title
+removals_.insert(DicomTag(0x0040, 0x0242));                      // Performed Station Name
+removals_.insert(DicomTag(0x0040, 0x0243));                      // Performed Location
+removals_.insert(DicomTag(0x0040, 0x0244));                      // Performed Procedure Step Start Date
+removals_.insert(DicomTag(0x0040, 0x0245));                      // Performed Procedure Step Start Time
+removals_.insert(DicomTag(0x0040, 0x0250));                      // Performed Procedure Step End Date
+removals_.insert(DicomTag(0x0040, 0x0251));                      // Performed Procedure Step End Time
+removals_.insert(DicomTag(0x0040, 0x0253));                      // Performed Procedure Step ID
+removals_.insert(DicomTag(0x0040, 0x0254));                      // Performed Procedure Step Description
+removals_.insert(DicomTag(0x0040, 0x0275));                      // Request Attributes Sequence
+removals_.insert(DicomTag(0x0040, 0x0280));                      // Comments on the Performed Procedure Step
+removals_.insert(DicomTag(0x0040, 0x0310));                      // Comments on Radiation Dose
+removals_.insert(DicomTag(0x0040, 0x050a));                      // Specimen Accession Number
+removals_.insert(DicomTag(0x0040, 0x051a));                      // Container Description
+removals_.insert(DicomTag(0x0040, 0x0555));   /* X/Z */          // Acquisition Context Sequence
+removals_.insert(DicomTag(0x0040, 0x0600));                      // Specimen Short Description
+removals_.insert(DicomTag(0x0040, 0x0602));                      // Specimen Detailed Description
+removals_.insert(DicomTag(0x0040, 0x06fa));                      // Slide Identifier
+removals_.insert(DicomTag(0x0040, 0x1001));                      // Requested Procedure ID
+removals_.insert(DicomTag(0x0040, 0x1002));                      // Reason for the Requested Procedure
+removals_.insert(DicomTag(0x0040, 0x1004));                      // Patient Transport Arrangements
+removals_.insert(DicomTag(0x0040, 0x1005));                      // Requested Procedure Location
+removals_.insert(DicomTag(0x0040, 0x100a));                      // Reason for Requested Procedure Code Sequence
+removals_.insert(DicomTag(0x0040, 0x1010));                      // Names of Intended Recipients of Results
+removals_.insert(DicomTag(0x0040, 0x1011));                      // Intended Recipients of Results Identification Sequence
+removals_.insert(DicomTag(0x0040, 0x1102));                      // Person's Address
+removals_.insert(DicomTag(0x0040, 0x1103));                      // Person's Telephone Numbers
+removals_.insert(DicomTag(0x0040, 0x1104));                      // Person's Telecom Information
+removals_.insert(DicomTag(0x0040, 0x1400));                      // Requested Procedure Comments
+removals_.insert(DicomTag(0x0040, 0x2001));                      // Reason for the Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2004));                      // Issue Date of Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2005));                      // Issue Time of Imaging Service Request
+removals_.insert(DicomTag(0x0040, 0x2008));                      // Order Entered By
+removals_.insert(DicomTag(0x0040, 0x2009));                      // Order Enterer's Location
+removals_.insert(DicomTag(0x0040, 0x2010));                      // Order Callback Phone Number
+removals_.insert(DicomTag(0x0040, 0x2011));                      // Order Callback Telecom Information
+removals_.insert(DicomTag(0x0040, 0x2400));                      // Imaging Service Request Comments
+removals_.insert(DicomTag(0x0040, 0x3001));                      // Confidentiality Constraint on Patient Data Description
+removals_.insert(DicomTag(0x0040, 0x4005));                      // Scheduled Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4008));                      // Scheduled Procedure Step Expiration DateTime
+removals_.insert(DicomTag(0x0040, 0x4010));                      // Scheduled Procedure Step Modification DateTime
+removals_.insert(DicomTag(0x0040, 0x4011));                      // Expected Completion DateTime
+removals_.insert(DicomTag(0x0040, 0x4025));                      // Scheduled Station Name Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4027));                      // Scheduled Station Geographic Location Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4028));                      // Performed Station Name Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4030));                      // Performed Station Geographic Location Code Sequence
+removals_.insert(DicomTag(0x0040, 0x4034));                      // Scheduled Human Performers Sequence
+removals_.insert(DicomTag(0x0040, 0x4035));                      // Actual Human Performers Sequence
+removals_.insert(DicomTag(0x0040, 0x4036));                      // Human Performer's Organization
+removals_.insert(DicomTag(0x0040, 0x4037));                      // Human Performer's Name
+removals_.insert(DicomTag(0x0040, 0x4050));                      // Performed Procedure Step Start DateTime
+removals_.insert(DicomTag(0x0040, 0x4051));                      // Performed Procedure Step End DateTime
+removals_.insert(DicomTag(0x0040, 0x4052));                      // Procedure Step Cancellation DateTime
+removals_.insert(DicomTag(0x0040, 0xa023));                      // Findings Group Recording Date (Trial)
+removals_.insert(DicomTag(0x0040, 0xa024));                      // Findings Group Recording Time (Trial)
+removals_.insert(DicomTag(0x0040, 0xa032));   /* X/D */          // Observation DateTime
+removals_.insert(DicomTag(0x0040, 0xa033));                      // Observation Start DateTime
+removals_.insert(DicomTag(0x0040, 0xa078));                      // Author Observer Sequence
+removals_.insert(DicomTag(0x0040, 0xa07a));                      // Participant Sequence
+removals_.insert(DicomTag(0x0040, 0xa07c));                      // Custodial Organization Sequence
+removals_.insert(DicomTag(0x0040, 0xa110));                      // Date of Document or Verbal Transaction (Trial)
+removals_.insert(DicomTag(0x0040, 0xa112));                      // Time of Document or Verbal Transaction (Trial)
+removals_.insert(DicomTag(0x0040, 0xa192));                      // Observation Date (Trial)
+removals_.insert(DicomTag(0x0040, 0xa193));                      // Observation Time (Trial)
+removals_.insert(DicomTag(0x0040, 0xa307));                      // Current Observer (Trial)
+removals_.insert(DicomTag(0x0040, 0xa352));                      // Verbal Source (Trial)
+removals_.insert(DicomTag(0x0040, 0xa353));                      // Address (Trial)
+removals_.insert(DicomTag(0x0040, 0xa354));                      // Telephone Number (Trial)
+removals_.insert(DicomTag(0x0040, 0xa358));                      // Verbal Source Identifier Code Sequence (Trial)
+removals_.insert(DicomTag(0x0040, 0xdb06));                      // Template Version
+removals_.insert(DicomTag(0x0040, 0xdb07));                      // Template Local Version
+removals_.insert(DicomTag(0x0040, 0xe004));                      // HL7 Document Effective Time
+removals_.insert(DicomTag(0x0044, 0x0004));                      // Approval Status DateTime
+removals_.insert(DicomTag(0x0044, 0x000b));                      // Product Expiration DateTime
+removals_.insert(DicomTag(0x0044, 0x0010));                      // Substance Administration DateTime
+removals_.insert(DicomTag(0x0044, 0x0105));                      // Assertion Expiration DateTime
+removals_.insert(DicomTag(0x0050, 0x001b));                      // Container Component ID
+removals_.insert(DicomTag(0x0050, 0x0020));                      // Device Description
+removals_.insert(DicomTag(0x0050, 0x0021));                      // Long Device Description
+removals_.insert(DicomTag(0x006a, 0x0006));                      // Annotation Group Description
+removals_.insert(DicomTag(0x0070, 0x0082));                      // Presentation Creation Date
+removals_.insert(DicomTag(0x0070, 0x0083));                      // Presentation Creation Time
+removals_.insert(DicomTag(0x0070, 0x0086));                      // Content Creator's Identification Code Sequence
+removals_.insert(DicomTag(0x0074, 0x1234));                      // Receiving AE
+removals_.insert(DicomTag(0x0074, 0x1236));                      // Requesting AE
+removals_.insert(DicomTag(0x0088, 0x0200));                      // Icon Image Sequence
+removals_.insert(DicomTag(0x0088, 0x0904));                      // Topic Title
+removals_.insert(DicomTag(0x0088, 0x0906));                      // Topic Subject
+removals_.insert(DicomTag(0x0088, 0x0910));                      // Topic Author
+removals_.insert(DicomTag(0x0088, 0x0912));                      // Topic Keywords
+removals_.insert(DicomTag(0x0100, 0x0420));                      // SOP Authorization DateTime
+removals_.insert(DicomTag(0x0400, 0x0310));                      // Certified Timestamp
+removals_.insert(DicomTag(0x0400, 0x0402));                      // Referenced Digital Signature Sequence
+removals_.insert(DicomTag(0x0400, 0x0403));                      // Referenced SOP Instance MAC Sequence
+removals_.insert(DicomTag(0x0400, 0x0404));                      // MAC
+removals_.insert(DicomTag(0x0400, 0x0550));                      // Modified Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0551));                      // Nonconforming Modified Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0552));                      // Nonconforming Data Element Value
+removals_.insert(DicomTag(0x0400, 0x0561));                      // Original Attributes Sequence
+removals_.insert(DicomTag(0x0400, 0x0600));                      // Instance Origin Status
+removals_.insert(DicomTag(0x2030, 0x0020));                      // Text String
+removals_.insert(DicomTag(0x2100, 0x0040));                      // Creation Date
+removals_.insert(DicomTag(0x2100, 0x0050));                      // Creation Time
+removals_.insert(DicomTag(0x2100, 0x0070));                      // Originator
+removals_.insert(DicomTag(0x2200, 0x0002));   /* X/Z */          // Label Text
+removals_.insert(DicomTag(0x2200, 0x0005));   /* X/Z */          // Barcode Value
+removals_.insert(DicomTag(0x3002, 0x0121));                      // Position Acquisition Template Name
+removals_.insert(DicomTag(0x3002, 0x0123));                      // Position Acquisition Template Description
+removals_.insert(DicomTag(0x3006, 0x0004));                      // Structure Set Name
+removals_.insert(DicomTag(0x3006, 0x0006));                      // Structure Set Description
+removals_.insert(DicomTag(0x3006, 0x0028));                      // ROI Description
+removals_.insert(DicomTag(0x3006, 0x0038));                      // ROI Generation Description
+removals_.insert(DicomTag(0x3006, 0x0085));                      // ROI Observation Label
+removals_.insert(DicomTag(0x3006, 0x0088));                      // ROI Observation Description
+removals_.insert(DicomTag(0x3008, 0x0054));   /* X/D */          // First Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0056));   /* X/D */          // Most Recent Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0105));   /* X/Z */          // Source Serial Number
+removals_.insert(DicomTag(0x3008, 0x0250));   /* X/D */          // Treatment Date
+removals_.insert(DicomTag(0x3008, 0x0251));   /* X/D */          // Treatment Time
+removals_.insert(DicomTag(0x300a, 0x0003));                      // RT Plan Name
+removals_.insert(DicomTag(0x300a, 0x0004));                      // RT Plan Description
+removals_.insert(DicomTag(0x300a, 0x0006));   /* X/D */          // RT Plan Date
+removals_.insert(DicomTag(0x300a, 0x0007));   /* X/D */          // RT Plan Time
+removals_.insert(DicomTag(0x300a, 0x000b));                      // Treatment Sites
+removals_.insert(DicomTag(0x300a, 0x000e));                      // Prescription Description
+removals_.insert(DicomTag(0x300a, 0x0016));                      // Dose Reference Description
+removals_.insert(DicomTag(0x300a, 0x0072));                      // Fraction Group Description
+removals_.insert(DicomTag(0x300a, 0x00b2));   /* X/Z */          // Treatment Machine Name
+removals_.insert(DicomTag(0x300a, 0x00c3));                      // Beam Description
+removals_.insert(DicomTag(0x300a, 0x00dd));                      // Bolus Description
+removals_.insert(DicomTag(0x300a, 0x0196));                      // Fixation Device Description
+removals_.insert(DicomTag(0x300a, 0x01a6));                      // Shielding Device Description
+removals_.insert(DicomTag(0x300a, 0x01b2));                      // Setup Technique Description
+removals_.insert(DicomTag(0x300a, 0x0216));                      // Source Manufacturer
+removals_.insert(DicomTag(0x300a, 0x02eb));                      // Compensator Description
+removals_.insert(DicomTag(0x300a, 0x0676));                      // Equipment Frame of Reference Description
+removals_.insert(DicomTag(0x300a, 0x078e));                      // Patient Treatment Preparation Procedure Parameter Description
+removals_.insert(DicomTag(0x300a, 0x0792));                      // Patient Treatment Preparation Method Description
+removals_.insert(DicomTag(0x300a, 0x0794));                      // Patient Setup Photo Description
+removals_.insert(DicomTag(0x300a, 0x079a));                      // Displacement Reference Label
+removals_.insert(DicomTag(0x300c, 0x0113));                      // Reason for Omission Description
+removals_.insert(DicomTag(0x300e, 0x0008));   /* X/Z */          // Reviewer Name
+removals_.insert(DicomTag(0x3010, 0x0036));                      // Entity Name
+removals_.insert(DicomTag(0x3010, 0x0037));                      // Entity Description
+removals_.insert(DicomTag(0x3010, 0x004c));   /* X/D */          // Intended Phase Start Date
+removals_.insert(DicomTag(0x3010, 0x004d));   /* X/D */          // Intended Phase End Date
+removals_.insert(DicomTag(0x3010, 0x0056));   /* X/D */          // RT Treatment Approach Label
+removals_.insert(DicomTag(0x3010, 0x0061));                      // Prior Treatment Dose Description
+removals_.insert(DicomTag(0x3010, 0x0077));   /* X/D */          // Treatment Site
+removals_.insert(DicomTag(0x3010, 0x0085));                      // Intended Fraction Start Time
+removals_.insert(DicomTag(0x4000, 0x0010));                      // Arbitrary
+removals_.insert(DicomTag(0x4000, 0x4000));                      // Text Comments
+removals_.insert(DicomTag(0x4008, 0x0040));                      // Results ID
+removals_.insert(DicomTag(0x4008, 0x0042));                      // Results ID Issuer
+removals_.insert(DicomTag(0x4008, 0x0100));                      // Interpretation Recorded Date
+removals_.insert(DicomTag(0x4008, 0x0101));                      // Interpretation Recorded Time
+removals_.insert(DicomTag(0x4008, 0x0102));                      // Interpretation Recorder
+removals_.insert(DicomTag(0x4008, 0x0108));                      // Interpretation Transcription Date
+removals_.insert(DicomTag(0x4008, 0x0109));                      // Interpretation Transcription Time
+removals_.insert(DicomTag(0x4008, 0x010a));                      // Interpretation Transcriber
+removals_.insert(DicomTag(0x4008, 0x010b));                      // Interpretation Text
+removals_.insert(DicomTag(0x4008, 0x010c));                      // Interpretation Author
+removals_.insert(DicomTag(0x4008, 0x0111));                      // Interpretation Approver Sequence
+removals_.insert(DicomTag(0x4008, 0x0112));                      // Interpretation Approval Date
+removals_.insert(DicomTag(0x4008, 0x0113));                      // Interpretation Approval Time
+removals_.insert(DicomTag(0x4008, 0x0114));                      // Physician Approving Interpretation
+removals_.insert(DicomTag(0x4008, 0x0115));                      // Interpretation Diagnosis Description
+removals_.insert(DicomTag(0x4008, 0x0118));                      // Results Distribution List Sequence
+removals_.insert(DicomTag(0x4008, 0x0119));                      // Distribution Name
+removals_.insert(DicomTag(0x4008, 0x011a));                      // Distribution Address
+removals_.insert(DicomTag(0x4008, 0x0200));                      // Interpretation ID
+removals_.insert(DicomTag(0x4008, 0x0202));                      // Interpretation ID Issuer
+removals_.insert(DicomTag(0x4008, 0x0300));                      // Impressions
+removals_.insert(DicomTag(0x4008, 0x4000));                      // Results Comments
+removals_.insert(DicomTag(0xfffa, 0xfffa));                      // Digital Signatures Sequence
+removals_.insert(DicomTag(0xfffc, 0xfffc));                      // Data Set Trailing Padding
+removedRanges_.push_back(DicomTagRange(0x5000, 0x50ff, 0x0000, 0xffff));  // Curve Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x3000, 0x3000));  // Overlay Data
+removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x4000, 0x4000));  // Overlay Comments
+uids_.insert(DicomTag(0x0000, 0x1001));                          // Requested SOP Instance UID
+uids_.insert(DicomTag(0x0002, 0x0003));                          // Media Storage SOP Instance UID
+uids_.insert(DicomTag(0x0004, 0x1511));                          // Referenced SOP Instance UID in File
+uids_.insert(DicomTag(0x0008, 0x0014));                          // Instance Creator UID
+uids_.insert(DicomTag(0x0008, 0x0017));                          // Acquisition UID
+uids_.insert(DicomTag(0x0008, 0x0019));                          // Pyramid UID
+uids_.insert(DicomTag(0x0008, 0x0058));                          // Failed SOP Instance UID List
+uids_.insert(DicomTag(0x0008, 0x1155));                          // Referenced SOP Instance UID
+uids_.insert(DicomTag(0x0008, 0x1195));                          // Transaction UID
+uids_.insert(DicomTag(0x0008, 0x3010));                          // Irradiation Event UID
+uids_.insert(DicomTag(0x0018, 0x1002));                          // Device UID
+uids_.insert(DicomTag(0x0018, 0x100b));                          // Manufacturer's Device Class UID
+uids_.insert(DicomTag(0x0018, 0x2042));                          // Target UID
+uids_.insert(DicomTag(0x0020, 0x0052));                          // Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x0200));                          // Synchronization Frame of Reference UID
+uids_.insert(DicomTag(0x0020, 0x9161));                          // Concatenation UID
+uids_.insert(DicomTag(0x0020, 0x9164));                          // Dimension Organization UID
+uids_.insert(DicomTag(0x0028, 0x1199));                          // Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x0028, 0x1214));                          // Large Palette Color Lookup Table UID
+uids_.insert(DicomTag(0x003a, 0x0310));                          // Multiplex Group UID
+uids_.insert(DicomTag(0x0040, 0x0554));                          // Specimen UID
+uids_.insert(DicomTag(0x0040, 0x4023));                          // Referenced General Purpose Scheduled Procedure Step Transaction UID
+uids_.insert(DicomTag(0x0040, 0xa124));                          // UID
+uids_.insert(DicomTag(0x0040, 0xa171));                          // Observation UID
+uids_.insert(DicomTag(0x0040, 0xa172));                          // Referenced Observation UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xa402));                          // Observation Subject UID (Trial)
+uids_.insert(DicomTag(0x0040, 0xdb0c));                          // Template Extension Organization UID
+uids_.insert(DicomTag(0x0040, 0xdb0d));                          // Template Extension Creator UID
+uids_.insert(DicomTag(0x0062, 0x0021));                          // Tracking UID
+uids_.insert(DicomTag(0x0064, 0x0003));                          // Source Frame of Reference UID
+uids_.insert(DicomTag(0x0070, 0x031a));                          // Fiducial UID
+uids_.insert(DicomTag(0x0070, 0x1101));                          // Presentation Display Collection UID
+uids_.insert(DicomTag(0x0070, 0x1102));                          // Presentation Sequence Collection UID
+uids_.insert(DicomTag(0x0088, 0x0140));                          // Storage Media File-set UID
+uids_.insert(DicomTag(0x0400, 0x0100));                          // Digital Signature UID
+uids_.insert(DicomTag(0x3006, 0x0024));                          // Referenced Frame of Reference UID
+uids_.insert(DicomTag(0x3006, 0x00c2));                          // Related Frame of Reference UID
+uids_.insert(DicomTag(0x300a, 0x0013));                          // Dose Reference UID
+uids_.insert(DicomTag(0x300a, 0x0083));                          // Referenced Dose Reference UID
+uids_.insert(DicomTag(0x300a, 0x0609));                          // Treatment Position Group UID
+uids_.insert(DicomTag(0x300a, 0x0650));                          // Patient Setup UID
+uids_.insert(DicomTag(0x300a, 0x0700));                          // Treatment Session UID
+uids_.insert(DicomTag(0x300a, 0x0785));                          // Referenced Treatment Position Group UID
+uids_.insert(DicomTag(0x3010, 0x0006));                          // Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x000b));                          // Referenced Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0013));                          // Constituent Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0015));                          // Source Conceptual Volume UID
+uids_.insert(DicomTag(0x3010, 0x0031));                          // Referenced Fiducials UID
+uids_.insert(DicomTag(0x3010, 0x003b));                          // RT Treatment Phase UID
+uids_.insert(DicomTag(0x3010, 0x006e));                          // Dosimetric Objective UID
+uids_.insert(DicomTag(0x3010, 0x006f));                          // Referenced Dosimetric Objective UID
diff --git a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp
new file mode 100644
index 0000000..053b741
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp
@@ -0,0 +1,770 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomWebJsonVisitor.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "../SerializationToolbox.h"
+#include "FromDcmtkBridge.h"
+
+#include 
+#include 
+
+
+static const char* const KEY_ALPHABETIC = "Alphabetic";
+static const char* const KEY_IDEOGRAPHIC = "Ideographic";
+static const char* const KEY_PHONETIC = "Phonetic";
+static const char* const KEY_BULK_DATA = "BulkData";
+static const char* const KEY_BULK_DATA_URI = "BulkDataURI";
+static const char* const KEY_INLINE_BINARY = "InlineBinary";
+static const char* const KEY_SQ = "SQ";
+static const char* const KEY_TAG = "tag";
+static const char* const KEY_VALUE = "Value";
+static const char* const KEY_VR = "vr";
+static const char* const KEY_NUMBER = "number";
+
+
+namespace Orthanc
+{
+#if ORTHANC_ENABLE_PUGIXML == 1
+  static void DecomposeXmlPersonName(pugi::xml_node& target,
+                                     const std::string& source)
+  {
+    std::vector tokens;
+    Toolbox::TokenizeString(tokens, source, '^');
+
+    if (tokens.size() >= 1)
+    {
+      target.append_child("FamilyName").text() = tokens[0].c_str();
+    }
+            
+    if (tokens.size() >= 2)
+    {
+      target.append_child("GivenName").text() = tokens[1].c_str();
+    }
+            
+    if (tokens.size() >= 3)
+    {
+      target.append_child("MiddleName").text() = tokens[2].c_str();
+    }
+            
+    if (tokens.size() >= 4)
+    {
+      target.append_child("NamePrefix").text() = tokens[3].c_str();
+    }
+            
+    if (tokens.size() >= 5)
+    {
+      target.append_child("NameSuffix").text() = tokens[4].c_str();
+    }
+  }
+  
+  static void ExploreXmlDataset(pugi::xml_node& target,
+                                const Json::Value& source)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
+    assert(source.type() == Json::objectValue);
+
+    Json::Value::Members members = source.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const DicomTag tag = FromDcmtkBridge::ParseTag(members[i]);
+      const Json::Value& content = source[members[i]];
+
+      assert(content.type() == Json::objectValue &&
+             content.isMember(KEY_VR) &&
+             content[KEY_VR].type() == Json::stringValue);
+      const std::string vr = content[KEY_VR].asString();
+
+      const std::string keyword = FromDcmtkBridge::GetTagName(tag, "");
+    
+      pugi::xml_node node = target.append_child("DicomAttribute");
+      node.append_attribute(KEY_TAG).set_value(members[i].c_str());
+      node.append_attribute(KEY_VR).set_value(vr.c_str());
+
+      if (keyword != std::string(DcmTag_ERROR_TagName))
+      {
+        node.append_attribute("keyword").set_value(keyword.c_str());
+      }   
+
+      if (content.isMember(KEY_VALUE))
+      {
+        assert(content[KEY_VALUE].type() == Json::arrayValue);
+        
+        for (Json::Value::ArrayIndex j = 0; j < content[KEY_VALUE].size(); j++)
+        {
+          std::string number = boost::lexical_cast(j + 1);
+
+          if (vr == "SQ")
+          {
+            if (content[KEY_VALUE][j].type() == Json::objectValue)
+            {
+              pugi::xml_node child = node.append_child("Item");
+              child.append_attribute(KEY_NUMBER).set_value(number.c_str());
+              ExploreXmlDataset(child, content[KEY_VALUE][j]);
+            }
+          }
+          if (vr == "PN")
+          {
+            bool hasAlphabetic = (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
+                                  content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue);
+
+            bool hasIdeographic = (content[KEY_VALUE][j].isMember(KEY_IDEOGRAPHIC) &&
+                                   content[KEY_VALUE][j][KEY_IDEOGRAPHIC].type() == Json::stringValue);
+
+            bool hasPhonetic = (content[KEY_VALUE][j].isMember(KEY_PHONETIC) &&
+                                content[KEY_VALUE][j][KEY_PHONETIC].type() == Json::stringValue);
+
+            if (hasAlphabetic ||
+                hasIdeographic ||
+                hasPhonetic)
+            {
+              pugi::xml_node child = node.append_child("PersonName");
+              child.append_attribute(KEY_NUMBER).set_value(number.c_str());
+
+              if (hasAlphabetic)
+              {
+                pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_ALPHABETIC].asString());
+              }
+
+              if (hasIdeographic)
+              {
+                pugi::xml_node name = child.append_child(KEY_IDEOGRAPHIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_IDEOGRAPHIC].asString());
+              }
+
+              if (hasPhonetic)
+              {
+                pugi::xml_node name = child.append_child(KEY_PHONETIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_PHONETIC].asString());
+              }
+            }
+          }
+          else
+          {
+            pugi::xml_node child = node.append_child(KEY_VALUE);
+            child.append_attribute(KEY_NUMBER).set_value(number.c_str());
+
+            switch (content[KEY_VALUE][j].type())
+            {
+              case Json::stringValue:
+                child.text() = content[KEY_VALUE][j].asCString();
+                break;
+
+              case Json::realValue:
+                child.text() = content[KEY_VALUE][j].asFloat();
+                break;
+
+              case Json::intValue:
+                child.text() = content[KEY_VALUE][j].asInt();
+                break;
+
+              case Json::uintValue:
+                child.text() = content[KEY_VALUE][j].asUInt();
+                break;
+
+              default:
+                break;
+            }
+          }
+        }
+      }
+      else if (content.isMember(KEY_BULK_DATA_URI) &&
+               content[KEY_BULK_DATA_URI].type() == Json::stringValue)
+      {
+        pugi::xml_node child = node.append_child(KEY_BULK_DATA);
+        child.append_attribute("URI").set_value(content[KEY_BULK_DATA_URI].asCString());
+      }
+      else if (content.isMember(KEY_INLINE_BINARY) &&
+               content[KEY_INLINE_BINARY].type() == Json::stringValue)
+      {
+        pugi::xml_node child = node.append_child(KEY_INLINE_BINARY);
+        child.text() = content[KEY_INLINE_BINARY].asCString();
+      }
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  static void DicomWebJsonToXml(pugi::xml_document& target,
+                                const Json::Value& source)
+  {
+    pugi::xml_node root = target.append_child("NativeDicomModel");
+    root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
+
+    ExploreXmlDataset(root, source);
+
+    pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+  }
+#endif
+
+
+  std::string DicomWebJsonVisitor::FormatTag(const DicomTag& tag)
+  {
+    char buf[16];
+    sprintf(buf, "%04X%04X", tag.GetGroup(), tag.GetElement());
+    return std::string(buf);
+  }
+
+    
+  Json::Value& DicomWebJsonVisitor::CreateNode(const std::vector& parentTags,
+                                               const std::vector& parentIndexes,
+                                               const DicomTag& tag)
+  {
+    assert(parentTags.size() == parentIndexes.size());      
+
+    Json::Value* node = &result_;
+
+    for (size_t i = 0; i < parentTags.size(); i++)
+    {
+      std::string t = FormatTag(parentTags[i]);
+
+      if (!node->isMember(t))
+      {
+        Json::Value item = Json::objectValue;
+        item[KEY_VR] = KEY_SQ;
+        item[KEY_VALUE] = Json::arrayValue;
+        item[KEY_VALUE].append(Json::objectValue);
+        (*node) [t] = item;
+
+        node = &(*node)[t][KEY_VALUE][0];
+      }
+      else if ((*node)  [t].type() != Json::objectValue ||
+               !(*node) [t].isMember(KEY_VR) ||
+               (*node)  [t][KEY_VR].type() != Json::stringValue ||
+               (*node)  [t][KEY_VR].asString() != KEY_SQ ||
+               !(*node) [t].isMember(KEY_VALUE) ||
+               (*node)  [t][KEY_VALUE].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        size_t currentSize = (*node) [t][KEY_VALUE].size();
+
+        if (parentIndexes[i] < currentSize)
+        {
+          // The node already exists
+        }
+        else if (parentIndexes[i] == currentSize)
+        {
+          (*node) [t][KEY_VALUE].append(Json::objectValue);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+          
+        node = &(*node) [t][KEY_VALUE][Json::ArrayIndex(parentIndexes[i])];
+      }
+    }
+
+    assert(node->type() == Json::objectValue);
+
+    std::string t = FormatTag(tag);
+    if (node->isMember(t))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      (*node) [t] = Json::objectValue;
+      return (*node) [t];
+    }
+  }
+
+    
+  Json::Value DicomWebJsonVisitor::FormatInteger(int64_t value)
+  {
+    if (value < 0)
+    {
+      return Json::Value(static_cast(value));
+    }
+    else
+    {
+      return Json::Value(static_cast(value));
+    }
+  }
+
+    
+  Json::Value DicomWebJsonVisitor::FormatDouble(double value)
+  {
+    try
+    {
+      long long a = boost::math::llround(value);
+
+      double d = fabs(value - static_cast(a));
+
+      if (d <= std::numeric_limits::epsilon() * 100.0)
+      {
+        return FormatInteger(a);
+      }
+      else
+      {
+        return Json::Value(value);
+      }
+    }
+    catch (boost::math::rounding_error&)
+    {
+      // Can occur if "long long" is too small to receive this value
+      // (e.g. infinity)
+      return Json::Value(value);
+    }
+  }
+
+  Json::Value DicomWebJsonVisitor::FormatDecimalString(double value, const std::string& originalString)
+  {
+    try
+    {
+      long long a = boost::math::llround(value);
+
+      double d = fabs(value - static_cast(a));
+
+      if (d <= std::numeric_limits::epsilon() * 100.0)
+      {
+        return FormatInteger(a);  // if the decimal number is an integer, you can represent it as an integer  
+      }
+      else
+      {
+        return Json::Value(originalString);  // keep the original string to avoid rounding errors e.g, transforming "0.143" into 0.14299999999999
+      }
+    }
+    catch (boost::math::rounding_error&)
+    {
+      // Can occur if "long long" is too small to receive this value
+      // (e.g. infinity)
+      return Json::Value(originalString);
+    }
+  }
+
+  DicomWebJsonVisitor::DicomWebJsonVisitor() :
+    formatter_(NULL)
+  {
+    Clear();
+  }
+
+  void DicomWebJsonVisitor::SetFormatter(DicomWebJsonVisitor::IBinaryFormatter &formatter)
+  {
+    formatter_ = &formatter;
+  }
+
+  void DicomWebJsonVisitor::Clear()
+  {
+    result_ = Json::objectValue;
+  }
+
+  const Json::Value &DicomWebJsonVisitor::GetResult() const
+  {
+    return result_;
+  }
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  void DicomWebJsonVisitor::FormatXml(std::string& target) const
+  {
+    pugi::xml_document doc;
+    DicomWebJsonToXml(doc, result_);
+    Toolbox::XmlToString(target, doc);
+  }
+#endif
+
+
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitNotSupported(const std::vector &parentTags,
+                                         const std::vector &parentIndexes,
+                                         const DicomTag &tag,
+                                         ValueRepresentation vr)
+  {
+    return Action_None;
+  }
+
+
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitSequence(const std::vector& parentTags,
+                                     const std::vector& parentIndexes,
+                                     const DicomTag& tag,
+                                     size_t countItems)
+  {
+    if (countItems == 0 &&
+        tag.GetElement() != 0x0000)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence);
+    }
+
+    return Action_None;
+  }
+  
+
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitBinary(const std::vector& parentTags,
+                                   const std::vector& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr,
+                                   const void* data,
+                                   size_t size)
+  {
+    assert(vr == ValueRepresentation_OtherByte ||
+           vr == ValueRepresentation_OtherDouble ||
+           vr == ValueRepresentation_OtherFloat ||
+           vr == ValueRepresentation_OtherLong ||
+           vr == ValueRepresentation_OtherWord ||
+           vr == ValueRepresentation_Unknown);
+
+    if (tag.GetElement() != 0x0000)
+    {
+      BinaryMode mode;
+      std::string bulkDataUri;
+        
+      if (formatter_ == NULL)
+      {
+        mode = BinaryMode_InlineBinary;
+      }
+      else
+      {
+        mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
+      }
+
+      if (mode != BinaryMode_Ignore)
+      {
+        Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+        node[KEY_VR] = EnumerationToString(vr);
+
+        /**
+         * The test on "size > 0" is new in Orthanc 1.9.3, and fixes
+         * issue #195 (No need for BulkDataURI when Data Element is
+         * empty): https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=195
+         **/
+        if (size > 0 ||
+            tag == DICOM_TAG_PIXEL_DATA ||
+            vr == ValueRepresentation_Sequence /* new in Orthanc 1.9.4 */)
+        {
+          switch (mode)
+          {
+            case BinaryMode_BulkDataUri:
+              node[KEY_BULK_DATA_URI] = bulkDataUri;
+              break;
+
+            case BinaryMode_InlineBinary:
+            {
+              std::string tmp(static_cast(data), size);
+          
+              std::string base64;
+              Toolbox::EncodeBase64(base64, tmp);
+
+              node[KEY_INLINE_BINARY] = base64;
+              break;
+            }
+
+            default:
+              throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
+        }
+      }
+    }
+
+    return Action_None;
+  }
+
+
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitIntegers(const std::vector& parentTags,
+                                     const std::vector& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr,
+                                     const std::vector& values)
+  {
+    if (tag.GetElement() != 0x0000 &&
+        vr != ValueRepresentation_NotSupported)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatInteger(values[i]));
+        }
+
+        node[KEY_VALUE] = content;
+      }
+    }
+
+    return Action_None;
+  }
+
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitDoubles(const std::vector& parentTags,
+                                    const std::vector& parentIndexes,
+                                    const DicomTag& tag,
+                                    ValueRepresentation vr,
+                                    const std::vector& values)
+  {
+    if (tag.GetElement() != 0x0000 &&
+        vr != ValueRepresentation_NotSupported)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatDouble(values[i]));
+        }
+          
+        node[KEY_VALUE] = content;
+      }
+    }
+
+    return Action_None;
+  }
+
+  
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitAttributes(const std::vector& parentTags,
+                                       const std::vector& parentIndexes,
+                                       const DicomTag& tag,
+                                       const std::vector& values)
+  {
+    if (tag.GetElement() != 0x0000)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(ValueRepresentation_AttributeTag);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatTag(values[i]));
+        }
+          
+        node[KEY_VALUE] = content;
+      }
+    }
+
+    return Action_None;
+  }
+
+  
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitString(std::string& newValue,
+                                   const std::vector& parentTags,
+                                   const std::vector& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr,
+                                   const std::string& value)
+  {
+    if (tag.GetElement() == 0x0000 ||
+        vr == ValueRepresentation_NotSupported)
+    {
+      return Action_None;
+    }
+    else
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+#if 0
+      /**
+       * TODO - The JSON file has an UTF-8 encoding, thus DCMTK
+       * replaces the specific character set with "ISO_IR 192"
+       * (UNICODE UTF-8). On Google Cloud Healthcare, however, the
+       * source encoding is reported, which seems more logical. We
+       * thus choose the Google convention. Enabling this block will
+       * mimic the DCMTK behavior.
+       **/
+      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        node[KEY_VALUE].append("ISO_IR 192");
+      }
+      else
+#endif
+      {
+        std::string truncated;
+        
+        if (!value.empty() &&
+            value[value.size() - 1] == '\0')
+        {
+          truncated = value.substr(0, value.size() - 1);
+        }
+        else
+        {
+          truncated = value;
+        }
+
+        if (!truncated.empty())
+        {
+          std::vector tokens;
+          Toolbox::TokenizeString(tokens, truncated, '\\');
+
+          if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET &&
+              tokens.size() > 1 &&
+              tokens[0].empty())
+          {
+            // Specific character set with code extension: Remove the
+            // first element from the vector of encodings
+            tokens.erase(tokens.begin());
+          }
+
+          node[KEY_VALUE] = Json::arrayValue;
+          for (size_t i = 0; i < tokens.size(); i++)
+          {
+            try
+            {
+              switch (vr)
+              {
+                case ValueRepresentation_PersonName:
+                {
+                  Json::Value tmp = Json::objectValue;
+                  if (!tokens[i].empty())
+                  {
+                    std::vector components;
+                    Toolbox::TokenizeString(components, tokens[i], '=');
+
+                    if (components.size() >= 1)
+                    {
+                      tmp[KEY_ALPHABETIC] = components[0];
+                    }
+
+                    if (components.size() >= 2)
+                    {
+                      tmp[KEY_IDEOGRAPHIC] = components[1];
+                    }
+
+                    if (components.size() >= 3)
+                    {
+                      tmp[KEY_PHONETIC] = components[2];
+                    }
+                  }
+                  
+                  node[KEY_VALUE].append(tmp);
+                  break;
+                }
+                  
+                case ValueRepresentation_IntegerString:
+                {
+                  /**
+                   * The calls to "StripSpaces()" below fix the
+                   * issue reported by Rana Asim Wajid on 2019-06-05
+                   * ("Error Exception while invoking plugin service
+                   * 32: Bad file format"):
+                   * https://groups.google.com/d/msg/orthanc-users/T32FovWPcCE/-hKFbfRJBgAJ
+                   **/
+
+                  std::string t = Toolbox::StripSpaces(tokens[i]);
+                  if (t.empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    int64_t tmp = boost::lexical_cast(t);
+                    node[KEY_VALUE].append(FormatInteger(tmp));
+                  }
+                 
+                  break;
+                }
+              
+                case ValueRepresentation_DecimalString:
+                {
+                  std::string t = Toolbox::StripSpaces(tokens[i]);
+                  boost::replace_all(t, ",", "."); // some invalid files uses "," instead of "."
+                  
+                  // remove invalid/useless trailing decimal separator
+                  if (t.size() > 0 && t[t.size()-1] == '.')
+                  {
+                    t.resize(t.size() -1);
+                  }
+
+                  if (t.empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    // https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
+                    // DS values can be represented as String or Number in Json.
+                    // For IS, DS, SV and UV, a JSON String representation can be used to preserve the original format during transformation of the representation, or if needed to avoid losing precision of a decimal string.
+                    // Since 1.12.5, always use the string repesentation.  Before, decimal numbers were represented as double which led to loss of precision (e.g: 0.143 represented as 0.1429999999)
+                    double tmp;
+                    if (SerializationToolbox::ParseDouble(tmp, t)) // make sure that the string contains a valid decimal number
+                    {
+                      node[KEY_VALUE].append(t);
+                    }
+                    else
+                    {
+                      throw boost::bad_lexical_cast();
+                    }
+                  }
+
+                  break;
+                }
+
+                default:
+                  if (tokens[i].empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    node[KEY_VALUE].append(tokens[i]);
+                  }
+                  
+                  break;
+              }
+            }
+            catch (boost::bad_lexical_cast&)
+            {
+              std::string tmp;
+              if (value.size() < 64 &&
+                  Toolbox::IsAsciiString(value))
+              {
+                tmp = ": " + value;
+              }
+              
+              LOG(WARNING) << "Ignoring DICOM tag (" << tag.Format()
+                           << ") with invalid content for VR " << EnumerationToString(vr) << tmp;
+            }
+          }
+        }
+      }
+    }
+      
+    return Action_None;
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h
new file mode 100644
index 0000000..0b2d801
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h
@@ -0,0 +1,140 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_ENABLE_PUGIXML)
+#  error Macro ORTHANC_ENABLE_PUGIXML must be defined to use this file
+#endif
+
+#include "ITagVisitor.h"
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+
+#include 
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomWebJsonVisitor : public ITagVisitor
+  {
+  public:
+    enum BinaryMode
+    {
+      BinaryMode_Ignore,
+      BinaryMode_BulkDataUri,
+      BinaryMode_InlineBinary
+    };
+    
+    class IBinaryFormatter : public boost::noncopyable
+    {
+    public:
+      virtual ~IBinaryFormatter()
+      {
+      }
+
+      virtual BinaryMode Format(std::string& bulkDataUri,
+                                const std::vector& parentTags,
+                                const std::vector& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr) = 0;
+    };
+    
+  private:
+    Json::Value        result_;
+    IBinaryFormatter  *formatter_;
+
+    static std::string FormatTag(const DicomTag& tag);
+    
+    Json::Value& CreateNode(const std::vector& parentTags,
+                            const std::vector& parentIndexes,
+                            const DicomTag& tag);
+
+    static Json::Value FormatInteger(int64_t value);
+
+    static Json::Value FormatDouble(double value);
+
+    static Json::Value FormatDecimalString(double value, const std::string& originalString);
+
+  public:
+    DicomWebJsonVisitor();
+
+    void SetFormatter(IBinaryFormatter& formatter);
+    
+    void Clear();
+
+    const Json::Value& GetResult() const;
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+    void FormatXml(std::string& target) const;
+#endif
+
+    virtual Action VisitNotSupported(const std::vector& parentTags,
+                                     const std::vector& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitSequence(const std::vector& parentTags,
+                                 const std::vector& parentIndexes,
+                                 const DicomTag& tag,
+                                 size_t countItems)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitBinary(const std::vector& parentTags,
+                               const std::vector& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const void* data,
+                               size_t size)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitIntegers(const std::vector& parentTags,
+                                 const std::vector& parentIndexes,
+                                 const DicomTag& tag,
+                                 ValueRepresentation vr,
+                                 const std::vector& values)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitDoubles(const std::vector& parentTags,
+                                const std::vector& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const std::vector& values)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitAttributes(const std::vector& parentTags,
+                                   const std::vector& parentIndexes,
+                                   const DicomTag& tag,
+                                   const std::vector& values)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitString(std::string& newValue,
+                               const std::vector& parentTags,
+                               const std::vector& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::string& value)
+      ORTHANC_OVERRIDE;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
new file mode 100644
index 0000000..7a84388
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
@@ -0,0 +1,3596 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "FromDcmtkBridge.h"
+#include "ToDcmtkBridge.h"
+#include "../ChunkedBuffer.h"
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../Toolbox.h"
+#include "../OrthancException.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../TemporaryFile.h"
+#endif
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#if DCMTK_VERSION_NUMBER >= 361
+#  include 
+#  include 
+#endif
+
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+#  if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1)
+#    include 
+#  endif
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+#  include 
+#  if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#    include 
+#  endif
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+#  include 
+#  if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#    include 
+#  endif
+#endif
+
+
+#include 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#  include 
+#  include   // include to support color images
+#endif
+
+
+static bool hasExternalDictionaries_ = false;
+
+
+namespace Orthanc
+{
+  FromDcmtkBridge::DictionaryWriterLock::DictionaryWriterLock() :
+    dictionary_(dcmDataDict.wrlock())
+  {
+  }
+
+
+  FromDcmtkBridge::DictionaryWriterLock::~DictionaryWriterLock()
+  {
+#if DCMTK_VERSION_NUMBER >= 364
+    dcmDataDict.wrunlock();
+#else
+    dcmDataDict.unlock();
+#endif
+  }
+
+
+  FromDcmtkBridge::DictionaryReaderLock::DictionaryReaderLock() :
+    dictionary_(dcmDataDict.rdlock())
+  {
+  }
+
+
+  FromDcmtkBridge::DictionaryReaderLock::~DictionaryReaderLock()
+  {
+#if DCMTK_VERSION_NUMBER >= 364
+    dcmDataDict.rdunlock();
+#else
+    dcmDataDict.unlock();
+#endif
+  }
+
+
+  static bool IsBinaryTag(const DcmTag& key)
+  {
+    return (key.isUnknownVR() ||
+            key.getEVR() == EVR_OB ||
+            key.getEVR() == EVR_OW ||
+            key.getEVR() == EVR_UN ||
+            key.getEVR() == EVR_ox);
+  }
+
+
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+  static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
+                                     FrameworkResources::FileResourceId resource)
+  {
+    std::string content;
+    FrameworkResources::GetFileResource(content, resource);
+
+#if ORTHANC_SANDBOXED == 0
+    TemporaryFile tmp;
+    tmp.Write(content);
+
+    if (!dictionary.loadDictionary(tmp.GetPath().c_str()))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot read embedded dictionary. Under Windows, make sure that " 
+                             "your TEMP directory does not contain special characters.");
+    }
+#else
+    if (!dictionary.loadFromMemory(content))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot read embedded dictionary. Under Windows, make sure that " 
+                             "your TEMP directory does not contain special characters.");
+    }
+#endif
+  }
+#endif
+
+
+  namespace
+  {
+    class ChunkedBufferStream : public DcmOutputStream
+    {
+    private:
+      class Consumer : public DcmConsumer
+      {
+      private:
+        ChunkedBuffer  buffer_;
+
+      public:
+        void Flatten(std::string& buffer)
+        {
+          buffer_.Flatten(buffer);
+        }
+
+        OFBool good() const ORTHANC_OVERRIDE
+        {
+          return true;
+        }
+
+        OFCondition status() const ORTHANC_OVERRIDE
+        {
+          return EC_Normal;
+        }
+
+        OFBool isFlushed() const ORTHANC_OVERRIDE
+        {
+          return true;
+        }
+
+        offile_off_t avail() const ORTHANC_OVERRIDE
+        {
+          // since we cannot report "unlimited", let's claim that we can still write 10MB.
+          // Note that offile_off_t is a signed type.
+          return 10 * 1024 * 1024;
+        }
+
+        offile_off_t write(const void *buf,
+                           offile_off_t buflen) ORTHANC_OVERRIDE
+        {
+          buffer_.AddChunk(buf, buflen);
+          return buflen;
+        }
+
+        void flush() ORTHANC_OVERRIDE
+        {
+          // Nothing to flush
+        }
+      };
+
+      Consumer consumer_;
+
+    public:
+      ChunkedBufferStream() :
+        DcmOutputStream(&consumer_)
+      {
+      }
+
+      void Flatten(std::string& buffer)
+      {
+        consumer_.Flatten(buffer);
+      }
+    };
+  }
+
+
+  namespace
+  {
+    ORTHANC_FORCE_INLINE
+    static std::string FloatToString(float v)
+    {
+      /**
+       * From "boost::lexical_cast" documentation: "For more involved
+       * conversions, such as where precision or formatting need tighter
+       * control than is offered by the default behavior of
+       * lexical_cast, the conventional stringstream approach is
+       * recommended."
+       * https://www.boost.org/doc/libs/1_65_0/doc/html/boost_lexical_cast.html
+       * http://www.gotw.ca/publications/mill19.htm
+       *
+       * The precision of 17 corresponds to "defaultRealPrecision" in JsonCpp:
+       * https://github.com/open-source-parsers/jsoncpp/blob/master/include/json/value.h
+       **/
+
+      //return boost::lexical_cast(v);  // This was used in Orthanc <= 1.9.0
+      
+      std::ostringstream ss;
+      ss << std::setprecision(17) << v;
+      return ss.str();
+    }
+
+
+    ORTHANC_FORCE_INLINE
+    static std::string DoubleToString(double v)
+    {
+      //return boost::lexical_cast(v);  // This was used in Orthanc <= 1.9.0
+      
+      std::ostringstream ss;
+      ss << std::setprecision(17) << v;
+      return ss.str();
+    }
+
+    
+#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter, toStringFunction) \
+                                                                        \
+    struct converter                                                    \
+    {                                                                   \
+      typedef cType CType;                                              \
+                                                                        \
+      ORTHANC_FORCE_INLINE                                              \
+        static bool Apply(CType& result,                                \
+                          DcmElement& element,                          \
+                          size_t i)                                     \
+      {                                                                 \
+        return dynamic_cast(element).getter(result, i).good(); \
+      }                                                                 \
+                                                                        \
+      ORTHANC_FORCE_INLINE                                              \
+        static std::string ToString(CType value)                        \
+      {                                                                 \
+        return toStringFunction(value);                                 \
+      }                                                                 \
+    };
+
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint32Converter, Sint32, DcmSignedLong, getSint32, boost::lexical_cast)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToSint16Converter, Sint16, DcmSignedShort, getSint16, boost::lexical_cast)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint32Converter, Uint32, DcmUnsignedLong, getUint32, boost::lexical_cast)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToUint16Converter, Uint16, DcmUnsignedShort, getUint16, boost::lexical_cast)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat32Converter, Float32, DcmFloatingPointSingle, getFloat32, FloatToString)
+    DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDouble, getFloat64, DoubleToString)
+
+
+    template 
+    static DicomValue* ApplyDcmtkToCTypeConverter(DcmElement& element)
+    {
+      F f;
+      typename F::CType value;
+
+      if (element.getLength() > sizeof(typename F::CType)
+          && (element.getLength() % sizeof(typename F::CType)) == 0)
+      {
+        size_t count = element.getLength() / sizeof(typename F::CType);
+        std::vector strings;
+        for (size_t i = 0; i < count; i++) {
+          if (f.Apply(value, element, i)) {
+            strings.push_back(F::ToString(value));
+          }
+        }
+        return new DicomValue(boost::algorithm::join(strings, "\\"), false);
+      }
+      else if (f.Apply(value, element, 0)) {
+        return new DicomValue(F::ToString(value), false);
+      }
+      else {
+        return new DicomValue;
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::InitializeDictionary(bool loadPrivateDictionary)
+  {
+    CLOG(INFO, DICOM) << "Using DCMTK version: " << DCMTK_VERSION_NUMBER;
+    
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+    {
+      DictionaryWriterLock lock;
+
+      lock.GetDictionary().clear();
+
+      CLOG(INFO, DICOM) << "Loading the embedded dictionaries";
+      /**
+       * Do not load DICONDE dictionary, it breaks the other tags. The
+       * command "strace storescu 2>&1 |grep dic" shows that DICONDE
+       * dictionary is not loaded by storescu.
+       **/
+      //LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICONDE);
+
+      LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICOM);
+
+      if (loadPrivateDictionary)
+      {
+        CLOG(INFO, DICOM) << "Loading the embedded dictionary of private tags";
+        LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_PRIVATE);
+      }
+      else
+      {
+        CLOG(INFO, DICOM) << "The dictionary of private tags has not been loaded";
+      }
+    }
+#else
+    {
+      std::vector dictionaries;
+      
+      const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
+      if (env != NULL)
+      {
+        // This mimics the behavior of DCMTK:
+        // https://support.dcmtk.org/docs/file_envvars.html
+#if defined(_WIN32)
+        Toolbox::TokenizeString(dictionaries, std::string(env), ';');
+#else
+        Toolbox::TokenizeString(dictionaries, std::string(env), ':');
+#endif
+      }
+      else
+      {
+        boost::filesystem::path base = DCMTK_DICTIONARY_DIR;
+        dictionaries.push_back((base / "dicom.dic").string());
+
+        if (loadPrivateDictionary)
+        {
+          dictionaries.push_back((base / "private.dic").string());
+        }
+      }
+
+      LoadExternalDictionaries(dictionaries);
+      hasExternalDictionaries_ = false;  // Fix the side-effect of "LoadExternalDictionaries()"
+    }
+#endif
+
+    /* make sure data dictionary is loaded */
+    if (!dcmDataDict.isDictionaryLoaded())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "No DICOM dictionary loaded, check environment variable: " +
+                             std::string(DCM_DICT_ENVIRONMENT_VARIABLE));
+    }
+
+    {
+      // Test the dictionary with a simple DICOM tag
+      DcmTag key(0x0010, 0x1030); // This is PatientWeight
+      if (key.getEVR() != EVR_DS)
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "The DICOM dictionary has not been correctly read");
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::LoadExternalDictionaries(const std::vector& dictionaries)
+  {
+    DictionaryWriterLock lock;
+
+    CLOG(INFO, DICOM) << "Clearing the DICOM dictionary";
+    lock.GetDictionary().clear();
+
+    for (size_t i = 0; i < dictionaries.size(); i++)
+    {
+      LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\"";
+        
+      if (!lock.GetDictionary().loadDictionary(dictionaries[i].c_str()))
+      {
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+    }    
+
+    hasExternalDictionaries_ = true;
+  }
+
+
+  void FromDcmtkBridge::RegisterDictionaryTag(const DicomTag& tag,
+                                              ValueRepresentation vr,
+                                              const std::string& name,
+                                              unsigned int minMultiplicity,
+                                              unsigned int maxMultiplicity,
+                                              const std::string& privateCreator)
+  {
+    if (minMultiplicity < 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    bool arbitrary = false;
+    if (maxMultiplicity == 0)
+    {
+      maxMultiplicity = DcmVariableVM;
+      arbitrary = true;
+    }
+    else if (maxMultiplicity < minMultiplicity)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    DcmEVR evr = ToDcmtkBridge::Convert(vr);
+
+    CLOG(INFO, DICOM) << "Registering tag in dictionary: (" << tag.Format() << ") "
+                      << (DcmVR(evr).getValidVRName()) << " " 
+                      << name << " (multiplicity: " << minMultiplicity << "-" 
+                      << (arbitrary ? "n" : boost::lexical_cast(maxMultiplicity)) << ")";
+
+    std::unique_ptr  entry;
+    if (privateCreator.empty())
+    {
+      if (tag.GetGroup() % 2 == 1)
+      {
+        char buf[128];
+        sprintf(buf, "Warning: You are registering a private tag (%04x,%04x), "
+                "but no private creator was associated with it", 
+                tag.GetGroup(), tag.GetElement());
+        LOG(WARNING) << buf;
+      }
+
+      entry.reset(new DcmDictEntry(tag.GetGroup(),
+                                   tag.GetElement(),
+                                   evr, name.c_str(),
+                                   static_cast(minMultiplicity),
+                                   static_cast(maxMultiplicity),
+                                   NULL    /* version */,
+                                   OFTrue  /* doCopyString */,
+                                   NULL    /* private creator */));
+    }
+    else
+    {
+      // "Private Data Elements have an odd Group Number that is not
+      // (0001,eeee), (0003,eeee), (0005,eeee), (0007,eeee), or
+      // (FFFF,eeee)."
+      if (tag.GetGroup() % 2 == 0 /* even */ ||
+          tag.GetGroup() == 0x0001 ||
+          tag.GetGroup() == 0x0003 ||
+          tag.GetGroup() == 0x0005 ||
+          tag.GetGroup() == 0x0007 ||
+          tag.GetGroup() == 0xffff)
+      {
+        char buf[128];
+        sprintf(buf, "Trying to register private tag (%04x,%04x), but it must have an odd group >= 0x0009",
+                tag.GetGroup(), tag.GetElement());
+        throw OrthancException(ErrorCode_ParameterOutOfRange, std::string(buf));
+      }
+
+      entry.reset(new DcmDictEntry(tag.GetGroup(),
+                                   tag.GetElement(),
+                                   evr, name.c_str(),
+                                   static_cast(minMultiplicity),
+                                   static_cast(maxMultiplicity),
+                                   "private" /* version */,
+                                   OFTrue    /* doCopyString */,
+                                   privateCreator.c_str()));
+    }
+
+    entry->setGroupRangeRestriction(DcmDictRange_Unspecified);
+    entry->setElementRangeRestriction(DcmDictRange_Unspecified);
+
+    {
+      DictionaryWriterLock lock;
+
+      if (lock.GetDictionary().findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()),
+                                         privateCreator.empty() ? NULL : privateCreator.c_str()))
+      {
+        throw OrthancException(ErrorCode_AlreadyExistingTag,
+                               "Cannot register twice the tag (" + tag.Format() +
+                               "), whose symbolic name is \"" + name + "\"");
+      }
+      else
+      {
+        lock.GetDictionary().addEntry(entry.release());
+      }
+    }
+  }
+
+
+  Encoding FromDcmtkBridge::DetectEncoding(bool& hasCodeExtensions,
+                                           DcmItem& dataset,
+                                           Encoding defaultEncoding)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
+
+    OFString tmp;
+    if (dataset.findAndGetOFStringArray(DCM_SpecificCharacterSet, tmp).good())
+    {
+      std::vector tokens;
+      Toolbox::TokenizeString(tokens, std::string(tmp.c_str()), '\\');
+
+      hasCodeExtensions = (tokens.size() > 1);
+
+      for (size_t i = 0; i < tokens.size(); i++)
+      {
+        std::string characterSet = Toolbox::StripSpaces(tokens[i]);
+
+        if (!characterSet.empty())
+        {
+          Encoding encoding;
+          
+          if (GetDicomEncoding(encoding, characterSet.c_str()))
+          {
+            // The specific character set is supported by the Orthanc core
+            return encoding;
+          }
+          else
+          {
+            LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
+                         << ", fallback to ASCII (remove all special characters)";
+            return Encoding_Ascii;
+          }
+        }
+      }
+    }
+    else
+    {
+      hasCodeExtensions = false;
+    }
+    
+    // No specific character set tag: Use the default encoding
+    return defaultEncoding;
+  }
+
+
+  Encoding FromDcmtkBridge::DetectEncoding(DcmItem &dataset,
+                                           Encoding defaultEncoding)
+  {
+    bool hasCodeExtensions;  // ignored
+    return DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+  }
+
+
+  void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target,
+                                            DcmItem& dataset,
+                                            unsigned int maxStringLength,
+                                            const std::set& ignoreTagLength)
+  {
+    const Encoding defaultEncoding = GetDefaultDicomEncoding();
+    
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+
+    target.Clear();
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element && element->isLeaf())
+      {
+        target.SetValueInternal(element->getTag().getGTag(),
+                                element->getTag().getETag(),
+                                ConvertLeafElement(*element, DicomToJsonFlags_Default,
+                                                   maxStringLength, encoding, hasCodeExtensions, ignoreTagLength));
+      }
+      else
+      {
+        DcmSequenceOfItems* sequence = dynamic_cast(element);
+        
+        if (sequence)
+        {
+          Json::Value jsonSequence = Json::arrayValue;
+          for (unsigned long s = 0; s < sequence->card(); s++)
+          {
+            DcmItem* child = sequence->getItem(s);
+            Json::Value& v = jsonSequence.append(Json::objectValue);
+            DatasetToJson(v, *child, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 
+                          maxStringLength, encoding, hasCodeExtensions,
+                          ignoreTagLength, 1);
+          }
+
+          target.SetSequenceValue(DicomTag(element->getTag().getGTag(), element->getTag().getETag()),
+                                  jsonSequence);
+        }
+      }
+    }
+  }
+
+
+  DicomTag FromDcmtkBridge::Convert(const DcmTag& tag)
+  {
+    return DicomTag(tag.getGTag(), tag.getETag());
+  }
+
+
+  DicomTag FromDcmtkBridge::GetTag(const DcmElement& element)
+  {
+    return DicomTag(element.getGTag(), element.getETag());
+  }
+
+
+  static DicomValue* CreateValueFromUtf8String(const DicomTag& tag,
+                                               const std::string& utf8,
+                                               unsigned int maxStringLength,
+                                               const std::set& ignoreTagLength)
+  {
+    if (maxStringLength != 0 &&
+        utf8.size() > maxStringLength &&
+        ignoreTagLength.find(tag) == ignoreTagLength.end())
+    {
+      return new DicomValue;  // Too long, create a NULL value
+    }
+    else
+    {
+      return new DicomValue(utf8, false);
+    }
+  }
+
+
+  DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element,
+                                                  DicomToJsonFlags flags,
+                                                  unsigned int maxStringLength,
+                                                  Encoding encoding,
+                                                  bool hasCodeExtensions,
+                                                  const std::set& ignoreTagLength)
+  {
+    if (!element.isLeaf())
+    {
+      // This function is only applicable to leaf elements
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    {
+      char *c = NULL;
+      if (element.isaString() &&
+          element.getString(c).good())
+      {
+        if (c == NULL)  // This case corresponds to the empty string
+        {
+          return new DicomValue("", false);
+        }
+        else
+        {
+          const std::string s(c);
+          const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
+          return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength);
+        }
+      }
+    }
+
+
+    if (element.getVR() == EVR_UN)
+    {
+      /**
+       * Unknown value representation: Lookup in the dictionary. This
+       * is notably the case for private tags registered with the
+       * "Dictionary" configuration option, or for public tags with
+       * "EVR_UN" in the case of Little Endian Implicit transfer
+       * syntax (cf. DICOM CP 246).
+       * ftp://medical.nema.org/medical/dicom/final/cp246_ft.pdf
+       **/
+      DictionaryReaderLock lock;
+
+      // The "entry" value is only valid while "lock" is active
+      const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(),
+                                                                 element.getTag().getPrivateCreator());
+      if (entry != NULL && 
+          entry->getVR().isaString())
+      {
+        Uint8* data = NULL;
+
+        if (element.getUint8Array(data) == EC_Normal)
+        {
+          Uint32 length = element.getLength();
+
+          if (data == NULL ||
+              length == 0)
+          {
+            return new DicomValue("", false);   // Empty string
+          }
+
+          // Remove the trailing padding, if any
+          if (length > 0 &&
+              length % 2 == 0 &&
+              data[length - 1] == '\0')
+          {
+            length = length - 1;
+          }
+
+          if (element.getTag().isPrivate())
+          {
+            // For private tags, we do not try and convert to UTF-8,
+            // as nothing ensures that the encoding of the private tag
+            // is the same as that of the remaining of the DICOM
+            // dataset. Only go for ASCII strings.
+            if (Toolbox::IsAsciiString(data, length))
+            {
+              const std::string s(reinterpret_cast(data), length);
+              return CreateValueFromUtf8String(GetTag(element), s, maxStringLength, ignoreTagLength);
+            }
+            else
+            {
+              // Not a plain ASCII string: Consider it as a binary
+              // value that is handled in the switch-case below
+            }
+          }
+          else
+          {
+            // For public tags, convert to UTF-8 by using the
+            // "SpecificCharacterSet" tag, if present. This branch is
+            // new in Orthanc 1.9.1 (cf. DICOM CP 246).
+            const std::string s(reinterpret_cast(data), length);
+            const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
+            return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength);
+          }
+        }
+      }
+    }
+
+    
+    try
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+      switch (element.getVR())
+      {
+
+        /**
+         * Deal with binary data (including PixelData).
+         **/
+
+        case EVR_OB:  // other byte
+        case EVR_OF:  // other float
+        case EVR_OW:  // other word
+        case EVR_UN:  // unknown value representation
+        case EVR_ox:  // OB or OW depending on context
+        case EVR_DS:  // decimal string
+        case EVR_IS:  // integer string
+        case EVR_AS:  // age string
+        case EVR_DA:  // date string
+        case EVR_DT:  // date time string
+        case EVR_TM:  // time string
+        case EVR_AE:  // application entity title
+        case EVR_CS:  // code string
+        case EVR_SH:  // short string
+        case EVR_LO:  // long string
+        case EVR_ST:  // short text
+        case EVR_LT:  // long text
+        case EVR_UT:  // unlimited text
+        case EVR_PN:  // person name
+        case EVR_UI:  // unique identifier
+        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+        {
+          if (!(flags & DicomToJsonFlags_ConvertBinaryToNull))
+          {
+            Uint8* data = NULL;
+            Uint16* data16 = NULL;
+            if (element.getUint8Array(data) == EC_Normal)
+            {
+              return new DicomValue(reinterpret_cast(data), element.getLength(), true);
+            }
+            else if (element.getUint16Array(data16) == EC_Normal)
+            {
+              return new DicomValue(reinterpret_cast(data16), element.getLength(), true);
+            }
+          }
+
+          return new DicomValue;
+        }
+    
+        /**
+         * Numeric types
+         **/ 
+      
+        case EVR_SL:  // signed long
+        {
+          return ApplyDcmtkToCTypeConverter(element);
+        }
+
+        case EVR_SS:  // signed short
+        {
+          return ApplyDcmtkToCTypeConverter(element);
+        }
+
+        case EVR_UL:  // unsigned long
+        {
+          return ApplyDcmtkToCTypeConverter(element);
+        }
+
+        case EVR_US:  // unsigned short
+        {
+          return ApplyDcmtkToCTypeConverter(element);
+        }
+
+        case EVR_FL:  // float single-precision
+        {
+          return ApplyDcmtkToCTypeConverter(element);
+        }
+
+        case EVR_FD:  // float double-precision
+        {
+          return ApplyDcmtkToCTypeConverter(element);
+        }
+
+
+        /**
+         * Attribute tag.
+         **/
+
+        case EVR_AT:
+        {
+          DcmTagKey tag;
+          if (dynamic_cast(element).getTagVal(tag, 0).good())
+          {
+            DicomTag t(tag.getGroup(), tag.getElement());
+            return new DicomValue(t.Format(), false);
+          }
+          else
+          {
+            return new DicomValue;
+          }
+        }
+
+
+        /**
+         * Sequence types, should never occur at this point because of
+         * "element.isLeaf()".
+         **/
+
+        case EVR_SQ:  // sequence of items
+          return new DicomValue;
+
+
+          /**
+           * Internal to DCMTK.
+           **/ 
+
+        case EVR_xs:  // SS or US depending on context
+        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+        case EVR_na:  // na="not applicable", for data which has no VR
+        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+        case EVR_item:  // used internally for items
+        case EVR_metainfo:  // used internally for meta info datasets
+        case EVR_dataset:  // used internally for datasets
+        case EVR_fileFormat:  // used internally for DICOM files
+        case EVR_dicomDir:  // used internally for DICOMDIR objects
+        case EVR_dirRecord:  // used internally for DICOMDIR records
+        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+        case EVR_pixelItem:  // used internally for pixel items in a compressed image
+        case EVR_PixelData:  // used internally for uncompressed pixeld data
+        case EVR_OverlayData:  // used internally for overlay data
+          return new DicomValue;
+
+
+          /**
+           * Default case.
+           **/ 
+
+        default:
+          return new DicomValue;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return new DicomValue;
+    }
+    catch (std::bad_cast&)
+    {
+      return new DicomValue;
+    }
+  }
+
+
+  static Json::Value& PrepareNode(Json::Value& parent,
+                                  DcmElement& element,
+                                  DicomToJsonFormat format)
+  {
+    assert(parent.type() == Json::objectValue);
+
+    DicomTag tag(FromDcmtkBridge::GetTag(element));
+    const std::string formattedTag = tag.Format();
+
+    if (format == DicomToJsonFormat_Short)
+    {
+      parent[formattedTag] = Json::nullValue;
+      return parent[formattedTag];
+    }
+
+    // This code gives access to the name of the private tags
+    std::string tagName = FromDcmtkBridge::GetTagName(element);
+    
+    switch (format)
+    {
+      case DicomToJsonFormat_Human:
+        parent[tagName] = Json::nullValue;
+        return parent[tagName];
+
+      case DicomToJsonFormat_Full:
+      {
+        parent[formattedTag] = Json::objectValue;
+        Json::Value& node = parent[formattedTag];
+
+        if (element.isLeaf())
+        {
+          node["Name"] = tagName;
+
+          if (element.getTag().getPrivateCreator() != NULL)
+          {
+            node["PrivateCreator"] = element.getTag().getPrivateCreator();
+          }
+
+          return node;
+        }
+        else
+        {
+          node["Name"] = tagName;
+          node["Type"] = "Sequence";
+          node["Value"] = Json::nullValue;
+          return node["Value"];
+        }
+      }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void LeafValueToJson(Json::Value& target,
+                              const DicomValue& value,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength)
+  {
+    Json::Value* targetValue = NULL;
+    Json::Value* targetType = NULL;
+
+    switch (format)
+    {
+      case DicomToJsonFormat_Short:
+      case DicomToJsonFormat_Human:
+      {
+        assert(target.type() == Json::nullValue);
+        targetValue = ⌖
+        break;
+      }      
+
+      case DicomToJsonFormat_Full:
+      {
+        assert(target.type() == Json::objectValue);
+        target["Value"] = Json::nullValue;
+        target["Type"] = Json::nullValue;
+        targetType = &target["Type"];
+        targetValue = &target["Value"];
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(targetValue != NULL);
+    assert(targetValue->type() == Json::nullValue);
+    assert(targetType == NULL || targetType->type() == Json::nullValue);
+
+    if (value.IsNull())
+    {
+      if (targetType != NULL)
+      {
+        *targetType = "Null";
+      }
+    }
+    else if (value.IsBinary())
+    {
+      if (flags & DicomToJsonFlags_ConvertBinaryToAscii)
+      {
+        *targetValue = Toolbox::ConvertToAscii(value.GetContent());
+      }
+      else
+      {
+        std::string s;
+        value.FormatDataUriScheme(s);
+        *targetValue = s;
+      }
+
+      if (targetType != NULL)
+      {
+        *targetType = "Binary";
+      }
+    }
+    else if (maxStringLength == 0 ||
+             value.GetContent().size() <= maxStringLength)
+    {
+      *targetValue = value.GetContent();
+
+      if (targetType != NULL)
+      {
+        *targetType = "String";
+      }
+    }
+    else
+    {
+      if (targetType != NULL)
+      {
+        *targetType = "TooLong";
+      }
+    }
+  }                              
+
+
+  void FromDcmtkBridge::ElementToJson(Json::Value& parent,
+                                      DcmElement& element,
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength,
+                                      Encoding encoding,
+                                      bool hasCodeExtensions,
+                                      const std::set& ignoreTagLength,
+                                      unsigned int depth)
+  {
+    if (parent.type() == Json::nullValue)
+    {
+      parent = Json::objectValue;
+    }
+
+    assert(parent.type() == Json::objectValue);
+    Json::Value& target = PrepareNode(parent, element, format);
+
+    if (element.isLeaf())
+    {
+      // The "0" below lets "LeafValueToJson()" take care of "TooLong" values
+      std::unique_ptr v(FromDcmtkBridge::ConvertLeafElement
+                                    (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength));
+
+      if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end())
+      {
+        LeafValueToJson(target, *v, format, flags, maxStringLength);
+      }
+      else
+      {
+        LeafValueToJson(target, *v, format, flags, 0);
+      }
+    }
+    else
+    {
+      assert(target.type() == Json::nullValue);
+      target = Json::arrayValue;
+
+      // "All subclasses of DcmElement except for DcmSequenceOfItems
+      // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
+      // etc. are not." The following dynamic_cast is thus OK.
+      DcmSequenceOfItems& sequence = dynamic_cast(element);
+
+      for (unsigned long i = 0; i < sequence.card(); i++)
+      {
+        DcmItem* child = sequence.getItem(i);
+        Json::Value& v = target.append(Json::objectValue);
+        DatasetToJson(v, *child, format, flags, maxStringLength, encoding, hasCodeExtensions,
+                      ignoreTagLength, depth + 1);
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::DatasetToJson(Json::Value& parent,
+                                      DcmItem& item,
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength,
+                                      Encoding encoding,
+                                      bool hasCodeExtensions,
+                                      const std::set& ignoreTagLength,
+                                      unsigned int depth)
+  {
+    assert(parent.type() == Json::objectValue);
+
+    for (unsigned long i = 0; i < item.card(); i++)
+    {
+      DcmElement* element = item.getElement(i);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
+
+      // New flag in Orthanc 1.9.1
+      if (depth == 0 &&
+          (flags & DicomToJsonFlags_StopAfterPixelData) &&
+          tag > DICOM_TAG_PIXEL_DATA)
+      {
+        continue;
+      }
+
+      // New flag in Orthanc 1.9.1
+      if ((flags & DicomToJsonFlags_SkipGroupLengths) &&
+          tag.GetElement() == 0x0000)
+      {
+        continue;
+      }
+
+      /*element->getTag().isPrivate()*/
+      if (tag.IsPrivate() &&
+          !(flags & DicomToJsonFlags_IncludePrivateTags))    
+      {
+        continue;
+      }
+
+      if (!(flags & DicomToJsonFlags_IncludeUnknownTags))
+      {
+        DictionaryReaderLock lock;
+        if (lock.GetDictionary().findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL)
+        {
+          continue;
+        }
+      }
+
+      if (IsBinaryTag(element->getTag()))
+      {
+        // This is a binary tag
+        if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) ||
+            (tag != DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludeBinary)))
+        {
+          continue;
+        }
+      }
+
+      FromDcmtkBridge::ElementToJson(parent, *element, format, flags, maxStringLength, encoding,
+                                     hasCodeExtensions, ignoreTagLength, depth);
+    }
+  }
+
+
+  void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target, 
+                                           DcmDataset& dataset,
+                                           DicomToJsonFormat format,
+                                           DicomToJsonFlags flags,
+                                           unsigned int maxStringLength,
+                                           const std::set& ignoreTagLength)
+  {
+    const Encoding defaultEncoding = GetDefaultDicomEncoding();
+    
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+
+    target = Json::objectValue;
+    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength, 0);
+  }
+
+
+  void FromDcmtkBridge::ExtractHeaderAsJson(Json::Value& target, 
+                                            DcmMetaInfo& dataset,
+                                            DicomToJsonFormat format,
+                                            DicomToJsonFlags flags,
+                                            unsigned int maxStringLength)
+  {
+    std::set ignoreTagLength;
+    target = Json::objectValue;
+    DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, false, ignoreTagLength, 0);
+  }
+
+
+  static std::string GetTagNameInternal(DcmTag& tag)
+  {
+    if (!hasExternalDictionaries_)
+    {
+      /**
+       * Some patches for important tags because of different DICOM
+       * dictionaries between DCMTK versions. Since Orthanc 1.9.4, we
+       * don't apply these patches if external dictionaries are
+       * loaded, notably for compatibility with DICONDE. In Orthanc <=
+       * 1.9.3, this was done by method "DicomTag::GetMainTagsName()".
+       **/
+      
+      DicomTag tmp(tag.getGroup(), tag.getElement());
+
+      if (tmp == DICOM_TAG_ACCESSION_NUMBER)
+        return "AccessionNumber";
+
+      if (tmp == DICOM_TAG_SOP_INSTANCE_UID)
+        return "SOPInstanceUID";
+
+      if (tmp == DICOM_TAG_PATIENT_ID)
+        return "PatientID";
+
+      if (tmp == DICOM_TAG_SERIES_INSTANCE_UID)
+        return "SeriesInstanceUID";
+
+      if (tmp == DICOM_TAG_STUDY_INSTANCE_UID)
+        return "StudyInstanceUID"; 
+
+      if (tmp == DICOM_TAG_PIXEL_DATA)
+        return "PixelData";
+
+      if (tmp == DICOM_TAG_IMAGE_INDEX)
+        return "ImageIndex";
+
+      if (tmp == DICOM_TAG_INSTANCE_NUMBER)
+        return "InstanceNumber";
+
+      if (tmp == DICOM_TAG_NUMBER_OF_SLICES)
+        return "NumberOfSlices";
+
+      if (tmp == DICOM_TAG_NUMBER_OF_FRAMES)
+        return "NumberOfFrames";
+
+      if (tmp == DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)
+        return "CardiacNumberOfImages";
+
+      if (tmp == DICOM_TAG_IMAGES_IN_ACQUISITION)
+        return "ImagesInAcquisition";
+
+      if (tmp == DICOM_TAG_PATIENT_NAME)
+        return "PatientName";
+
+      if (tmp == DICOM_TAG_IMAGE_POSITION_PATIENT)
+        return "ImagePositionPatient";
+
+      if (tmp == DICOM_TAG_IMAGE_ORIENTATION_PATIENT)
+        return "ImageOrientationPatient";
+
+      // New in Orthanc 1.6.0, as tagged as "RETIRED_" since DCMTK 3.6.4
+      if (tmp == DICOM_TAG_OTHER_PATIENT_IDS)
+        return "OtherPatientIDs";
+
+      // End of patches
+    }
+
+#if 0
+    // This version explicitly calls the dictionary
+    const DcmDataDictionary& dict = dcmDataDict.rdlock();
+    const DcmDictEntry* entry = dict.findEntry(tag, NULL);
+
+    std::string s(DcmTag_ERROR_TagName);
+    if (entry != NULL)
+    {
+      s = std::string(entry->getTagName());
+    }
+
+    dcmDataDict.unlock();
+    return s;
+#else
+    const char* name = tag.getTagName();
+    if (name == NULL)
+    {
+      return DcmTag_ERROR_TagName;
+    }
+    else
+    {
+      return std::string(name);
+    }
+#endif
+  }
+
+
+  static bool GetTagFromNameInternal(DicomTag& tag, const std::string& tagName)
+  {
+    // conversion from old tag names (ex: RETIRED_OtherPatientIDs is the new name for OtherPatientIDs that is still a valid name for DICOM_TAG_OTHER_PATIENT_IDS)
+    if (tagName == "OtherPatientIDs")
+    {
+      tag = DICOM_TAG_OTHER_PATIENT_IDS;
+      return true;
+    }
+
+    return false;
+  }
+
+  std::string FromDcmtkBridge::GetTagName(const DicomTag& t,
+                                          const std::string& privateCreator)
+  {
+    
+    DcmTag tag(t.GetGroup(), t.GetElement());
+
+    if (!privateCreator.empty())
+    {
+      tag.setPrivateCreator(privateCreator.c_str());
+    }
+
+    return GetTagNameInternal(tag);
+  }
+
+
+  std::string FromDcmtkBridge::GetTagName(const DcmElement& element)
+  {
+    // Copy the tag to ensure const-correctness of DcmElement. Note
+    // that the private creator information is also copied.
+    DcmTag tag(element.getTag());  
+
+    return GetTagNameInternal(tag);
+  }
+
+  std::string FromDcmtkBridge::GetTagName(const DicomElement &element)
+  {
+    return GetTagName(element.GetTag(), "");
+  }
+
+
+
+  DicomTag FromDcmtkBridge::ParseTag(const char* name)
+  {
+    DicomTag parsed(0, 0);
+    if (DicomTag::ParseHexadecimal(parsed, name))
+    {
+      return parsed;
+    }
+
+#if 0
+    const DcmDataDictionary& dict = dcmDataDict.rdlock();
+    const DcmDictEntry* entry = dict.findEntry(name);
+
+    if (entry == NULL)
+    {
+      dcmDataDict.unlock();
+      throw OrthancException(ErrorCode_UnknownDicomTag);
+    }
+    else
+    {
+      DcmTagKey key = entry->getKey();
+      DicomTag tag(key.getGroup(), key.getElement());
+      dcmDataDict.unlock();
+      return tag;
+    }
+#else
+    DcmTag tag;
+    if (DcmTag::findTagFromName(name, tag).good())
+    {
+      return DicomTag(tag.getGTag(), tag.getETag());
+    }
+    else
+    {
+      DicomTag dcmTag(0, 0);
+      if (GetTagFromNameInternal(dcmTag, name))
+      {
+        return dcmTag;
+      }
+
+      CLOG(INFO, DICOM) << "Unknown DICOM tag: \"" << name << "\"";
+      throw OrthancException(ErrorCode_UnknownDicomTag, name, false);
+    }
+#endif
+  }
+
+  DicomTag FromDcmtkBridge::ParseTag(const std::string &name)
+  {
+    return ParseTag(name.c_str());
+  }
+
+  bool FromDcmtkBridge::HasTag(const DicomMap &fields, const std::string &tagName)
+  {
+    return fields.HasTag(ParseTag(tagName));
+  }
+
+  void FromDcmtkBridge::FormatListOfTags(std::string& output, const std::set& tags)
+  {
+    std::set values;
+    for (std::set::const_iterator it = tags.begin();
+         it != tags.end(); ++it)
+    {
+      values.insert(it->Format());
+    }
+
+    Toolbox::JoinStrings(output, values, ";");
+  }
+
+  void FromDcmtkBridge::FormatListOfTags(Json::Value& output, const std::set& tags)
+  {
+    output = Json::arrayValue;
+    for (std::set::const_iterator it = tags.begin();
+         it != tags.end(); ++it)
+    {
+      output.append(it->Format());
+    }
+  }
+
+  // parses a list like "0010,0010;PatientBirthDate;0020,0020"
+  void FromDcmtkBridge::ParseListOfTags(std::set& result, const std::string& source)
+  {
+    result.clear();
+
+    std::vector tokens;
+    Toolbox::TokenizeString(tokens, source, ';');
+
+    for (std::vector::const_iterator it = tokens.begin();
+         it != tokens.end(); ++it)
+    {
+      if (it->size() > 0)
+      {
+        DicomTag tag = FromDcmtkBridge::ParseTag(*it);
+        result.insert(tag);
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::ParseListOfTags(std::set& result, const Json::Value& source)
+  {
+    result.clear();
+
+    if (!source.isArray())
+    {
+      throw OrthancException(ErrorCode_BadRequest, "List of tags is not an array");
+    }
+
+    for (Json::ArrayIndex i = 0; i < source.size(); i++)
+    {
+      const std::string& value = source[i].asString();
+      DicomTag tag = FromDcmtkBridge::ParseTag(value);
+      result.insert(tag);
+    }
+  }
+
+  const DicomValue &FromDcmtkBridge::GetValue(const DicomMap &fields,
+                                              const std::string &tagName)
+  {
+    return fields.GetValue(ParseTag(tagName));
+  }
+
+  void FromDcmtkBridge::SetValue(DicomMap &target,
+                                 const std::string &tagName,
+                                 DicomValue *value)
+  {
+    const DicomTag tag = ParseTag(tagName);
+    target.SetValueInternal(tag.GetGroup(), tag.GetElement(), value);
+  }
+
+
+  bool FromDcmtkBridge::IsUnknownTag(const DicomTag& tag)
+  {
+    DcmTag tmp(tag.GetGroup(), tag.GetElement());
+    return tmp.isUnknownVR();
+  }
+
+
+  void FromDcmtkBridge::ToJson(Json::Value& result,
+                               const DicomMap& values,
+                               DicomToJsonFormat format)
+  {
+    if (result.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    result.clear();
+
+    for (DicomMap::Content::const_iterator 
+           it = values.content_.begin(); it != values.content_.end(); ++it)
+    {
+      switch (format)
+      {
+        case DicomToJsonFormat_Human:
+        {
+          // TODO Inject PrivateCreator if some is available in the DicomMap?
+          const std::string tagName = GetTagName(it->first, "");
+
+          if (it->second->IsNull())
+          {
+            result[tagName] = Json::nullValue;
+          }
+          else if (it->second->IsSequence())
+          {
+            result[tagName] = Json::arrayValue;
+            const Json::Value& jsonSequence = it->second->GetSequenceContent();
+
+            for (Json::Value::ArrayIndex i = 0; i < jsonSequence.size(); ++i)
+            {
+              Json::Value target = Json::objectValue;
+              Toolbox::SimplifyDicomAsJson(target, jsonSequence[i], DicomToJsonFormat_Human);
+              result[tagName].append(target);
+            }
+          }
+          else
+          {
+            // TODO IsBinary
+            result[tagName] = it->second->GetContent();
+          }
+          break;
+        }
+
+        case DicomToJsonFormat_Full:
+        {
+          // TODO Inject PrivateCreator if some is available in the DicomMap?
+          const std::string tagName = GetTagName(it->first, "");
+
+          Json::Value value = Json::objectValue;
+
+          value["Name"] = tagName;
+
+          if (it->second->IsNull())
+          {
+            value["Type"] = "Null";
+            value["Value"] = Json::nullValue;
+          }
+          else if (it->second->IsSequence())
+          {
+            value["Type"] = "Sequence";
+            value["Value"] = it->second->GetSequenceContent();
+          }
+          else
+          {
+            // TODO IsBinary
+            value["Type"] = "String";
+            value["Value"] = it->second->GetContent();
+          }
+
+          result[it->first.Format()] = value;
+          break;
+        }
+
+        case DicomToJsonFormat_Short:
+        {
+          const std::string hex = it->first.Format();
+
+          if (it->second->IsNull())
+          {
+            result[hex] = Json::nullValue;
+          }
+          else if (it->second->IsSequence())
+          {
+            result[hex] = Json::arrayValue;
+            const Json::Value& jsonSequence = it->second->GetSequenceContent();
+
+            for (Json::Value::ArrayIndex i = 0; i < jsonSequence.size(); ++i)
+            {
+              Json::Value target = Json::objectValue;
+              Toolbox::SimplifyDicomAsJson(target, jsonSequence[i], DicomToJsonFormat_Short);
+              result[hex].append(target);
+            }
+          }
+          else
+          {
+            // TODO IsBinary
+            result[hex] = it->second->GetContent();
+          }
+
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+
+
+  std::string FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType level)
+  {
+    char uid[100];
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        // The "PatientID" field is of type LO (Long String), 64
+        // Bytes Maximum. An UUID is of length 36, thus it can be used
+        // as a random PatientID.
+        return Toolbox::GenerateUuid();
+
+      case ResourceType_Instance:
+        return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT);
+
+      case ResourceType_Series:
+        return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT);
+
+      case ResourceType_Study:
+        return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT);
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+#if 0
+  /**
+   * This was the implementation in Orthanc <= 1.12.7. This version
+   * uses "DcmFileFormat::calcElementLength()", which cannot handle
+   * DICOM files whose size cannot be represented on 32 bits.
+   **/
+  static bool SaveToMemoryBufferInternal(std::string& buffer,
+                                         DcmFileFormat& dicom,
+                                         E_TransferSyntax xfer,
+                                         std::string& errorMessage)
+  {
+    E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
+
+    // Create a memory buffer with the proper size
+    {
+      const uint32_t estimatedSize = dicom.calcElementLength(xfer, encodingType);  // (*)
+      buffer.resize(estimatedSize);
+    }
+
+    DcmOutputBufferStream ob(&buffer[0], buffer.size());
+
+    // Fill the memory buffer with the meta-header and the dataset
+    dicom.transferInit();
+    OFCondition c = dicom.write(ob, xfer, encodingType, NULL,
+                                /*opt_groupLength*/ EGL_recalcGL,
+                                /*opt_paddingType*/ EPD_noChange,
+                                /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
+                                EWM_updateMeta /* creates new SOP instance UID on lossy */);
+    dicom.transferEnd();
+
+    if (c.good())
+    {
+      // The DICOM file is successfully written, truncate the target
+      // buffer if its size was overestimated by (*)
+      ob.flush();
+
+      size_t effectiveSize = static_cast(ob.filled());
+      if (effectiveSize < buffer.size())
+      {
+        buffer.resize(effectiveSize);
+      }
+
+      return true;
+    }
+    else
+    {
+      // Error
+      buffer.clear();
+      errorMessage = std::string(c.text());
+      return false;
+    }
+  }
+#endif
+
+
+#if 1
+  /**
+   * This is the cleaner implementation used in Orthanc >= 1.12.8,
+   * which allows to write DICOM files larger than 4GB.
+   **/
+  static bool SaveToMemoryBufferInternal(std::string& buffer,
+                                         DcmFileFormat& dicom,
+                                         E_TransferSyntax xfer,
+                                         std::string& errorMessage)
+  {
+    ChunkedBufferStream ob;
+
+    // Fill the (chunked) memory buffer with the meta-header and the dataset
+    dicom.transferInit();
+    OFCondition c = dicom.write(ob, xfer, /*opt_sequenceType*/ EET_ExplicitLength, NULL,
+                                /*opt_groupLength*/ EGL_recalcGL,
+                                /*opt_paddingType*/ EPD_noChange,
+                                /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
+                                EWM_updateMeta /* creates new SOP instance UID on lossy */);
+    dicom.transferEnd();
+
+    if (c.good())
+    {
+      ob.flush();
+      ob.Flatten(buffer);
+      return true;
+    }
+    else
+    {
+      // Error
+      buffer.clear();
+      errorMessage = std::string(c.text());
+      return false;
+    }
+  }
+#endif
+
+
+  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
+                                           DcmDataset& dataSet)
+  {
+    std::string errorMessageNotUsed;
+    return SaveToMemoryBuffer(buffer, dataSet, errorMessageNotUsed);
+  }
+
+
+
+  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
+                                           DcmDataset& dataSet,
+                                           std::string& errorMessage)
+  {
+    // Determine the transfer syntax which shall be used to write the
+    // information to the file. If not possible, switch to the Little
+    // Endian syntax, with explicit length.
+
+    // http://support.dcmtk.org/docs/dcxfer_8h-source.html
+
+
+    /**
+     * Note that up to Orthanc 0.7.1 (inclusive), the
+     * "EXS_LittleEndianExplicit" was always used to save the DICOM
+     * dataset into memory. We now keep the original transfer syntax
+     * (if available).
+     **/
+    E_TransferSyntax xfer = dataSet.getCurrentXfer();
+    if (xfer == EXS_Unknown)
+    {
+      // No information about the original transfer syntax: This is
+      // most probably a DICOM dataset that was read from memory.
+      xfer = EXS_LittleEndianExplicit;
+    }
+
+    // Create the meta-header information
+    DcmFileFormat ff(&dataSet);
+    ff.validateMetaInfo(xfer);
+    ff.removeInvalidGroups();
+
+    return SaveToMemoryBufferInternal(buffer, ff, xfer, errorMessage);
+  }
+
+
+  bool FromDcmtkBridge::Transcode(DcmFileFormat& dicom,
+                                  DicomTransferSyntax syntax,
+                                  const DcmRepresentationParameter* representation)
+  {
+    E_TransferSyntax xfer;
+    if (!LookupDcmtkTransferSyntax(xfer, syntax))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      DicomTransferSyntax sourceSyntax;
+      bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom);
+      
+      if (!dicom.chooseRepresentation(xfer, representation).good() ||
+          !dicom.canWriteXfer(xfer) ||
+          !dicom.validateMetaInfo(xfer, EWM_updateMeta).good())
+      {
+        return false;
+      }
+      else
+      {
+        dicom.removeInvalidGroups();
+
+        if (known)
+        {
+          CLOG(INFO, DICOM) << "Transcoded an image from transfer syntax "
+                            << GetTransferSyntaxUid(sourceSyntax) << " to "
+                            << GetTransferSyntaxUid(syntax);
+        }
+        else
+        {
+          CLOG(INFO, DICOM) << "Transcoded an image from unknown transfer syntax to "
+                            << GetTransferSyntaxUid(syntax);
+        }
+        
+        return true;
+      }
+    }
+  }
+
+
+  ValueRepresentation FromDcmtkBridge::LookupValueRepresentation(const DicomTag& tag)
+  {
+    DcmTag t(tag.GetGroup(), tag.GetElement());
+    return Convert(t.getEVR());
+  }
+
+  ValueRepresentation FromDcmtkBridge::Convert(const DcmEVR vr)
+  {
+    switch (vr)
+    {
+      case EVR_AE:
+        return ValueRepresentation_ApplicationEntity;
+
+      case EVR_AS:
+        return ValueRepresentation_AgeString;
+
+      case EVR_AT:
+        return ValueRepresentation_AttributeTag;
+
+      case EVR_CS:
+        return ValueRepresentation_CodeString;
+
+      case EVR_DA:
+        return ValueRepresentation_Date;
+
+      case EVR_DS:
+        return ValueRepresentation_DecimalString;
+
+      case EVR_DT:
+        return ValueRepresentation_DateTime;
+
+      case EVR_FL:
+        return ValueRepresentation_FloatingPointSingle;
+
+      case EVR_FD:
+        return ValueRepresentation_FloatingPointDouble;
+
+      case EVR_IS:
+        return ValueRepresentation_IntegerString;
+
+      case EVR_LO:
+        return ValueRepresentation_LongString;
+
+      case EVR_LT:
+        return ValueRepresentation_LongText;
+
+      case EVR_OB:
+        return ValueRepresentation_OtherByte;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_OD:
+        return ValueRepresentation_OtherDouble;
+#endif
+
+      case EVR_OF:
+        return ValueRepresentation_OtherFloat;
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EVR_OL:
+        return ValueRepresentation_OtherLong;
+#endif
+
+      case EVR_OW:
+        return ValueRepresentation_OtherWord;
+
+      case EVR_PN:
+        return ValueRepresentation_PersonName;
+
+      case EVR_SH:
+        return ValueRepresentation_ShortString;
+
+      case EVR_SL:
+        return ValueRepresentation_SignedLong;
+
+      case EVR_SQ:
+        return ValueRepresentation_Sequence;
+
+      case EVR_SS:
+        return ValueRepresentation_SignedShort;
+
+      case EVR_ST:
+        return ValueRepresentation_ShortText;
+
+      case EVR_TM:
+        return ValueRepresentation_Time;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UC:
+        return ValueRepresentation_UnlimitedCharacters;
+#endif
+
+      case EVR_UI:
+        return ValueRepresentation_UniqueIdentifier;
+
+      case EVR_UL:
+        return ValueRepresentation_UnsignedLong;
+
+      case EVR_UN:
+        return ValueRepresentation_Unknown;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UR:
+        return ValueRepresentation_UniversalResource;
+#endif
+
+      case EVR_US:
+        return ValueRepresentation_UnsignedShort;
+
+      case EVR_UT:
+        return ValueRepresentation_UnlimitedText;
+
+      default:
+        return ValueRepresentation_NotSupported;
+    }
+  }
+
+
+  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag,
+                                                   const std::string& privateCreator)
+  {
+    if (tag.IsPrivate() &&
+        privateCreator.empty())
+    {
+      // This solves issue 140 (Modifying private tags with REST API
+      // changes VR from LO to UN)
+      // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=140
+      LOG(WARNING) << "Private creator should not be empty while creating a private tag: " << tag.Format();
+    }
+    
+#if DCMTK_VERSION_NUMBER >= 362
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    if (tag.IsPrivate())
+    {
+      return DcmItem::newDicomElement(key, privateCreator.c_str());
+    }
+    else
+    {
+      return DcmItem::newDicomElement(key, NULL);
+    }
+    
+#else
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    if (tag.IsPrivate())
+    {
+      // https://forum.dcmtk.org/viewtopic.php?t=4527
+      LOG(WARNING) << "You are using DCMTK <= 3.6.1: All the private tags "
+        "are considered as having a binary value representation";
+      key.setPrivateCreator(privateCreator.c_str());
+      return new DcmOtherByteOtherWord(key);
+    }
+    else
+    {
+      return newDicomElement(key);
+    }
+#endif      
+  }
+
+
+
+  void FromDcmtkBridge::FillElementWithString(DcmElement& element,
+                                              const std::string& utf8Value,
+                                              bool decodeDataUriScheme,
+                                              Encoding dicomEncoding)
+  {
+    std::string binary;
+    const std::string* decoded = &utf8Value;
+
+    if (decodeDataUriScheme &&
+        boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
+    {
+      std::string mime;
+      if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      decoded = &binary;
+    }
+    else if (dicomEncoding != Encoding_Utf8)
+    {
+      binary = Toolbox::ConvertFromUtf8(utf8Value, dicomEncoding);
+      decoded = &binary;
+    }
+
+    if (IsBinaryTag(element.getTag()))
+    {
+      bool ok;
+
+      switch (element.getTag().getEVR())
+      {
+        case EVR_OW:
+          if (decoded->size() % sizeof(Uint16) != 0)
+          {
+            LOG(ERROR) << "A tag with OW VR must have an even number of bytes";
+            ok = false;
+          }
+          else
+          {
+            ok = element.putUint16Array((const Uint16*) decoded->c_str(), decoded->size() / sizeof(Uint16)).good();
+          }
+          
+          break;
+      
+        default:
+          ok = element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good();
+          break;
+      }
+      
+      if (ok)
+      {
+        return;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    bool ok = false;
+    
+    try
+    {
+      switch (element.getTag().getEVR())
+      {
+        // http://support.dcmtk.org/docs/dcvr_8h-source.html
+
+        /**
+         * TODO.
+         **/
+
+        case EVR_OB:  // other byte
+        case EVR_OW:  // other word
+          throw OrthancException(ErrorCode_NotImplemented);
+    
+        case EVR_UN:  // unknown value representation
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+
+
+          /**
+           * String types.
+           **/
+      
+        case EVR_DS:  // decimal string
+        case EVR_IS:  // integer string
+        case EVR_AS:  // age string
+        case EVR_DA:  // date string
+        case EVR_DT:  // date time string
+        case EVR_TM:  // time string
+        case EVR_AE:  // application entity title
+        case EVR_CS:  // code string
+        case EVR_SH:  // short string
+        case EVR_LO:  // long string
+        case EVR_ST:  // short text
+        case EVR_LT:  // long text
+        case EVR_UT:  // unlimited text
+        case EVR_PN:  // person name
+        case EVR_UI:  // unique identifier
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_UC:  // unlimited characters
+        case EVR_UR:  // URI/URL
+#endif
+        {
+          ok = element.putString(decoded->c_str()).good();
+          break;
+        }
+
+        
+        /**
+         * Numerical types
+         **/ 
+      
+        case EVR_SL:  // signed long
+        {
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putSint32(boost::lexical_cast(*decoded)).good();
+          }
+          break;
+        }
+
+        case EVR_SS:  // signed short
+        {
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putSint16(boost::lexical_cast(*decoded)).good();
+          }
+          break;
+        }
+
+        case EVR_UL:  // unsigned long
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:  // other long (requires byte-swapping)
+#endif
+        {
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putUint32(boost::lexical_cast(*decoded)).good();
+          }
+          break;
+        }
+
+        case EVR_xs: // unsigned short, signed short or multiple values
+        {
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else if (decoded->find('-') != std::string::npos)
+          {
+            ok = element.putSint16(boost::lexical_cast(*decoded)).good();
+          }
+          else
+          {
+            ok = element.putUint16(boost::lexical_cast(*decoded)).good();  
+          }
+          break;
+        }
+
+        case EVR_US:  // unsigned short
+        {
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putUint16(boost::lexical_cast(*decoded)).good();
+          }
+          break;
+        }
+
+        case EVR_FL:  // float single-precision
+        case EVR_OF:  // other float (requires byte swapping)
+        {
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putFloat32(boost::lexical_cast(*decoded)).good();
+          }
+          break;
+        }
+
+        case EVR_FD:  // float double-precision
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:  // other double (requires byte-swapping)
+#endif
+        {
+          if (decoded->find('\\') != std::string::npos)
+          {
+            ok = element.putString(decoded->c_str()).good();
+          }
+          else
+          {
+            ok = element.putFloat64(boost::lexical_cast(*decoded)).good();
+          }
+          break;
+        }
+
+
+        /**
+         * Other types
+         **/
+        
+        case EVR_AT:  // attribute tag, new in Orthanc 1.9.4
+        {
+          DicomTag value = ParseTag(utf8Value);
+          ok = element.putTagVal(DcmTagKey(value.GetGroup(), value.GetElement())).good();
+          break;
+        }
+
+          
+        /**
+         * Sequence types, should never occur at this point.
+         **/
+
+        case EVR_SQ:  // sequence of items
+        {
+          ok = false;
+          break;
+        }
+
+
+        /**
+         * Internal to DCMTK.
+         **/ 
+
+        case EVR_ox:  // OB or OW depending on context
+        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+        case EVR_na:  // na="not applicable", for data which has no VR
+        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+        case EVR_item:  // used internally for items
+        case EVR_metainfo:  // used internally for meta info datasets
+        case EVR_dataset:  // used internally for datasets
+        case EVR_fileFormat:  // used internally for DICOM files
+        case EVR_dicomDir:  // used internally for DICOMDIR objects
+        case EVR_dirRecord:  // used internally for DICOMDIR records
+        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+        case EVR_pixelItem:  // used internally for pixel items in a compressed image
+        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        case EVR_PixelData:  // used internally for uncompressed pixeld data
+        case EVR_OverlayData:  // used internally for overlay data
+        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+        default:
+          break;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      ok = false;
+    }
+
+    if (!ok)
+    {
+      DicomTag tag(element.getTag().getGroup(), element.getTag().getElement());
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "While creating a DICOM instance, tag (" + tag.Format() +
+                             ") has out-of-range value: \"" + (*decoded) + "\"");
+    }
+  }
+
+
+  DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag,
+                                        const Json::Value& value,
+                                        bool decodeDataUriScheme,
+                                        Encoding dicomEncoding,
+                                        const std::string& privateCreator)
+  {
+    std::unique_ptr element;
+
+    switch (value.type())
+    {
+      case Json::stringValue:
+        element.reset(CreateElementForTag(tag, privateCreator));
+        FillElementWithString(*element, value.asString(), decodeDataUriScheme, dicomEncoding);
+        break;
+
+      case Json::nullValue:
+        element.reset(CreateElementForTag(tag, privateCreator));
+        FillElementWithString(*element, "", decodeDataUriScheme, dicomEncoding);
+        break;
+
+      case Json::arrayValue:
+      {
+        const char* p = NULL;
+        if (tag.IsPrivate() &&
+            !privateCreator.empty())
+        {
+          p = privateCreator.c_str();
+        }
+        
+        DcmTag key(tag.GetGroup(), tag.GetElement(), p);
+        if (key.getEVR() != EVR_SQ)
+        {
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Bad Parameter type for tag " + tag.Format());
+        }
+
+        DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key);
+        element.reset(sequence);
+        
+        for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+        {
+          std::unique_ptr item(new DcmItem);
+
+          switch (value[i].type())
+          {
+            case Json::objectValue:
+            {
+              Json::Value::Members members = value[i].getMemberNames();
+              for (Json::Value::ArrayIndex j = 0; j < members.size(); j++)
+              {
+                item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding, privateCreator));
+              }
+              break;
+            }
+
+            case Json::arrayValue:
+            {
+              // Lua cannot disambiguate between an empty dictionary
+              // and an empty array
+              if (value[i].size() != 0)
+              {
+                throw OrthancException(ErrorCode_BadParameterType);
+              }
+              break;
+            }
+
+            default:
+              throw OrthancException(ErrorCode_BadParameterType);
+          }
+
+          sequence->append(item.release());
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_BadParameterType, "Bad Parameter type for tag " + tag.Format());
+    }
+
+    return element.release();
+  }
+
+
+  DcmPixelSequence* FromDcmtkBridge::GetPixelSequence(DcmDataset& dataset)
+  {
+    DcmElement *element = NULL;
+    if (!dataset.findAndGetElement(DCM_PixelData, element).good())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    DcmPixelData& pixelData = dynamic_cast(*element);
+
+    E_TransferSyntax repType;
+    const DcmRepresentationParameter *repParam = NULL;
+    pixelData.getCurrentRepresentationKey(repType, repParam);
+    
+    DcmPixelSequence* pixelSequence = NULL;
+    if (!pixelData.getEncapsulatedRepresentation(repType, repParam, pixelSequence).good())
+    {
+      return NULL;
+    }
+    else
+    {
+      return pixelSequence;
+    }
+  }
+
+
+  Encoding FromDcmtkBridge::ExtractEncoding(const Json::Value& json,
+                                            Encoding defaultEncoding)
+  {
+    if (json.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    Encoding encoding = defaultEncoding;
+
+    const Json::Value::Members tags = json.getMemberNames();
+    
+    // Look for SpecificCharacterSet (0008,0005) in the JSON file
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
+      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        const Json::Value& value = json[tags[i]];
+        if (value.type() != Json::stringValue ||
+            (value.asString().length() != 0 &&
+             !GetDicomEncoding(encoding, value.asCString())))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Unknown encoding while creating DICOM from JSON: " +
+                                 value.toStyledString());
+        }
+
+        if (value.asString().length() == 0)
+        {
+          return defaultEncoding;
+        }
+      }
+    }
+
+    return encoding;
+  } 
+
+
+  static void SetString(DcmDataset& target,
+                        const DcmTag& tag,
+                        const std::string& value)
+  {
+    if (!target.putAndInsertString(tag, value.c_str()).good())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  DcmDataset* FromDcmtkBridge::FromJson(const Json::Value& json,  // Encoded using UTF-8
+                                        bool generateIdentifiers,
+                                        bool decodeDataUriScheme,
+                                        Encoding defaultEncoding,
+                                        const std::string& privateCreator)
+  {
+    std::unique_ptr result(new DcmDataset);
+    Encoding encoding = ExtractEncoding(json, defaultEncoding);
+
+    SetString(*result, DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(encoding));
+
+    const Json::Value::Members tags = json.getMemberNames();
+    
+    bool hasPatientId = false;
+    bool hasStudyInstanceUid = false;
+    bool hasSeriesInstanceUid = false;
+    bool hasSopInstanceUid = false;
+
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
+      const Json::Value& value = json[tags[i]];
+
+      if (tag == DICOM_TAG_PATIENT_ID)
+      {
+        hasPatientId = true;
+      }
+      else if (tag == DICOM_TAG_STUDY_INSTANCE_UID)
+      {
+        hasStudyInstanceUid = true;
+      }
+      else if (tag == DICOM_TAG_SERIES_INSTANCE_UID)
+      {
+        hasSeriesInstanceUid = true;
+      }
+      else if (tag == DICOM_TAG_SOP_INSTANCE_UID)
+      {
+        hasSopInstanceUid = true;
+      }
+
+      if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        std::unique_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
+
+        result->findAndDeleteElement(element->getTag());
+
+        DcmElement* tmp = element.release();
+        if (!result->insert(tmp, false, false).good())
+        {
+          delete tmp;
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+
+    if (!hasPatientId &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_PatientID, GenerateUniqueIdentifier(ResourceType_Patient));
+    }
+
+    if (!hasStudyInstanceUid &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_StudyInstanceUID, GenerateUniqueIdentifier(ResourceType_Study));
+    }
+
+    if (!hasSeriesInstanceUid &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_SeriesInstanceUID, GenerateUniqueIdentifier(ResourceType_Series));
+    }
+
+    if (!hasSopInstanceUid &&
+        generateIdentifiers)
+    {
+      SetString(*result, DCM_SOPInstanceUID, GenerateUniqueIdentifier(ResourceType_Instance));
+    }
+
+    return result.release();
+  }
+
+
+  DcmFileFormat* FromDcmtkBridge::LoadFromMemoryBuffer(const void* buffer,
+                                                       size_t size)
+  {
+    DcmInputBufferStream is;
+    if (size > 0)
+    {
+      is.setBuffer(buffer, size);
+    }
+    is.setEos();
+
+    std::unique_ptr result(new DcmFileFormat);
+
+    result->transferInit();
+
+    /**
+     * New in Orthanc 1.6.0: The "size" is given as an argument to the
+     * "read()" method. This can avoid huge memory consumption if
+     * parsing an invalid DICOM file, which can notably been observed
+     * by executing the integration test "test_upload_compressed" on
+     * valgrind running Orthanc.
+     **/
+    if (!result->read(is, EXS_Unknown, EGL_noChange, size).good())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot parse an invalid DICOM file (size: " +
+                             boost::lexical_cast(size) + " bytes)");
+    }
+
+    result->loadAllDataIntoMemory();
+    result->transferEnd();
+
+    return result.release();
+  }
+
+
+  void FromDcmtkBridge::FromJson(DicomMap& target,
+                                 const Json::Value& source,
+                                 const char* fieldName)
+  {
+    if (source.type() != Json::objectValue)
+    {
+      if (fieldName != NULL) 
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, std::string("Expecting an object in field '") + std::string(fieldName) + std::string("'"));
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Expecting an object");
+      }
+    }
+
+    target.Clear();
+
+    Json::Value::Members members = source.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& value = source[members[i]];
+
+      if (value.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, std::string("Expecting a string in field '") + members[i] + std::string("'"));
+      }
+      
+      target.SetValue(ParseTag(members[i]), value.asString(), false);
+    }
+  }
+
+
+
+
+  void FromDcmtkBridge::ChangeStringEncoding(DcmItem& dataset,
+                                             Encoding source,
+                                             bool hasSourceCodeExtensions,
+                                             Encoding target)
+  {
+    // Recursive exploration of a dataset to change the encoding of
+    // each string-like element
+
+    if (source == target)
+    {
+      return;
+    }
+
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element)
+      {
+        if (element->isLeaf())
+        {
+          char *c = NULL;
+          if (element->isaString() &&
+              element->getString(c).good() && 
+              c != NULL)
+          {
+            std::string a = Toolbox::ConvertToUtf8(c, source, hasSourceCodeExtensions);
+            std::string b = Toolbox::ConvertFromUtf8(a, target);
+            element->putString(b.c_str());
+          }
+        }
+        else
+        {
+          // "All subclasses of DcmElement except for DcmSequenceOfItems
+          // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
+          // etc. are not." The following dynamic_cast is thus OK.
+          DcmSequenceOfItems& sequence = dynamic_cast(*element);
+
+          for (unsigned long j = 0; j < sequence.card(); j++)
+          {
+            ChangeStringEncoding(*sequence.getItem(j), source, hasSourceCodeExtensions, target);
+          }
+        }
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::InitializeCodecs()
+  {
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    CLOG(INFO, DICOM) << "Registering JPEG Lossless codecs in DCMTK";
+    DJLSDecoderRegistration::registerCodecs();
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJLSEncoderRegistration::registerCodecs();
+# endif
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    CLOG(INFO, DICOM) << "Registering JPEG codecs in DCMTK";
+    DJDecoderRegistration::registerCodecs();
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJEncoderRegistration::registerCodecs();
+# endif
+#endif
+
+    CLOG(INFO, DICOM) << "Registering RLE codecs in DCMTK";
+    DcmRLEDecoderRegistration::registerCodecs(); 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DcmRLEEncoderRegistration::registerCodecs();
+#endif
+  }
+
+
+  void FromDcmtkBridge::FinalizeCodecs()
+  {
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    // Unregister JPEG-LS codecs
+    DJLSDecoderRegistration::cleanup();
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJLSEncoderRegistration::cleanup();
+# endif
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    // Unregister JPEG codecs
+    DJDecoderRegistration::cleanup();
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJEncoderRegistration::cleanup();
+# endif
+#endif
+
+    DcmRLEDecoderRegistration::cleanup(); 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DcmRLEEncoderRegistration::cleanup();
+#endif
+  }
+
+
+
+  // Forward declaration
+  static bool ApplyVisitorToElement(DcmElement& element,
+                                    ITagVisitor& visitor,
+                                    const std::vector& parentTags,
+                                    const std::vector& parentIndexes,
+                                    Encoding encoding,
+                                    bool hasCodeExtensions);
+ 
+  static void ApplyVisitorToDataset(DcmItem& dataset,
+                                    ITagVisitor& visitor,
+                                    const std::vector& parentTags,
+                                    const std::vector& parentIndexes,
+                                    Encoding encoding,
+                                    bool hasCodeExtensions)
+  {
+    assert(parentTags.size() == parentIndexes.size());
+
+    std::set toRemove;
+    
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        if (!ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions))
+        {
+          toRemove.insert(element->getTag());
+        }
+      }      
+    }
+
+    // Remove all the tags that were planned for removal (cf. ITagVisitor::Action_Remove)
+    for (std::set::const_iterator
+           it = toRemove.begin(); it != toRemove.end(); ++it)
+    {
+      std::unique_ptr tmp(dataset.remove(*it));
+    }
+  }
+
+
+  // Returns "true" iff the element must be kept. If "false" is
+  // returned, the element will be removed.
+  static bool ApplyVisitorToLeaf(DcmElement& element,
+                                 ITagVisitor& visitor,
+                                 const std::vector& parentTags,
+                                 const std::vector& parentIndexes,
+                                 const DicomTag& tag,
+                                 Encoding encoding,
+                                 bool hasCodeExtensions)
+  {
+    // TODO - Merge this function, that is more recent, with ConvertLeafElement()
+
+    assert(element.isLeaf());
+
+    DcmEVR evr = element.getTag().getEVR();
+
+    
+    /**
+     * Fix the EVR for types internal to DCMTK 
+     **/
+
+    if (evr == EVR_ox)  // OB or OW depending on context
+    {
+      evr = EVR_OB;
+    }
+    else if (evr == EVR_xs) // SS or US depending on context
+    {
+      // So far we assume that it's alway US (as a best guess: https://forum.dcmtk.org/viewtopic.php?t=932)
+      // However, e.g. in a LUTDescriptor (3 values), the middle value can be a SS depending on other tag values while first and third value are always US.
+      // This patch, although not perfect fixes  https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=214.
+      // It might need some rework once we encounter a LUTDescriptor with a SS value. ref: https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.1
+      evr = EVR_US;  
+    }
+    else if (evr == EVR_lt) // US, SS or OW depending on context, used for LUT Data (thus the name)
+    {
+      // best guess is OW: final user should be able to interpret it correctly depending on the context
+      evr = EVR_OW;      
+    }
+
+    if (evr == EVR_UNKNOWN ||  // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        evr == EVR_UNKNOWN2B)  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+    {
+      evr = EVR_UN;
+    }
+
+    if (evr == EVR_UN)
+    {
+      // New in Orthanc 1.9.5
+      FromDcmtkBridge::DictionaryReaderLock lock;
+
+      // The "entry" value is only valid while "lock" is active
+      const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(),
+                                                                 element.getTag().getPrivateCreator());
+
+      if (entry != NULL)
+      {
+        evr = entry->getEVR();
+      }
+    }
+
+    const ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
+
+    
+    /**
+     * Deal with binary data (including PixelData).
+     **/
+
+    if (evr == EVR_OB ||  // other byte
+        evr == EVR_OW ||  // other word
+        evr == EVR_UN)    // unknown value representation
+    {
+      Uint16* data16 = NULL;
+      Uint8* data = NULL;
+
+      ITagVisitor::Action action;
+      
+      if ((element.getTag() == DCM_PixelData ||  // (*) New in Orthanc 1.9.1
+           evr == EVR_OW) &&
+          element.getUint16Array(data16) == EC_Normal)
+      {
+        action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
+      }
+      else if (evr != EVR_OW &&
+               element.getUint8Array(data) == EC_Normal)
+      {
+        /**
+         * WARNING: The call to "getUint8Array()" crashes
+         * (segmentation fault) on big-endian architectures if applied
+         * to pixel data, during the call to "swapIfNecessary()" in
+         * "DcmPolymorphOBOW::getUint8Array()" (this method is not
+         * reimplemented in derived class "DcmPixelData"). However,
+         * "getUint16Array()" works correctly, hence (*).
+         **/
+        action = visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
+      }
+      else
+      {
+        action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+      }
+
+      switch (action)
+      {
+        case ITagVisitor::Action_None:
+          return true;  // We're done
+
+        case ITagVisitor::Action_Remove:
+          return false;
+
+        case ITagVisitor::Action_Replace:
+          throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace binary data");
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    /**
+     * Deal with plain strings (and convert them to UTF-8)
+     **/
+
+    char *c = NULL;
+    if (element.isaString() &&
+        element.getString(c).good())
+    {
+      std::string utf8;
+
+      if (c != NULL)  // This case corresponds to the empty string
+      {
+        if (element.getTag() == DCM_SpecificCharacterSet)
+        {
+          utf8.assign(c);
+        }
+        else
+        {
+          std::string s(c);
+          utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
+        }
+      }
+
+      std::string newValue;
+      ITagVisitor::Action action = visitor.VisitString
+        (newValue, parentTags, parentIndexes, tag, vr, utf8);
+
+      switch (action)
+      {
+        case ITagVisitor::Action_None:
+          return true;
+
+        case ITagVisitor::Action_Remove:
+          return false;
+
+        case ITagVisitor::Action_Replace:
+        {
+          std::string s = Toolbox::ConvertFromUtf8(newValue, encoding);
+          if (element.putString(s.c_str()) != EC_Normal)
+          {
+            throw OrthancException(ErrorCode_InternalError,
+                                   "Iterator cannot replace value of tag: " + tag.Format());
+          }
+
+          return true;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+
+    ITagVisitor::Action action;
+    
+    try
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+      switch (evr)
+      {
+
+        /**
+         * Plain string values.
+         **/
+
+        case EVR_DS:  // decimal string
+        case EVR_IS:  // integer string
+        case EVR_AS:  // age string
+        case EVR_DA:  // date string
+        case EVR_DT:  // date time string
+        case EVR_TM:  // time string
+        case EVR_AE:  // application entity title
+        case EVR_CS:  // code string
+        case EVR_SH:  // short string
+        case EVR_LO:  // long string
+        case EVR_ST:  // short text
+        case EVR_LT:  // long text
+        case EVR_UT:  // unlimited text
+        case EVR_PN:  // person name
+        case EVR_UI:  // unique identifier
+        {
+          Uint8* data = NULL;
+          
+          if (element.getUint8Array(data) == EC_Normal)
+          {
+            const Uint32 length = element.getLength();
+            Uint32 l = 0;
+            while (l < length &&
+                   data[l] != 0)
+            {
+              l++;
+            }
+
+            std::string ignored;
+            std::string s(reinterpret_cast(data), l);
+            action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr,
+                                         Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
+          }
+          else
+          {
+            action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+          }
+
+          if (action == ITagVisitor::Action_Replace)
+          {
+            LOG(WARNING) << "Iterator cannot replace this string tag: "
+                         << FromDcmtkBridge::GetTagName(element)
+                         << " (" << tag.Format() << ")";
+            return true;
+          }
+
+          break;
+        }
+    
+        /**
+         * Numeric types
+         **/ 
+      
+        case EVR_SL:  // signed long
+        {
+          DcmSignedLong& content = dynamic_cast(element);
+
+          std::vector values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Sint32 f;
+            if (content.getSint32(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_SS:  // signed short
+        {
+          DcmSignedShort& content = dynamic_cast(element);
+
+          std::vector values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Sint16 f;
+            if (content.getSint16(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_UL:  // unsigned long
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:
+#endif
+        {
+          DcmUnsignedLong& content = dynamic_cast(element);
+
+          std::vector values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Uint32 f;
+            if (content.getUint32(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_US:  // unsigned short
+        {
+          DcmUnsignedShort& content = dynamic_cast(element);
+
+          std::vector values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Uint16 f;
+            if (content.getUint16(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          action = visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_FL:  // float single-precision
+        case EVR_OF:
+        {
+          DcmFloatingPointSingle& content = dynamic_cast(element);
+
+          std::vector values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Float32 f;
+            if (content.getFloat32(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_FD:  // float double-precision
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:
+#endif
+        {
+          DcmFloatingPointDouble& content = dynamic_cast(element);
+
+          std::vector values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Float64 f;
+            if (content.getFloat64(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          action = visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+
+        /**
+         * Attribute tag.
+         **/
+
+        case EVR_AT:
+        {
+          DcmAttributeTag& content = dynamic_cast(element);
+
+          std::vector values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            DcmTagKey f;
+            if (content.getTagVal(f, i).good())
+            {
+              DicomTag t(f.getGroup(), f.getElement());
+              values.push_back(t);
+            }
+          }
+
+          assert(vr == ValueRepresentation_AttributeTag);
+          action = visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
+          break;
+        }
+
+
+        /**
+         * Sequence types, should never occur at this point because of
+         * "element.isLeaf()".
+         **/
+
+        case EVR_SQ:  // sequence of items
+        {
+          return true;
+        }
+        
+        
+        /**
+         * Internal to DCMTK.
+         **/ 
+
+        case EVR_xs:  // SS or US depending on context
+        case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+        case EVR_na:  // na="not applicable", for data which has no VR
+        case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+        case EVR_item:  // used internally for items
+        case EVR_metainfo:  // used internally for meta info datasets
+        case EVR_dataset:  // used internally for datasets
+        case EVR_fileFormat:  // used internally for DICOM files
+        case EVR_dicomDir:  // used internally for DICOMDIR objects
+        case EVR_dirRecord:  // used internally for DICOMDIR records
+        case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+        case EVR_pixelItem:  // used internally for pixel items in a compressed image
+        case EVR_PixelData:  // used internally for uncompressed pixeld data
+        case EVR_OverlayData:  // used internally for overlay data
+        {
+          action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+          break;
+        }
+        
+
+        /**
+         * Default case.
+         **/ 
+
+        default:
+          return true;
+      }
+
+      switch (action)
+      {
+        case ITagVisitor::Action_None:
+          return true;  // We're done
+
+        case ITagVisitor::Action_Remove:
+          return false;
+
+        case ITagVisitor::Action_Replace:
+          throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace non-string-like data");
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return true;
+    }
+    catch (std::bad_cast&)
+    {
+      return true;
+    }
+  }
+
+
+  // Returns "true" iff the element must be kept. If "false" is
+  // returned, the element will be removed.
+  static bool ApplyVisitorToElement(DcmElement& element,
+                                    ITagVisitor& visitor,
+                                    const std::vector& parentTags,
+                                    const std::vector& parentIndexes,
+                                    Encoding encoding,
+                                    bool hasCodeExtensions)
+  {
+    assert(parentTags.size() == parentIndexes.size());
+
+    DicomTag tag(FromDcmtkBridge::Convert(element.getTag()));
+
+    if (element.isLeaf())
+    {
+      return ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions);
+    }
+    else
+    {
+      // "All subclasses of DcmElement except for DcmSequenceOfItems
+      // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
+      // etc. are not." The following dynamic_cast is thus OK.
+      DcmSequenceOfItems& sequence = dynamic_cast(element);
+
+      ITagVisitor::Action action = visitor.VisitSequence(parentTags, parentIndexes, tag, sequence.card());
+
+      switch (action)
+      {
+        case ITagVisitor::Action_None:
+          if (sequence.card() != 0)  // Minor optimization to avoid creating "tags" and "indexes" if not needed
+          {
+            std::vector tags = parentTags;
+            std::vector indexes = parentIndexes;
+            tags.push_back(tag);
+            indexes.push_back(0);
+
+            for (unsigned long i = 0; i < sequence.card(); i++)
+            {
+              indexes.back() = static_cast(i);
+              DcmItem* child = sequence.getItem(i);
+              ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding, hasCodeExtensions);
+            }
+          }
+
+          return true;  // Keep
+
+        case ITagVisitor::Action_Remove:
+          return false;
+
+        case ITagVisitor::Action_Replace:
+          throw OrthancException(ErrorCode_NotImplemented, "Iterator cannot replace sequences");
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+    }
+  }
+
+
+  void FromDcmtkBridge::Apply(DcmItem& dataset,
+                              ITagVisitor& visitor,
+                              Encoding defaultEncoding)
+  {
+    std::vector parentTags;
+    std::vector parentIndexes;
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+    ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions);
+  }
+
+
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    DcmFileFormat& dicom)
+  {
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      return LookupOrthancTransferSyntax(target, *dicom.getDataset());
+    }
+  }
+
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    DcmDataset& dataset)
+  {
+    E_TransferSyntax xfer = dataset.getCurrentXfer();
+    if (xfer == EXS_Unknown)
+    {
+      dataset.updateOriginalXfer();
+      xfer = dataset.getOriginalXfer();
+      if (xfer == EXS_Unknown)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot determine the transfer syntax of the DICOM instance");
+      }
+    }
+
+    return FromDcmtkBridge::LookupOrthancTransferSyntax(target, xfer);
+  }
+
+
+  std::string FromDcmtkBridge::FormatMissingTagsForStore(DcmDataset& dicom)
+  {
+    std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
+
+    const char* c = NULL;
+    if (dicom.findAndGetString(DCM_PatientID, c).good() &&
+        c != NULL)
+    {
+      patientId.assign(c);
+    }
+
+    c = NULL;
+    if (dicom.findAndGetString(DCM_StudyInstanceUID, c).good() &&
+        c != NULL)
+    {
+      studyInstanceUid.assign(c);
+    }
+
+    c = NULL;
+    if (dicom.findAndGetString(DCM_SeriesInstanceUID, c).good() &&
+        c != NULL)
+    {
+      seriesInstanceUid.assign(c);
+    }
+
+    c = NULL;
+    if (dicom.findAndGetString(DCM_SOPInstanceUID, c).good() &&
+        c != NULL)
+    {
+      sopInstanceUid.assign(c);
+    }
+    
+    return DicomMap::FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+  }
+
+
+  void FromDcmtkBridge::IDicomPathVisitor::ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor,
+                                                         DcmItem& item,
+                                                         const DicomPath& pattern,
+                                                         const DicomPath& actualPath)
+  {
+    const size_t level = actualPath.GetPrefixLength();
+      
+    if (level == pattern.GetPrefixLength())
+    {
+      visitor.Visit(item, actualPath);
+    }
+    else
+    {
+      assert(level < pattern.GetPrefixLength());
+
+      const DicomTag& tmp = pattern.GetPrefixTag(level);
+      DcmTagKey tag(tmp.GetGroup(), tmp.GetElement());
+
+      DcmSequenceOfItems *sequence = NULL;
+      if (item.findAndGetSequence(tag, sequence).good() &&
+          sequence != NULL)
+      {
+        for (unsigned long i = 0; i < sequence->card(); i++)
+        {
+          if (pattern.IsPrefixUniversal(level) ||
+              pattern.GetPrefixIndex(level) == static_cast(i))
+          {
+            DcmItem *child = sequence->getItem(i);
+            if (child != NULL)
+            {
+              DicomPath childPath = actualPath;
+              childPath.AddIndexedTagToPrefix(pattern.GetPrefixTag(level), static_cast(i));
+              
+              ApplyInternal(visitor, *child, pattern, childPath);
+            }
+          }
+        }
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::IDicomPathVisitor::Apply(IDicomPathVisitor& visitor,
+                                                 DcmDataset& dataset,
+                                                 const DicomPath& path)
+  {
+    DicomPath actualPath(path.GetFinalTag());
+    ApplyInternal(visitor, dataset, path, actualPath);
+  }
+
+
+  void FromDcmtkBridge::RemovePath(DcmDataset& dataset,
+                                   const DicomPath& path)
+  {
+    class Visitor : public FromDcmtkBridge::IDicomPathVisitor
+    {
+    public:
+      virtual void Visit(DcmItem& item,
+                         const DicomPath& path) ORTHANC_OVERRIDE
+      {
+        DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
+        std::unique_ptr removed(item.remove(key));
+      }
+    };
+    
+    Visitor visitor;
+    IDicomPathVisitor::Apply(visitor, dataset, path);
+  }
+  
+
+  void FromDcmtkBridge::ClearPath(DcmDataset& dataset,
+                                  const DicomPath& path,
+                                  bool onlyIfExists)
+  {
+    class Visitor : public FromDcmtkBridge::IDicomPathVisitor
+    {
+    public:
+      bool  onlyIfExists_;
+      
+    public:
+      explicit Visitor(bool onlyIfExists) :
+        onlyIfExists_(onlyIfExists)
+      {
+      }
+      
+      virtual void Visit(DcmItem& item,
+                         const DicomPath& path) ORTHANC_OVERRIDE
+      {
+        DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
+
+        if (onlyIfExists_ &&
+            !item.tagExists(key))
+        {
+          // The tag is non-existing, do not clear it
+        }
+        else
+        {
+          if (!item.insertEmptyElement(key, OFTrue /* replace old value */).good())
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+      }
+    };
+    
+    Visitor visitor(onlyIfExists);
+    IDicomPathVisitor::Apply(visitor, dataset, path);
+  }
+  
+
+  void FromDcmtkBridge::ReplacePath(DcmDataset& dataset,
+                                    const DicomPath& path,
+                                    const DcmElement& element,
+                                    DicomReplaceMode mode)
+  {
+    class Visitor : public FromDcmtkBridge::IDicomPathVisitor
+    {
+    private:
+      std::unique_ptr element_;
+      DicomReplaceMode            mode_;
+    
+    public:
+      Visitor(const DcmElement& element,
+              DicomReplaceMode mode) :
+        element_(dynamic_cast(element.clone())),
+        mode_(mode)
+      {
+        if (element_.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot clone DcmElement");
+        }
+      }
+    
+      virtual void Visit(DcmItem& item,
+                         const DicomPath& path) ORTHANC_OVERRIDE
+      {
+        std::unique_ptr cloned(dynamic_cast(element_->clone()));
+        if (cloned.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot clone DcmElement");
+        }
+        else
+        {      
+          DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
+
+          if (!item.tagExists(key))
+          {
+            switch (mode_)
+            {
+              case DicomReplaceMode_InsertIfAbsent:
+                break;  // Fine, we can proceed with insertion
+                
+              case DicomReplaceMode_ThrowIfAbsent:
+                throw OrthancException(ErrorCode_InexistentItem, "Cannot replace inexistent tag: " + GetTagName(*element_));
+                
+              case DicomReplaceMode_IgnoreIfAbsent:
+                return;  // Don't proceed with insertion
+                
+              default:
+                throw OrthancException(ErrorCode_ParameterOutOfRange);
+            }
+          }
+          
+          if (!item.insert(cloned.release(), OFTrue /* replace old */).good())
+          {
+            throw OrthancException(ErrorCode_InternalError, "Cannot replace an element: " + GetTagName(*element_));
+          }
+        }
+      }
+    };
+
+    DcmTagKey key(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
+  
+    if (element.getTag() != key)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "The final tag must be the same as the tag of the element during a replacement");
+    }
+    else
+    {
+      Visitor visitor(element, mode);
+      IDicomPathVisitor::Apply(visitor, dataset, path);
+    }
+  }
+
+
+  bool FromDcmtkBridge::LookupSequenceItem(DicomMap& target,
+                                           DcmDataset& dataset,
+                                           const DicomPath& path,
+                                           size_t sequenceIndex)
+  {
+    class Visitor : public FromDcmtkBridge::IDicomPathVisitor
+    {
+    private:
+      bool       found_;
+      DicomMap&  target_;
+      size_t     sequenceIndex_;
+      
+    public:
+      Visitor(DicomMap& target,
+              size_t sequenceIndex) :
+        found_(false),
+        target_(target),
+        sequenceIndex_(sequenceIndex)
+      {
+      }
+      
+      virtual void Visit(DcmItem& item,
+                         const DicomPath& path) ORTHANC_OVERRIDE
+      {
+        DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement());
+
+        DcmSequenceOfItems *sequence = NULL;
+        
+        if (item.findAndGetSequence(tag, sequence).good() &&
+            sequence != NULL &&
+            sequenceIndex_ < sequence->card())
+        {
+          std::set ignoreTagLength;
+          ExtractDicomSummary(target_, *sequence->getItem(sequenceIndex_), 0, ignoreTagLength);
+          found_ = true;
+        }
+      }
+
+      bool HasFound() const
+      {
+        return found_;
+      }
+    };
+
+    Visitor visitor(target, sequenceIndex);
+    IDicomPathVisitor::Apply(visitor, dataset, path);
+    return visitor.HasFound();
+  }
+
+
+  bool FromDcmtkBridge::LookupStringValue(std::string& target,
+                                          DcmDataset& dataset,
+                                          const DicomTag& key)
+  {
+    DcmTagKey dcmkey(key.GetGroup(), key.GetElement());
+    
+    const char* str = NULL;
+    const Uint8* data = NULL;
+    unsigned long size = 0;
+
+    if (dataset.findAndGetString(dcmkey, str).good() &&
+        str != NULL)
+    {
+      target.assign(str);
+      return true;
+    }
+    else if (dataset.findAndGetUint8Array(dcmkey, data, &size).good() &&
+             data != NULL &&
+             size > 0)
+    {
+      /**
+       * This special case is necessary for borderline DICOM files
+       * that have DICOM tags have the "UN" value representation. New
+       * in Orthanc 1.10.1.
+       * https://groups.google.com/g/orthanc-users/c/86fobx3ZyoM/m/KBG17un6AQAJ
+       **/
+      unsigned long l = 0;
+      while (l < size &&
+             data[l] != 0)
+      {
+        l++;
+      }
+
+      target.assign(reinterpret_cast(data), l);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+
+#include "./FromDcmtkBridge_TransferSyntaxes.impl.h"
diff --git a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h
new file mode 100644
index 0000000..19d2e12
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h
@@ -0,0 +1,341 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "ITagVisitor.h"
+#include "../DicomFormat/DicomElement.h"
+#include "../DicomFormat/DicomMap.h"
+#include "../DicomFormat/DicomPath.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1
+#endif
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include 
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined
+#endif
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC FromDcmtkBridge : public boost::noncopyable
+  {
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+    FRIEND_TEST(FromDcmtkBridge, FromJson);
+#endif
+
+    friend class ParsedDicomFile;
+
+  public:
+    // New in Orthanc 1.9.4
+    class ORTHANC_PUBLIC IDicomPathVisitor : public boost::noncopyable
+    {
+    private:
+      static void ApplyInternal(FromDcmtkBridge::IDicomPathVisitor& visitor,
+                                DcmItem& item,
+                                const DicomPath& pattern,
+                                const DicomPath& actualPath);
+      
+    public:
+      virtual ~IDicomPathVisitor()
+      {
+      }
+
+      virtual void Visit(DcmItem& item,
+                         const DicomPath& path) = 0;
+
+      static void Apply(IDicomPathVisitor& visitor,
+                        DcmDataset& dataset,
+                        const DicomPath& path);
+    };
+    
+
+    class ORTHANC_PUBLIC DictionaryWriterLock : public boost::noncopyable
+    {
+    private:
+      DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryWriterLock();
+
+      ~DictionaryWriterLock();
+
+      DcmDataDictionary& GetDictionary()
+      {
+        return dictionary_;
+      }
+    };
+
+
+    class ORTHANC_PUBLIC DictionaryReaderLock : public boost::noncopyable
+    {
+    private:
+      const DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryReaderLock();
+
+      ~DictionaryReaderLock();
+
+      const DcmDataDictionary& GetDictionary() const
+      {
+        return dictionary_;
+      }
+    };
+
+
+  private:
+    FromDcmtkBridge();  // Pure static class
+
+    static void DatasetToJson(Json::Value& parent,
+                              DcmItem& item,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength,
+                              Encoding encoding,
+                              bool hasCodeExtensions,
+                              const std::set& ignoreTagLength,
+                              unsigned int depth);
+
+    static void ElementToJson(Json::Value& parent,
+                              DcmElement& element,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength,
+                              Encoding dicomEncoding,
+                              bool hasCodeExtensions,
+                              const std::set& ignoreTagLength,
+                              unsigned int depth);
+
+    static void ChangeStringEncoding(DcmItem& dataset,
+                                     Encoding source,
+                                     bool hasSourceCodeExtensions,
+                                     Encoding target);
+
+  public:
+    /**
+     * Initialize DCMTK to use the default DICOM dictionaries (either
+     * embedded into the binaries for official releases, or using the
+     * environment variable "DCM_DICT_ENVIRONMENT_VARIABLE", or using
+     * the system-wide path to the DCMTK library for developers)
+     **/
+    static void InitializeDictionary(bool loadPrivateDictionary);
+
+    /**
+     * Replace the default DICOM dictionaries by the manually-provided
+     * external dictionaries. This is needed to use DICONDE for
+     * instance. Pay attention to the fact that the current dictionary
+     * will be reinitialized (all its tags are cleared).
+     **/
+    static void LoadExternalDictionaries(const std::vector& dictionaries);
+
+    static void RegisterDictionaryTag(const DicomTag& tag,
+                                      ValueRepresentation vr,
+                                      const std::string& name,
+                                      unsigned int minMultiplicity,
+                                      unsigned int maxMultiplicity,
+                                      const std::string& privateCreator);
+
+    static Encoding DetectEncoding(bool& hasCodeExtensions,
+                                   DcmItem& dataset,
+                                   Encoding defaultEncoding);
+
+    // Compatibility wrapper for Orthanc <= 1.5.4
+    static Encoding DetectEncoding(DcmItem& dataset,
+                                   Encoding defaultEncoding);
+
+    static DicomTag Convert(const DcmTag& tag);
+
+    static DicomTag GetTag(const DcmElement& element);
+
+    static bool IsUnknownTag(const DicomTag& tag);
+
+    static DicomValue* ConvertLeafElement(DcmElement& element,
+                                          DicomToJsonFlags flags,
+                                          unsigned int maxStringLength,
+                                          Encoding encoding,
+                                          bool hasCodeExtensions,
+                                          const std::set& ignoreTagLength);
+
+    static void ExtractHeaderAsJson(Json::Value& target, 
+                                    DcmMetaInfo& header,
+                                    DicomToJsonFormat format,
+                                    DicomToJsonFlags flags,
+                                    unsigned int maxStringLength);
+
+    static std::string GetTagName(const DicomTag& tag,
+                                  const std::string& privateCreator);
+
+    static std::string GetTagName(const DcmElement& element);
+
+    static std::string GetTagName(const DicomElement& element);
+
+    static DicomTag ParseTag(const char* name);
+
+    static DicomTag ParseTag(const std::string& name);
+
+    // parses a list like "0010,0010;PatientBirthDate;0020,0020"
+    static void ParseListOfTags(std::set& result, const std::string& source);
+
+    static void ParseListOfTags(std::set& result, const Json::Value& source);
+
+    static void FormatListOfTags(std::string& output, const std::set& tags);
+
+    static void FormatListOfTags(Json::Value& output, const std::set& tags);
+
+    static bool HasTag(const DicomMap& fields,
+                       const std::string& tagName);
+
+    static const DicomValue& GetValue(const DicomMap& fields,
+                                      const std::string& tagName);
+
+    static void SetValue(DicomMap& target,
+                         const std::string& tagName,
+                         DicomValue* value);
+
+    static void ToJson(Json::Value& result,
+                       const DicomMap& values,
+                       DicomToJsonFormat format);
+
+    static std::string GenerateUniqueIdentifier(ResourceType level);
+
+    static bool SaveToMemoryBuffer(std::string& buffer,
+                                   DcmDataset& dataSet);
+
+    static bool SaveToMemoryBuffer(std::string& buffer,
+                                   DcmDataset& dataSet,
+                                   std::string& errorMessage);
+
+    static bool Transcode(DcmFileFormat& dicom,
+                          DicomTransferSyntax syntax,
+                          const DcmRepresentationParameter* representation);
+
+    static ValueRepresentation Convert(DcmEVR vr);
+
+    static ValueRepresentation LookupValueRepresentation(const DicomTag& tag);
+
+    static DcmElement* CreateElementForTag(const DicomTag& tag,
+                                           const std::string& privateCreator);
+    
+    static void FillElementWithString(DcmElement& element,
+                                      const std::string& utf8alue,  // Encoded using UTF-8
+                                      bool decodeDataUriScheme,
+                                      Encoding dicomEncoding);
+
+    static DcmElement* FromJson(const DicomTag& tag,
+                                const Json::Value& element,  // Encoded using UTF-8
+                                bool decodeDataUriScheme,
+                                Encoding dicomEncoding,
+                                const std::string& privateCreator);
+
+    static DcmPixelSequence* GetPixelSequence(DcmDataset& dataset);
+
+    static Encoding ExtractEncoding(const Json::Value& json,
+                                    Encoding defaultEncoding);
+
+    static DcmDataset* FromJson(const Json::Value& json,  // Encoded using UTF-8
+                                bool generateIdentifiers,
+                                bool decodeDataUriScheme,
+                                Encoding defaultEncoding,
+                                const std::string& privateCreator);
+
+    static DcmFileFormat* LoadFromMemoryBuffer(const void* buffer,
+                                               size_t size);
+
+    static void FromJson(DicomMap& values,
+                         const Json::Value& result,
+                         const char* fieldName = NULL);
+
+    static void ExtractDicomSummary(DicomMap& target, 
+                                    DcmItem& dataset,
+                                    unsigned int maxStringLength,
+                                    const std::set& ignoreTagLength);
+
+    static void ExtractDicomAsJson(Json::Value& target, 
+                                   DcmDataset& dataset,
+                                   DicomToJsonFormat format,
+                                   DicomToJsonFlags flags,
+                                   unsigned int maxStringLength,
+                                   const std::set& ignoreTagLength);
+
+    static void InitializeCodecs();
+
+    static void FinalizeCodecs();
+
+    static void Apply(DcmItem& dataset,
+                      ITagVisitor& visitor,
+                      Encoding defaultEncoding);
+
+    static bool LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                          DicomTransferSyntax source);
+
+    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                            E_TransferSyntax source);
+
+    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                            DcmFileFormat& dicom);
+
+    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                            DcmDataset& dicom);
+
+    static std::string FormatMissingTagsForStore(DcmDataset& dicom);
+
+    static void RemovePath(DcmDataset& dataset,
+                           const DicomPath& path);
+
+    static void ClearPath(DcmDataset& dataset,
+                          const DicomPath& path,
+                          bool onlyIfExists);
+
+    static void ReplacePath(DcmDataset& dataset,
+                            const DicomPath& path,
+                            const DcmElement& element,
+                            DicomReplaceMode mode);
+
+    static bool LookupSequenceItem(DicomMap& target,
+                                   DcmDataset& dataset,
+                                   const DicomPath& path,
+                                   size_t sequenceIndex);
+
+    static bool LookupStringValue(std::string& target,
+                                  DcmDataset& dataset,
+                                  const DicomTag& key);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h
new file mode 100644
index 0000000..cc31f0e
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h
@@ -0,0 +1,540 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+// This file is autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                                  DicomTransferSyntax source)
+  {
+    switch (source)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        target = EXS_LittleEndianImplicit;
+        return true;
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        target = EXS_LittleEndianExplicit;
+        return true;
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        target = EXS_DeflatedLittleEndianExplicit;
+        return true;
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        target = EXS_BigEndianExplicit;
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess1:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess1TransferSyntax;
+#  else
+        target = EXS_JPEGProcess1;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess2_4TransferSyntax;
+#  else
+        target = EXS_JPEGProcess2_4;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess3_5TransferSyntax;
+#  else
+        target = EXS_JPEGProcess3_5;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess6_8TransferSyntax;
+#  else
+        target = EXS_JPEGProcess6_8;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess7_9TransferSyntax;
+#  else
+        target = EXS_JPEGProcess7_9;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess10_12TransferSyntax;
+#  else
+        target = EXS_JPEGProcess10_12;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess11_13TransferSyntax;
+#  else
+        target = EXS_JPEGProcess11_13;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess14TransferSyntax;
+#  else
+        target = EXS_JPEGProcess14;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess15:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess15TransferSyntax;
+#  else
+        target = EXS_JPEGProcess15;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess16_18TransferSyntax;
+#  else
+        target = EXS_JPEGProcess16_18;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess17_19TransferSyntax;
+#  else
+        target = EXS_JPEGProcess17_19;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess20_22TransferSyntax;
+#  else
+        target = EXS_JPEGProcess20_22;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess21_23TransferSyntax;
+#  else
+        target = EXS_JPEGProcess21_23;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess24_26TransferSyntax;
+#  else
+        target = EXS_JPEGProcess24_26;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess25_27TransferSyntax;
+#  else
+        target = EXS_JPEGProcess25_27;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess28:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess28TransferSyntax;
+#  else
+        target = EXS_JPEGProcess28;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess29:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess29TransferSyntax;
+#  else
+        target = EXS_JPEGProcess29;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess14SV1TransferSyntax;
+#  else
+        target = EXS_JPEGProcess14SV1;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        target = EXS_JPEGLSLossless;
+        return true;
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        target = EXS_JPEGLSLossy;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        target = EXS_JPEG2000LosslessOnly;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000:
+        target = EXS_JPEG2000;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        target = EXS_JPEG2000MulticomponentLosslessOnly;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        target = EXS_JPEG2000Multicomponent;
+        return true;
+
+      case DicomTransferSyntax_JPIPReferenced:
+        target = EXS_JPIPReferenced;
+        return true;
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        target = EXS_JPIPReferencedDeflate;
+        return true;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        target = EXS_MPEG2MainProfileAtMainLevel;
+        return true;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        target = EXS_MPEG2MainProfileAtHighLevel;
+        return true;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        target = EXS_MPEG4HighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        target = EXS_MPEG4BDcompatibleHighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        target = EXS_MPEG4HighProfileLevel4_2_For2DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        target = EXS_MPEG4HighProfileLevel4_2_For3DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        target = EXS_MPEG4StereoHighProfileLevel4_2;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        target = EXS_HEVCMainProfileLevel5_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        target = EXS_HEVCMain10ProfileLevel5_1;
+        return true;
+#endif
+
+      case DicomTransferSyntax_RLELossless:
+        target = EXS_RLELossless;
+        return true;
+
+      default:
+        return false;
+    }
+  }
+  
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    E_TransferSyntax source)
+  {
+    switch (source)
+    {
+      case EXS_LittleEndianImplicit:
+        target = DicomTransferSyntax_LittleEndianImplicit;
+        return true;
+
+      case EXS_LittleEndianExplicit:
+        target = DicomTransferSyntax_LittleEndianExplicit;
+        return true;
+
+      case EXS_DeflatedLittleEndianExplicit:
+        target = DicomTransferSyntax_DeflatedLittleEndianExplicit;
+        return true;
+
+      case EXS_BigEndianExplicit:
+        target = DicomTransferSyntax_BigEndianExplicit;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess1TransferSyntax:
+#  else
+      case EXS_JPEGProcess1:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess1;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess2_4TransferSyntax:
+#  else
+      case EXS_JPEGProcess2_4:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess2_4;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess3_5TransferSyntax:
+#  else
+      case EXS_JPEGProcess3_5:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess3_5;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess6_8TransferSyntax:
+#  else
+      case EXS_JPEGProcess6_8:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess6_8;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess7_9TransferSyntax:
+#  else
+      case EXS_JPEGProcess7_9:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess7_9;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess10_12TransferSyntax:
+#  else
+      case EXS_JPEGProcess10_12:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess10_12;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess11_13TransferSyntax:
+#  else
+      case EXS_JPEGProcess11_13:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess11_13;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess14TransferSyntax:
+#  else
+      case EXS_JPEGProcess14:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess14;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess15TransferSyntax:
+#  else
+      case EXS_JPEGProcess15:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess15;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess16_18TransferSyntax:
+#  else
+      case EXS_JPEGProcess16_18:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess16_18;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess17_19TransferSyntax:
+#  else
+      case EXS_JPEGProcess17_19:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess17_19;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess20_22TransferSyntax:
+#  else
+      case EXS_JPEGProcess20_22:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess20_22;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess21_23TransferSyntax:
+#  else
+      case EXS_JPEGProcess21_23:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess21_23;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess24_26TransferSyntax:
+#  else
+      case EXS_JPEGProcess24_26:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess24_26;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess25_27TransferSyntax:
+#  else
+      case EXS_JPEGProcess25_27:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess25_27;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess28TransferSyntax:
+#  else
+      case EXS_JPEGProcess28:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess28;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess29TransferSyntax:
+#  else
+      case EXS_JPEGProcess29:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess29;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess14SV1TransferSyntax:
+#  else
+      case EXS_JPEGProcess14SV1:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess14SV1;
+        return true;
+
+      case EXS_JPEGLSLossless:
+        target = DicomTransferSyntax_JPEGLSLossless;
+        return true;
+
+      case EXS_JPEGLSLossy:
+        target = DicomTransferSyntax_JPEGLSLossy;
+        return true;
+
+      case EXS_JPEG2000LosslessOnly:
+        target = DicomTransferSyntax_JPEG2000LosslessOnly;
+        return true;
+
+      case EXS_JPEG2000:
+        target = DicomTransferSyntax_JPEG2000;
+        return true;
+
+      case EXS_JPEG2000MulticomponentLosslessOnly:
+        target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly;
+        return true;
+
+      case EXS_JPEG2000Multicomponent:
+        target = DicomTransferSyntax_JPEG2000Multicomponent;
+        return true;
+
+      case EXS_JPIPReferenced:
+        target = DicomTransferSyntax_JPIPReferenced;
+        return true;
+
+      case EXS_JPIPReferencedDeflate:
+        target = DicomTransferSyntax_JPIPReferencedDeflate;
+        return true;
+
+      case EXS_MPEG2MainProfileAtMainLevel:
+        target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel;
+        return true;
+
+      case EXS_MPEG2MainProfileAtHighLevel:
+        target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel;
+        return true;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_1:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4BDcompatibleHighProfileLevel4_1:
+        target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_2_For2DVideo:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_2_For3DVideo:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4StereoHighProfileLevel4_2:
+        target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EXS_HEVCMainProfileLevel5_1:
+        target = DicomTransferSyntax_HEVCMainProfileLevel5_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EXS_HEVCMain10ProfileLevel5_1:
+        target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1;
+        return true;
+#endif
+
+      case EXS_RLELossless:
+        target = DicomTransferSyntax_RLELossless;
+        return true;
+
+      default:
+        return false;
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp
new file mode 100644
index 0000000..12d0f1a
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp
@@ -0,0 +1,429 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "IDicomTranscoder.h"
+
+#include "../OrthancException.h"
+#include "FromDcmtkBridge.h"
+#include "ParsedDicomFile.h"
+
+#include 
+#include 
+
+namespace Orthanc
+{
+  IDicomTranscoder::TranscodingType IDicomTranscoder::GetTranscodingType(DicomTransferSyntax target,
+                                                                         DicomTransferSyntax source)
+  {
+    if (target == source)
+    {
+      return TranscodingType_Lossless;
+    }
+    else if (target == DicomTransferSyntax_LittleEndianImplicit ||
+             target == DicomTransferSyntax_LittleEndianExplicit ||
+             target == DicomTransferSyntax_BigEndianExplicit ||
+             target == DicomTransferSyntax_DeflatedLittleEndianExplicit ||
+             target == DicomTransferSyntax_JPEGProcess14 ||
+             target == DicomTransferSyntax_JPEGProcess14SV1 ||
+             target == DicomTransferSyntax_JPEGLSLossless ||
+             target == DicomTransferSyntax_JPEG2000LosslessOnly ||
+             target == DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly)
+    {
+      return TranscodingType_Lossless;
+    }
+    else if (target == DicomTransferSyntax_JPEGProcess1 ||
+             target == DicomTransferSyntax_JPEGProcess2_4 ||
+             target == DicomTransferSyntax_JPEGLSLossy ||
+             target == DicomTransferSyntax_JPEG2000 ||
+             target == DicomTransferSyntax_JPEG2000Multicomponent)
+    {
+      return TranscodingType_Lossy;
+    }
+    else
+    {
+      return TranscodingType_Unknown;
+    }
+  }
+
+
+  std::string IDicomTranscoder::GetSopInstanceUid(DcmFileFormat& dicom)
+  {
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    DcmDataset& dataset = *dicom.getDataset();
+
+    std::string s;
+    if (FromDcmtkBridge::LookupStringValue(s, dataset, DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      return s;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID");
+    }
+  }
+
+
+  void IDicomTranscoder::CheckTranscoding(IDicomTranscoder::DicomImage& transcoded,
+                                          DicomTransferSyntax sourceSyntax,
+                                          const std::string& sourceSopInstanceUid,
+                                          const std::set& allowedSyntaxes,
+                                          bool allowNewSopInstanceUid)
+  {
+    DcmFileFormat& parsed = transcoded.GetParsed();
+    
+    if (parsed.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::string targetSopInstanceUid = GetSopInstanceUid(parsed);
+
+    if (parsed.getDataset()->tagExists(DCM_PixelData))
+    {
+      if (!allowNewSopInstanceUid && (targetSopInstanceUid != sourceSopInstanceUid))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      if (targetSopInstanceUid != sourceSopInstanceUid)
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "No pixel data: Transcoding must not change the SOP instance UID");
+      }
+    }
+
+    DicomTransferSyntax targetSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, parsed))
+    {
+      return;  // Unknown transfer syntax, cannot do further test
+    }
+
+    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
+    {
+      // No transcoding should have happened
+      if (targetSopInstanceUid != sourceSopInstanceUid)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+        
+    if (allowedSyntaxes.find(targetSyntax) == allowedSyntaxes.end())
+    {
+      throw OrthancException(ErrorCode_InternalError, "An incorrect output transfer syntax was chosen");
+    }
+    
+    if (parsed.getDataset()->tagExists(DCM_PixelData))
+    {
+      switch (GetTranscodingType(targetSyntax, sourceSyntax))
+      {
+        case TranscodingType_Lossy:
+          if (targetSopInstanceUid == sourceSopInstanceUid)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        case TranscodingType_Lossless:
+          if (targetSopInstanceUid != sourceSopInstanceUid)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        default:
+          break;
+      }
+    }
+  }
+
+
+  void IDicomTranscoder::DicomImage::Parse()
+  {
+    if (parsed_.get() != NULL)
+    {
+      // Already parsed
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (buffer_.get() != NULL)
+    {
+      if (isExternalBuffer_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(
+                        buffer_->empty() ? NULL : buffer_->c_str(), buffer_->size()));
+        
+        if (parsed_.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }      
+      }
+    }
+    else if (isExternalBuffer_)
+    {
+      parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(externalBuffer_, externalSize_));
+      
+      if (parsed_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }      
+    }
+    else
+    {
+      // No buffer is available
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+  
+  void IDicomTranscoder::DicomImage::Serialize()
+  {
+    if (parsed_.get() == NULL ||
+        buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (parsed_->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      buffer_.reset(new std::string);
+      FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, *parsed_->getDataset());
+    }
+  }
+
+  
+  IDicomTranscoder::DicomImage::DicomImage() :
+    isExternalBuffer_(false),
+    externalBuffer_(NULL),
+    externalSize_(0)
+  {
+  }
+
+
+  void IDicomTranscoder::DicomImage::Clear()
+  {
+    parsed_.reset(NULL);
+    buffer_.reset(NULL);
+    isExternalBuffer_ = false;
+  }
+
+  
+  void IDicomTranscoder::DicomImage::AcquireParsed(ParsedDicomFile& parsed)
+  {
+    AcquireParsed(parsed.ReleaseDcmtkObject());
+  }
+  
+      
+  void IDicomTranscoder::DicomImage::AcquireParsed(DcmFileFormat* parsed)
+  {
+    if (parsed == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (parsed->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else if (parsed_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parsed_.reset(parsed);
+    }
+  }
+  
+
+  void IDicomTranscoder::DicomImage::AcquireParsed(DicomImage& other)
+  {
+    AcquireParsed(other.ReleaseParsed());
+  }
+  
+
+  void IDicomTranscoder::DicomImage::AcquireBuffer(std::string& buffer /* will be swapped */)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      buffer_.reset(new std::string);
+      buffer_->swap(buffer);
+    }
+  }
+
+
+  void IDicomTranscoder::DicomImage::AcquireBuffer(DicomImage& other)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (other.isExternalBuffer_)
+    {
+      assert(other.buffer_.get() == NULL);
+      isExternalBuffer_ = true;
+      externalBuffer_ = other.externalBuffer_;
+      externalSize_ = other.externalSize_;
+    }
+    else if (other.buffer_.get() != NULL)
+    {
+      buffer_.reset(other.buffer_.release());
+    }
+    else
+    {
+      buffer_.reset(NULL);
+    }    
+  }
+
+  
+  void IDicomTranscoder::DicomImage::SetExternalBuffer(const void* buffer,
+                                                       size_t size)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isExternalBuffer_ = true;
+      externalBuffer_ = buffer;
+      externalSize_ = size;
+    }
+  }
+
+
+  void IDicomTranscoder::DicomImage::SetExternalBuffer(const std::string& buffer)
+  {
+    SetExternalBuffer(buffer.empty() ? NULL : buffer.c_str(), buffer.size());
+  }
+
+
+  DcmFileFormat& IDicomTranscoder::DicomImage::GetParsed()
+  {
+    if (parsed_.get() != NULL)
+    {
+      return *parsed_;
+    }
+    else if (buffer_.get() != NULL ||
+             isExternalBuffer_)
+    {
+      Parse();
+      return *parsed_;
+    }
+    else
+    {
+      throw OrthancException(
+        ErrorCode_BadSequenceOfCalls,
+        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
+    }
+  }
+  
+
+  DcmFileFormat* IDicomTranscoder::DicomImage::ReleaseParsed()
+  {
+    if (parsed_.get() != NULL)
+    {
+      buffer_.reset(NULL);
+      return parsed_.release();
+    }
+    else if (buffer_.get() != NULL ||
+             isExternalBuffer_)
+    {
+      Parse();
+      buffer_.reset(NULL);
+      return parsed_.release();
+    }
+    else
+    {
+      throw OrthancException(
+        ErrorCode_BadSequenceOfCalls,
+        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
+    }
+  }
+
+
+  ParsedDicomFile* IDicomTranscoder::DicomImage::ReleaseAsParsedDicomFile()
+  {
+    return ParsedDicomFile::AcquireDcmtkObject(ReleaseParsed());
+  }
+
+  
+  const void* IDicomTranscoder::DicomImage::GetBufferData()
+  {
+    if (isExternalBuffer_)
+    {
+      assert(buffer_.get() == NULL);
+      return externalBuffer_;
+    }
+    else
+    {    
+      if (buffer_.get() == NULL)
+      {
+        Serialize();
+      }
+
+      assert(buffer_.get() != NULL);
+      return buffer_->empty() ? NULL : buffer_->c_str();
+    }
+  }
+
+  
+  size_t IDicomTranscoder::DicomImage::GetBufferSize()
+  {
+    if (isExternalBuffer_)
+    {
+      assert(buffer_.get() == NULL);
+      return externalSize_;
+    }
+    else
+    {    
+      if (buffer_.get() == NULL)
+      {
+        Serialize();
+      }
+
+      assert(buffer_.get() != NULL);
+      return buffer_->size();
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h
new file mode 100644
index 0000000..5f8f74a
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h
@@ -0,0 +1,127 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Compatibility.h"
+#include "../Enumerations.h"
+
+#include 
+#include 
+
+class DcmFileFormat;
+
+namespace Orthanc
+{
+  class ParsedDicomFile;
+  
+  /**
+   * WARNING: This class might be called from several threads at
+   * once. Make sure to implement proper locking.
+   **/
+  class ORTHANC_PUBLIC IDicomTranscoder : public boost::noncopyable
+  {
+  public:
+    class ORTHANC_PUBLIC DicomImage : public boost::noncopyable
+    {
+    private:
+      std::unique_ptr  parsed_;
+      std::unique_ptr    buffer_;
+      bool                            isExternalBuffer_;
+      const void*                     externalBuffer_;
+      size_t                          externalSize_;
+
+      void Parse();
+
+      void Serialize();
+
+      DcmFileFormat* ReleaseParsed();
+
+    public:
+      DicomImage();
+      
+      void Clear();
+      
+      // Calling this method will invalidate the "ParsedDicomFile" object
+      void AcquireParsed(ParsedDicomFile& parsed);
+      
+      void AcquireParsed(DcmFileFormat* parsed);
+
+      void AcquireParsed(DicomImage& other);
+
+      void AcquireBuffer(std::string& buffer /* will be swapped */);
+
+      void AcquireBuffer(DicomImage& other);
+
+      void SetExternalBuffer(const void* buffer,
+                             size_t size);
+
+      void SetExternalBuffer(const std::string& buffer);
+
+      DcmFileFormat& GetParsed();
+
+      ParsedDicomFile* ReleaseAsParsedDicomFile();
+
+      const void* GetBufferData();
+
+      size_t GetBufferSize();
+    };
+
+
+  protected:
+    enum TranscodingType
+    {
+      TranscodingType_Lossy,
+      TranscodingType_Lossless,
+      TranscodingType_Unknown
+    };
+
+    static TranscodingType GetTranscodingType(DicomTransferSyntax target,
+                                              DicomTransferSyntax source);
+
+    static void CheckTranscoding(DicomImage& transcoded,
+                                 DicomTransferSyntax sourceSyntax,
+                                 const std::string& sourceSopInstanceUid,
+                                 const std::set& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid);
+    
+  public:    
+    virtual ~IDicomTranscoder()
+    {
+    }
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) = 0;
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) = 0;
+
+    static std::string GetSopInstanceUid(DcmFileFormat& dicom);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ITagVisitor.h b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h
new file mode 100644
index 0000000..5d83045
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h
@@ -0,0 +1,97 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../DicomFormat/DicomTag.h"
+
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ITagVisitor : public boost::noncopyable
+  {
+  public:
+    enum Action
+    {
+      Action_Replace,
+      Action_Remove,  // New in Orthanc 1.9.5
+      Action_None
+    };
+
+    virtual ~ITagVisitor()
+    {
+    }
+
+    // Visiting a DICOM element that is internal to DCMTK. Can return
+    // "Remove" or "None".
+    virtual Action VisitNotSupported(const std::vector& parentTags,
+                                     const std::vector& parentIndexes,
+                                     const DicomTag& tag,
+                                     ValueRepresentation vr) = 0;
+
+    // SQ - can return "Remove" or "None"
+    virtual Action VisitSequence(const std::vector& parentTags,
+                                 const std::vector& parentIndexes,
+                                 const DicomTag& tag,
+                                 size_t countItems) = 0;
+
+    // SL, SS, UL, US - can return "Remove" or "None"
+    virtual Action VisitIntegers(const std::vector& parentTags,
+                                 const std::vector& parentIndexes,
+                                 const DicomTag& tag,
+                                 ValueRepresentation vr,
+                                 const std::vector& values) = 0;
+
+    // FL, FD, OD, OF - can return "Remove" or "None"
+    virtual Action VisitDoubles(const std::vector& parentTags,
+                                const std::vector& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr,
+                                const std::vector& values) = 0;
+
+    // AT - can return "Remove" or "None"
+    virtual Action VisitAttributes(const std::vector& parentTags,
+                                   const std::vector& parentIndexes,
+                                   const DicomTag& tag,
+                                   const std::vector& values) = 0;
+
+    // OB, OL, OW, UN - can return "Remove" or "None"
+    virtual Action VisitBinary(const std::vector& parentTags,
+                               const std::vector& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const void* data,
+                               size_t size) = 0;
+
+    // Visiting an UTF-8 string - can return "Replace", "Remove" or "None"
+    virtual Action VisitString(std::string& newValue,
+                               const std::vector& parentTags,
+                               const std::vector& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::string& value) = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp
new file mode 100644
index 0000000..3360598
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp
@@ -0,0 +1,418 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "DicomFrameIndex.h"
+
+#include "../../OrthancException.h"
+#include "../../DicomFormat/DicomImageInformation.h"
+#include "../FromDcmtkBridge.h"
+#include "../../Endianness.h"
+#include "DicomImageDecoder.h"
+
+#include 
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class DicomFrameIndex::FragmentIndex : public DicomFrameIndex::IIndex
+  {
+  private:
+    DcmPixelSequence*           pixelSequence_;
+    std::vector  startFragment_;
+    std::vector   countFragments_;
+    std::vector   frameSize_;
+
+    void GetOffsetTable(std::vector& table)
+    {
+      DcmPixelItem* item = NULL;
+      if (!pixelSequence_->getItem(item, 0).good() ||
+          item == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      uint32_t length = item->getLength();
+      if (length == 0)
+      {
+        // Degenerate case: Empty offset table means only one frame
+        // that overlaps all the fragments
+        table.resize(1);
+        table[0] = 0;
+        return;
+      }
+
+      if (length % 4 != 0)
+      {
+        // Error: Each fragment is index with 4 bytes (uint32_t)
+        throw OrthancException(ErrorCode_BadFileFormat);        
+      }
+
+      uint8_t* content = NULL;
+      if (!item->getUint8Array(content).good() ||
+          content == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      table.resize(length / 4);
+
+      // The offset table is always in little endian in the DICOM
+      // file. Swap it to host endianness if needed.
+      const uint32_t* offset = reinterpret_cast(content);
+      for (size_t i = 0; i < table.size(); i++, offset++)
+      {
+        table[i] = le32toh(*offset);
+      }
+    }
+
+
+  public:
+    FragmentIndex(DcmPixelSequence* pixelSequence,
+                  unsigned int countFrames) :
+      pixelSequence_(pixelSequence)
+    {
+      assert(pixelSequence != NULL);
+
+      startFragment_.resize(countFrames);
+      countFragments_.resize(countFrames);
+      frameSize_.resize(countFrames);
+
+      // The first fragment corresponds to the offset table
+      unsigned int countFragments = static_cast(pixelSequence_->card());
+      if (countFragments < countFrames + 1)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      if (countFragments == countFrames + 1)
+      {
+        // Simple case: There is one fragment per frame.
+
+        DcmObject* fragment = pixelSequence_->nextInContainer(NULL);  // Skip the offset table
+        if (fragment == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        for (unsigned int i = 0; i < countFrames; i++)
+        {
+          fragment = pixelSequence_->nextInContainer(fragment);
+          startFragment_[i] = dynamic_cast(fragment);
+          frameSize_[i] = fragment->getLength();
+          countFragments_[i] = 1;
+        }
+
+        return;
+      }
+
+      // Parse the offset table
+      std::vector offsetOfFrame;
+      GetOffsetTable(offsetOfFrame);
+      
+      if (offsetOfFrame.size() != countFrames ||
+          offsetOfFrame[0] != 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      // Loop over the fragments (ignoring the offset table). This is
+      // an alternative, faster implementation to DCMTK's
+      // "DcmCodec::determineStartFragment()".
+      DcmObject* fragment = pixelSequence_->nextInContainer(NULL);
+      if (fragment == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      fragment = pixelSequence_->nextInContainer(fragment); // Skip the offset table
+      if (fragment == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      uint32_t offset = 0;
+      unsigned int currentFrame = 0;
+      startFragment_[0] = dynamic_cast(fragment);
+
+      unsigned int currentFragment = 1;
+      while (fragment != NULL)
+      {
+        if (currentFrame + 1 < countFrames &&
+            offset == offsetOfFrame[currentFrame + 1])
+        {
+          currentFrame += 1;
+          startFragment_[currentFrame] = dynamic_cast(fragment);
+        }
+
+        frameSize_[currentFrame] += fragment->getLength();
+        countFragments_[currentFrame]++;
+
+        // 8 bytes = overhead for the item tag and length field
+        offset += fragment->getLength() + 8;
+
+        currentFragment++;
+        fragment = pixelSequence_->nextInContainer(fragment);
+      }
+
+      if (currentFragment != countFragments ||
+          currentFrame + 1 != countFrames ||
+          fragment != NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      assert(startFragment_.size() == countFragments_.size() &&
+             startFragment_.size() == frameSize_.size());
+    }
+
+
+    virtual void GetRawFrame(std::string& frame,
+                             unsigned int index) const ORTHANC_OVERRIDE
+    {
+      if (index >= startFragment_.size())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      frame.resize(frameSize_[index]);
+      if (frame.size() == 0)
+      {
+        return;
+      }
+
+      uint8_t* target = reinterpret_cast(&frame[0]);
+
+      size_t offset = 0;
+      DcmPixelItem* fragment = startFragment_[index];
+      for (unsigned int i = 0; i < countFragments_[index]; i++)
+      {
+        uint8_t* content = NULL;
+        if (!fragment->getUint8Array(content).good() ||
+            content == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        assert(offset + fragment->getLength() <= frame.size());
+
+        memcpy(target + offset, content, fragment->getLength());
+        offset += fragment->getLength();
+
+        fragment = dynamic_cast(pixelSequence_->nextInContainer(fragment));
+      }
+    }
+  };
+
+
+
+  class DicomFrameIndex::UncompressedIndex : public DicomFrameIndex::IIndex
+  {
+  private:
+    uint8_t*  pixelData_;
+    size_t    frameSize_;
+
+  public: 
+    UncompressedIndex(DcmDataset& dataset,
+                      unsigned int countFrames,
+                      size_t frameSize) :
+      pixelData_(NULL),
+      frameSize_(frameSize)
+    {
+      size_t size = 0;
+
+      DcmElement* e;
+      if (dataset.findAndGetElement(DCM_PixelData, e).good() &&
+          e != NULL)
+      {
+        size = e->getLength();
+
+        if (size > 0)
+        {
+          pixelData_ = NULL;
+          if (!e->getUint8Array(pixelData_).good() ||
+              pixelData_ == NULL)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat);
+          }
+        }
+      }
+
+      if (size < frameSize_ * countFrames)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    virtual void GetRawFrame(std::string& frame,
+                             unsigned int index) const ORTHANC_OVERRIDE
+    {
+      frame.resize(frameSize_);
+      if (frameSize_ > 0)
+      {
+        memcpy(&frame[0], pixelData_ + index * frameSize_, frameSize_);
+      }
+    }
+  };
+
+
+  class DicomFrameIndex::PsmctRle1Index : public DicomFrameIndex::IIndex
+  {
+  private:
+    std::string  pixelData_;
+    size_t       frameSize_;
+
+  public: 
+    PsmctRle1Index(DcmDataset& dataset,
+                   unsigned int countFrames,
+                   size_t frameSize) :
+      frameSize_(frameSize)
+    {
+      if (!DicomImageDecoder::DecodePsmctRle1(pixelData_, dataset) ||
+          pixelData_.size() < frameSize * countFrames)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    virtual void GetRawFrame(std::string& frame,
+                             unsigned int index) const ORTHANC_OVERRIDE
+    {
+      frame.resize(frameSize_);
+      if (frameSize_ > 0)
+      {
+        memcpy(&frame[0], reinterpret_cast(&pixelData_[0]) + index * frameSize_, frameSize_);
+      }
+    }
+  };
+
+
+  unsigned int DicomFrameIndex::GetFramesCount(DcmDataset& dicom)
+  {
+    DicomTransferSyntax transferSyntax;
+    if (FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom) &&
+        (transferSyntax == DicomTransferSyntax_MPEG2MainProfileAtMainLevel ||
+         transferSyntax == DicomTransferSyntax_MPEG2MainProfileAtHighLevel ||
+         transferSyntax == DicomTransferSyntax_MPEG4HighProfileLevel4_1 ||
+         transferSyntax == DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1 ||
+         transferSyntax == DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo ||
+         transferSyntax == DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo ||
+         transferSyntax == DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2 ||
+         transferSyntax == DicomTransferSyntax_HEVCMainProfileLevel5_1 ||
+         transferSyntax == DicomTransferSyntax_HEVCMain10ProfileLevel5_1))
+    {
+      /**
+       * Fixes an issue that was present from Orthanc 1.6.0 until
+       * 1.8.0 for the special case of the videos: In a video, the
+       * number of frames doesn't correspond to the number of
+       * fragments. We consider that there is one single frame (the
+       * video itself).
+       **/
+      return 1;
+    }            
+
+    const char* tmp = NULL;
+    if (!dicom.findAndGetString(DCM_NumberOfFrames, tmp).good() ||
+        tmp == NULL)
+    {
+      return 1;
+    }
+
+    int count = -1;
+    try
+    {
+      count = boost::lexical_cast(tmp);
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+    }
+
+    if (count < 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);        
+    }
+    else
+    {
+      return static_cast(count);
+    }
+  }
+
+
+  DicomFrameIndex::DicomFrameIndex(DcmDataset& dicom)
+  {
+    countFrames_ = GetFramesCount(dicom);
+    if (countFrames_ == 0)
+    {
+      // The image has no frame. No index is to be built.
+      return;
+    }
+
+    // Extract information about the image structure
+    DicomMap tags;
+    std::set ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomSummary(tags, dicom, DicomImageInformation::GetUsefulTagLength(), ignoreTagLength);
+
+    DicomImageInformation information(tags);
+
+    // Test whether this image is composed of a sequence of fragments
+    if (dicom.tagExists(DCM_PixelData))
+    {
+      DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dicom);
+      if (pixelSequence != NULL)
+      {
+        index_.reset(new FragmentIndex(pixelSequence, countFrames_));
+      }
+      else
+      {
+        // Access to the raw pixel data
+        index_.reset(new UncompressedIndex(dicom, countFrames_, information.GetFrameSize()));
+      }
+    }
+    else if (DicomImageDecoder::IsPsmctRle1(dicom))
+    {
+      index_.reset(new PsmctRle1Index(dicom, countFrames_, information.GetFrameSize()));
+    }
+  }
+
+
+  void DicomFrameIndex::GetRawFrame(std::string& frame,
+                                    unsigned int index) const
+  {
+    if (index >= countFrames_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else if (index_.get() != NULL)
+    {
+      return index_->GetRawFrame(frame, index);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Cannot access a raw frame");
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h
new file mode 100644
index 0000000..c801eb7
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h
@@ -0,0 +1,73 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../../Compatibility.h"
+#include "../../Enumerations.h"
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class DicomFrameIndex
+  {
+  private:
+    class IIndex : public boost::noncopyable
+    {
+    public:
+      virtual ~IIndex()
+      {
+      }
+
+      virtual void GetRawFrame(std::string& frame,
+                               unsigned int index) const = 0;
+    };
+
+    class FragmentIndex;
+    class UncompressedIndex;
+    class PsmctRle1Index;
+
+    std::unique_ptr  index_;
+    unsigned int             countFrames_;
+
+  public:
+    explicit DicomFrameIndex(DcmDataset& dicom);
+
+    unsigned int GetFramesCount() const
+    {
+      return countFrames_;
+    }
+
+    void GetRawFrame(std::string& frame,
+                     unsigned int index) const;
+
+    static unsigned int GetFramesCount(DcmDataset& dicom);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp
new file mode 100644
index 0000000..0ad9ca0
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp
@@ -0,0 +1,1174 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "DicomImageDecoder.h"
+
+#include "../ParsedDicomFile.h"
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project
+  (cf. function "DecodePsmctRle1()"):
+
+  Program: GDCM (Grassroots DICOM). A DICOM library
+  Module:  http://gdcm.sourceforge.net/Copyright.html
+
+  Copyright (c) 2006-2011 Mathieu Malaterre
+  Copyright (c) 1993-2005 CREATIS
+  (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image)
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+  * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any
+  contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to
+  endorse or promote products derived from this software without specific
+  prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+#include "../../Images/Image.h"
+#include "../../Images/ImageProcessing.h"
+#include "../../DicomFormat/DicomIntegerPixelAccessor.h"
+#include "../ToDcmtkBridge.h"
+#include "../FromDcmtkBridge.h"
+
+#if ORTHANC_ENABLE_PNG == 1
+#  include "../../Images/PngWriter.h"
+#endif
+
+#if ORTHANC_ENABLE_JPEG == 1
+#  include "../../Images/JpegWriter.h"
+#endif
+#include "../../Images/PamWriter.h"
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#  include 
+#endif
+
+#if DCMTK_VERSION_NUMBER <= 360
+#  define EXS_JPEGProcess1      EXS_JPEGProcess1TransferSyntax
+#  define EXS_JPEGProcess2_4    EXS_JPEGProcess2_4TransferSyntax
+#  define EXS_JPEGProcess6_8    EXS_JPEGProcess6_8TransferSyntax
+#  define EXS_JPEGProcess10_12  EXS_JPEGProcess10_12TransferSyntax
+#  define EXS_JPEGProcess14     EXS_JPEGProcess14TransferSyntax
+#  define EXS_JPEGProcess14SV1  EXS_JPEGProcess14SV1TransferSyntax
+#endif
+
+namespace Orthanc
+{
+  static const Endianness ENDIANNESS = Toolbox::DetectEndianness();
+  static const DicomTag DICOM_TAG_CONTENT(0x07a1, 0x100a);
+  static const DicomTag DICOM_TAG_COMPRESSION_TYPE(0x07a1, 0x1011);
+
+
+  bool DicomImageDecoder::IsPsmctRle1(DcmDataset& dataset)
+  {
+    DcmElement* e;
+    char* c;
+
+    // Check whether the DICOM instance contains an image encoded with
+    // the PMSCT_RLE1 scheme.
+    if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_COMPRESSION_TYPE), e).good() ||
+        !dataset.tagExistsWithValue(ToDcmtkBridge::Convert(DICOM_TAG_CONTENT)) ||  // New in Orthanc 1.7.4
+        e == NULL ||
+        !e->isaString() ||
+        !e->getString(c).good() ||
+        c == NULL ||
+        strcmp("PMSCT_RLE1", c))
+    {
+      return false;
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+
+  bool DicomImageDecoder::DecodePsmctRle1(std::string& output,
+                                          DcmDataset& dataset)
+  {
+    // Check whether the DICOM instance contains an image encoded with
+    // the PMSCT_RLE1 scheme.
+    if (!IsPsmctRle1(dataset))
+    {
+      return false;
+    }
+
+    // OK, this is a custom RLE encoding from Philips. Get the pixel
+    // data from the appropriate private DICOM tag.
+    Uint8* pixData = NULL;
+    DcmElement* e;
+    if (!dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_CONTENT), e).good() ||
+        e == NULL ||
+        e->getUint8Array(pixData) != EC_Normal)
+    {
+      return false;
+    }    
+
+    // The "unsigned" below IS VERY IMPORTANT
+    const uint8_t* inbuffer = reinterpret_cast(pixData);
+    const size_t length = e->getLength();
+
+    /**
+     * The code below is an adaptation of a sample code for GDCM by
+     * Mathieu Malaterre (under a BSD license).
+     * http://gdcm.sourceforge.net/html/rle2img_8cxx-example.html
+     **/
+
+    // RLE pass
+    std::vector temp;
+    temp.reserve(length);
+    for (size_t i = 0; i < length; i++)
+    {
+      if (inbuffer[i] == 0xa5)
+      {
+        temp.push_back(inbuffer[i+2]);
+        for (uint8_t repeat = inbuffer[i + 1]; repeat != 0; repeat--)
+        {
+          temp.push_back(inbuffer[i+2]);
+        }
+        i += 2;
+      }
+      else
+      {
+        temp.push_back(inbuffer[i]);
+      }
+    }
+
+    // Delta encoding pass
+    uint16_t delta = 0;
+    output.clear();
+    output.reserve(temp.size());
+    for (size_t i = 0; i < temp.size(); i++)
+    {
+      uint16_t value;
+
+      if (temp[i] == 0x5a)
+      {
+        uint16_t v1 = temp[i + 1];
+        uint16_t v2 = temp[i + 2];
+        value = (v2 << 8) + v1;
+        i += 2;
+      }
+      else
+      {
+        value = delta + (int8_t) temp[i];
+      }
+
+      output.push_back(value & 0xff);
+      output.push_back(value >> 8);
+      delta = value;
+    }
+
+    if (output.size() % 2)
+    {
+      output.resize(output.size() - 1);
+    }
+
+    return true;
+  }
+
+
+  class DicomImageDecoder::ImageSource
+  {
+  private:
+    std::string psmct_;
+    std::unique_ptr slowAccessor_;
+
+  public:
+    void Setup(DcmDataset& dataset,
+               unsigned int frame)
+    {
+      psmct_.clear();
+      slowAccessor_.reset(NULL);
+
+      // See also: http://support.dcmtk.org/wiki/dcmtk/howto/accessing-compressed-data
+
+      DicomMap m;
+      std::set ignoreTagLength;
+      FromDcmtkBridge::ExtractDicomSummary(m, dataset, DicomImageInformation::GetUsefulTagLength(), ignoreTagLength);
+
+      /**
+       * Create an accessor to the raw values of the DICOM image.
+       **/
+
+      DcmElement* e;
+      if (dataset.findAndGetElement(ToDcmtkBridge::Convert(DICOM_TAG_PIXEL_DATA), e).good() &&
+          e != NULL)
+      {
+        Uint8* pixData = NULL;
+        if (e->getUint8Array(pixData) == EC_Normal)
+        {    
+          slowAccessor_.reset(new DicomIntegerPixelAccessor(m, pixData, e->getLength()));
+        }
+      }
+      else if (DecodePsmctRle1(psmct_, dataset))
+      {
+        LOG(INFO) << "The PMSCT_RLE1 decoding has succeeded";
+        Uint8* pixData = NULL;
+        if (psmct_.size() > 0)
+        {
+          pixData = reinterpret_cast(&psmct_[0]);
+        }
+
+        slowAccessor_.reset(new DicomIntegerPixelAccessor(m, pixData, psmct_.size()));
+      }
+    
+      if (slowAccessor_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      slowAccessor_->SetCurrentFrame(frame);
+    }
+
+    unsigned int GetWidth() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetInformation().GetWidth();
+    }
+
+    unsigned int GetHeight() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetInformation().GetHeight();
+    }
+
+    unsigned int GetChannelCount() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetInformation().GetChannelCount();
+    }
+
+    const DicomIntegerPixelAccessor& GetAccessor() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return *slowAccessor_;
+    }
+
+    unsigned int GetSize() const
+    {
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetSize();
+    }
+  };
+
+
+  ImageAccessor* DicomImageDecoder::CreateImage(DcmDataset& dataset,
+                                                bool ignorePhotometricInterpretation)
+  {
+    DicomMap m;
+    std::set ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomSummary(m, dataset, DicomImageInformation::GetUsefulTagLength(), ignoreTagLength);
+
+    DicomImageInformation info(m);
+    PixelFormat format;
+    
+    if (!info.ExtractPixelFormat(format, ignorePhotometricInterpretation))
+    {
+      LOG(WARNING) << "Unsupported DICOM image: " << info.GetBitsStored() 
+                   << "bpp, " << info.GetChannelCount() << " channels, " 
+                   << (info.IsSigned() ? "signed" : "unsigned")
+                   << (info.IsPlanar() ? ", planar, " : ", non-planar, ")
+                   << EnumerationToString(info.GetPhotometricInterpretation())
+                   << " photometric interpretation";
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    return new Image(format, info.GetWidth(), info.GetHeight(), false);
+  }
+
+
+  template 
+  static void CopyPixels(ImageAccessor& target,
+                         const DicomIntegerPixelAccessor& source)
+  {
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    const PixelType minValue = std::numeric_limits::min();
+    const PixelType maxValue = std::numeric_limits::max();
+
+    const unsigned int height = source.GetInformation().GetHeight();
+    const unsigned int width = source.GetInformation().GetWidth();
+    const unsigned int channels = source.GetInformation().GetChannelCount();
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      PixelType* pixel = reinterpret_cast(target.GetRow(y));
+      for (unsigned int x = 0; x < width; x++)
+      {
+        for (unsigned int c = 0; c < channels; c++, pixel++)
+        {
+          int32_t v = source.GetValue(x, y, c);
+          if (v < static_cast(minValue))
+          {
+            *pixel = minValue;
+          }
+          else if (v > static_cast(maxValue))
+          {
+            *pixel = maxValue;
+          }
+          else
+          {
+            *pixel = static_cast(v);
+          }
+        }
+      }
+    }
+  }
+
+
+  static ImageAccessor* DecodeLookupTable(std::unique_ptr& target,
+                                          const DicomImageInformation& info,
+                                          DcmDataset& dataset,
+                                          const uint8_t* pixelData,
+                                          unsigned long pixelLength)
+  {
+    LOG(INFO) << "Decoding a lookup table";
+
+    OFString r, g, b;
+    PixelFormat format;
+    const uint16_t* lutRed = NULL;
+    const uint16_t* lutGreen = NULL;
+    const uint16_t* lutBlue = NULL;
+    unsigned long rc = 0;
+    unsigned long gc = 0;
+    unsigned long bc = 0;
+
+    if (pixelData == NULL &&
+        !dataset.findAndGetUint8Array(DCM_PixelData, pixelData, &pixelLength).good())
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    if (info.IsPlanar() ||
+        info.GetNumberOfFrames() != 1 ||
+        !info.ExtractPixelFormat(format, false) ||
+        !dataset.findAndGetOFStringArray(DCM_BluePaletteColorLookupTableDescriptor, b).good() ||
+        !dataset.findAndGetOFStringArray(DCM_GreenPaletteColorLookupTableDescriptor, g).good() ||
+        !dataset.findAndGetOFStringArray(DCM_RedPaletteColorLookupTableDescriptor, r).good() ||
+        !dataset.findAndGetUint16Array(DCM_BluePaletteColorLookupTableData, lutBlue, &bc).good() ||
+        !dataset.findAndGetUint16Array(DCM_GreenPaletteColorLookupTableData, lutGreen, &gc).good() ||
+        !dataset.findAndGetUint16Array(DCM_RedPaletteColorLookupTableData, lutRed, &rc).good() ||
+        r != g ||
+        r != b ||
+        g != b ||
+        lutRed == NULL ||
+        lutGreen == NULL ||
+        lutBlue == NULL ||
+        pixelData == NULL)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    switch (format)
+    {
+      case PixelFormat_RGB24:
+      {
+        if (r != "256\\0\\16" ||
+            rc != 256 ||
+            gc != 256 ||
+            bc != 256)
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        if (pixelLength != target->GetWidth() * target->GetHeight())
+        {
+          DcmElement *elem;
+          Uint16 bitsAllocated = 0;
+
+          if (!dataset.findAndGetUint16(DCM_BitsAllocated, bitsAllocated).good())
+          {
+            throw OrthancException(ErrorCode_NotImplemented);  
+          }
+
+          if (!dataset.findAndGetElement(DCM_PixelData, elem).good())
+          {
+            throw OrthancException(ErrorCode_NotImplemented);  
+          }
+
+          // In implicit VR files, pixelLength is expressed in words (OW) although pixels can actually be 8 bits
+          // -> pixelLength is wrong by a factor of two and the image can still be decoded!
+          // seen in some Philips ClearVue 650 images (using 8 bits LUT)
+          if (!(elem->getVR() == EVR_OW && bitsAllocated == 8 && (2*pixelLength == target->GetWidth() * target->GetHeight())))  
+          {
+            throw OrthancException(ErrorCode_NotImplemented);
+          }
+        }
+
+        const uint8_t* source = reinterpret_cast(pixelData);
+        const unsigned int width = target->GetWidth();
+        const unsigned int height = target->GetHeight();
+        
+        for (unsigned int y = 0; y < height; y++)
+        {
+          uint8_t* p = reinterpret_cast(target->GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++)
+          {
+            p[0] = lutRed[*source] >> 8;
+            p[1] = lutGreen[*source] >> 8;
+            p[2] = lutBlue[*source] >> 8;
+            source++;
+            p += 3;
+          }
+        }
+
+        return target.release();
+      }
+
+      case PixelFormat_RGB48:
+      {
+        if (r != "0\\0\\16" ||
+            rc != 65536 ||
+            gc != 65536 ||
+            bc != 65536 ||
+            pixelLength != 2 * target->GetWidth() * target->GetHeight())
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        const uint16_t* source = reinterpret_cast(pixelData);
+        const unsigned int width = target->GetWidth();
+        const unsigned int height = target->GetHeight();
+        
+        for (unsigned int y = 0; y < height; y++)
+        {
+          uint16_t* p = reinterpret_cast(target->GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++)
+          {
+            p[0] = lutRed[*source];
+            p[1] = lutGreen[*source];
+            p[2] = lutBlue[*source];
+            source++;
+            p += 3;
+          }
+        }
+
+        return target.release();
+      }
+
+      default:
+        break;
+    }
+
+    throw OrthancException(ErrorCode_InternalError);
+  }                                          
+
+
+  ImageAccessor* DicomImageDecoder::DecodeUncompressedImage(DcmDataset& dataset,
+                                                            unsigned int frame)
+  {
+    /**
+     * Create the target image.
+     **/
+
+    std::unique_ptr target(CreateImage(dataset, false));
+
+    ImageSource source;
+    source.Setup(dataset, frame);
+
+    if (source.GetWidth() != target->GetWidth() ||
+        source.GetHeight() != target->GetHeight())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    
+    /**
+     * Deal with lookup tables
+     **/
+
+    const DicomImageInformation& info = source.GetAccessor().GetInformation();
+
+    if (info.GetPhotometricInterpretation() == PhotometricInterpretation_Palette)
+    {
+      return DecodeLookupTable(target, info, dataset, NULL, 0);
+    }       
+
+
+    /**
+     * If the format of the DICOM buffer is natively supported, use a
+     * direct access to copy its values.
+     **/
+
+    bool fastVersionSuccess = false;
+    PixelFormat sourceFormat;
+    if (!info.IsPlanar() &&
+        info.GetBitsStored() != 1 &&  // Black-and-white image, notably DICOM SEG (new in Orthanc 1.10.0)
+        info.ExtractPixelFormat(sourceFormat, false))
+    {
+      try
+      {
+        size_t frameSize = info.GetHeight() * info.GetWidth() * GetBytesPerPixel(sourceFormat);
+        if ((frame + 1) * frameSize <= source.GetSize())
+        {
+          const uint8_t* buffer = reinterpret_cast(source.GetAccessor().GetPixelData());
+
+          ImageAccessor sourceImage;
+          sourceImage.AssignReadOnly(sourceFormat, 
+                                     info.GetWidth(), 
+                                     info.GetHeight(),
+                                     info.GetWidth() * GetBytesPerPixel(sourceFormat),
+                                     buffer + frame * frameSize);
+
+          switch (ENDIANNESS)
+          {
+            case Endianness_Little:
+              ImageProcessing::Convert(*target, sourceImage);
+              break;
+
+            case Endianness_Big:
+            {
+              // We cannot do byte swapping directly on the constant DcmDataset
+              std::unique_ptr copy(Image::Clone(sourceImage));
+              ImageProcessing::SwapEndianness(*copy);
+              ImageProcessing::Convert(*target, *copy);
+              break;
+            }
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+            
+          ImageProcessing::ShiftRight(*target, info.GetShift());
+          fastVersionSuccess = true;
+        }
+      }
+      catch (OrthancException&)
+      {
+        // Unsupported conversion, use the slow version
+      }
+    }
+
+    /**
+     * Slow version : loop over the DICOM buffer, storing its value
+     * into the target image.
+     **/
+
+    if (!fastVersionSuccess)
+    {
+      switch (target->GetFormat())
+      {
+        case PixelFormat_RGB24:
+        case PixelFormat_RGBA32:
+        case PixelFormat_Grayscale8:
+          CopyPixels(*target, source.GetAccessor());
+          break;
+        
+        case PixelFormat_Grayscale16:
+          CopyPixels(*target, source.GetAccessor());
+          break;
+
+        case PixelFormat_SignedGrayscale16:
+          CopyPixels(*target, source.GetAccessor());
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    return target.release();
+  }
+
+
+  static ImageAccessor* DecodePlanarConfiguration(const ImageAccessor& source)
+  {
+    /**
+     * This function will interleave the RGB channels, if the source
+     * DICOM image has the "Planar Configuration" (0028,0006) tag that
+     * equals 1. This process was not applied to images using the RLE
+     * codec, which led to the following issue:
+     * https://groups.google.com/g/orthanc-users/c/CSVWfRasSR0/m/y1XDRXVnAgAJ
+     **/
+
+    const unsigned int height = source.GetHeight();
+    const unsigned int width = source.GetWidth();
+    const size_t size = static_cast(height) * static_cast(width);
+
+    if (source.GetFormat() != PixelFormat_RGB24 ||
+        3 * width != source.GetPitch())
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    std::unique_ptr target(new Image(PixelFormat_RGB24, width, height, false));
+
+    const uint8_t* red = reinterpret_cast(source.GetConstBuffer());
+    const uint8_t* green = red + size;
+    const uint8_t* blue = red + 2 * size;
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      uint8_t* interleaved = reinterpret_cast(target->GetRow(y));
+      for (unsigned int x = 0; x < width; x++)
+      {
+        interleaved[0] = *red;
+        interleaved[1] = *green;
+        interleaved[2] = *blue;
+        interleaved += 3;
+        red++;
+        green++;
+        blue++;
+      }
+    }
+
+    return target.release();
+  }
+
+
+  ImageAccessor* DicomImageDecoder::ApplyCodec
+  (const DcmCodec& codec,
+   const DcmCodecParameter& parameters,
+   const DcmRepresentationParameter& representationParameter,
+   DcmDataset& dataset,
+   unsigned int frame)
+  {
+    DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset);
+    if (pixelSequence == NULL)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    DicomMap m;
+    std::set ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomSummary(m, dataset, DicomImageInformation::GetUsefulTagLength(), ignoreTagLength);
+    DicomImageInformation info(m);
+
+    std::unique_ptr target(CreateImage(dataset, true));
+
+    Uint32 startFragment = 0;  // Default 
+    OFString decompressedColorModel;  // Out
+
+    OFCondition c;
+    
+    if (info.GetPhotometricInterpretation() == PhotometricInterpretation_Palette &&
+        info.GetChannelCount() == 1)
+    {
+      std::string uncompressed;
+      uncompressed.resize(info.GetWidth() * info.GetHeight() * info.GetBytesPerValue());
+
+      if (uncompressed.size() == 0 ||
+          !codec.decodeFrame(&representationParameter, 
+                             pixelSequence, ¶meters, 
+                             &dataset, frame, startFragment, &uncompressed[0],
+                             uncompressed.size(), decompressedColorModel).good())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot decode a palette image");
+      }
+
+      return DecodeLookupTable(target, info, dataset,
+                               reinterpret_cast(uncompressed.c_str()),
+                               uncompressed.size());
+    }
+    else
+    {
+      if (!codec.decodeFrame(&representationParameter, 
+                             pixelSequence, ¶meters, 
+                             &dataset, frame, startFragment, target->GetBuffer(), 
+                             target->GetSize(), decompressedColorModel).good())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot decode a non-palette image");
+      }
+
+      std::string colorModel = Orthanc::Toolbox::StripSpaces(decompressedColorModel.c_str());
+
+      if (target->GetFormat() == PixelFormat_RGB24 &&
+          (colorModel == "RGB" || colorModel == "YBR_FULL") &&
+          info.IsPlanar())
+      {
+        std::unique_ptr output(DecodePlanarConfiguration(*target));
+
+        if (colorModel == "YBR_FULL")
+        {
+          ImageProcessing::ConvertJpegYCbCrToRgb(*output);
+        }
+
+        return output.release();
+      }
+      else
+      {
+        return target.release();
+      }
+    }
+  }
+
+
+  static void UndoBigEndianSwapping(ImageAccessor& decoded)
+  {
+    if (ENDIANNESS == Endianness_Big &&
+        decoded.GetFormat() == PixelFormat_Grayscale8)
+    {
+      /**
+       * Undo the call to "swapIfNecessary()" that is done in
+       * "dcmjpeg/libsrc/djcodecd.cc" and "dcmjpls/libsrc/djcodecd.cc"
+       * if "jpeg->bytesPerSample() == 1", presumably because DCMTK
+       * plans for DICOM-to-DICOM conversion
+       **/
+      if (decoded.GetPitch() % 2 == 0)
+      {
+        swapBytes(decoded.GetBuffer(), decoded.GetPitch() * decoded.GetHeight(), sizeof(uint16_t));
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError, "Cannot swap the bytes of an image that has an odd width");
+      }
+    }
+  }
+  
+
+  ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset,
+                                           unsigned int frame)
+  {
+    E_TransferSyntax syntax = dataset.getCurrentXfer();
+
+    /**
+     * Deal with uncompressed, raw images.
+     * http://support.dcmtk.org/docs/dcxfer_8h-source.html
+     **/
+    if (syntax == EXS_Unknown ||
+        syntax == EXS_LittleEndianImplicit ||
+        syntax == EXS_BigEndianImplicit ||
+        syntax == EXS_LittleEndianExplicit ||
+        syntax == EXS_BigEndianExplicit)
+    {
+      return DecodeUncompressedImage(dataset, frame);
+    }
+
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    /**
+     * Deal with JPEG-LS images.
+     **/
+
+    if (syntax == EXS_JPEGLSLossless ||
+        syntax == EXS_JPEGLSLossy)
+    {
+      // The (2, OFTrue) are the default parameters as found in DCMTK 3.6.2
+      // http://support.dcmtk.org/docs/classDJLSRepresentationParameter.html
+      DJLSRepresentationParameter representationParameter(2, OFTrue);
+
+      DJLSCodecParameter parameters;
+      std::unique_ptr decoder;
+
+      switch (syntax)
+      {
+        case EXS_JPEGLSLossless:
+          LOG(INFO) << "Decoding a JPEG-LS lossless DICOM image";
+          decoder.reset(new DJLSLosslessDecoder);
+          break;
+          
+        case EXS_JPEGLSLossy:
+          LOG(INFO) << "Decoding a JPEG-LS near-lossless DICOM image";
+          decoder.reset(new DJLSNearLosslessDecoder);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    
+      std::unique_ptr result(ApplyCodec(*decoder, parameters, representationParameter, dataset, frame));
+      UndoBigEndianSwapping(*result);  // New in Orthanc 1.9.1 to decode on big-endian architectures
+      return result.release();
+    }
+#endif
+
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    /**
+     * Deal with JPEG images.
+     **/
+
+    if (syntax == EXS_JPEGProcess1     ||  // DJDecoderBaseline
+        syntax == EXS_JPEGProcess2_4   ||  // DJDecoderExtended
+        syntax == EXS_JPEGProcess6_8   ||  // DJDecoderSpectralSelection (retired)
+        syntax == EXS_JPEGProcess10_12 ||  // DJDecoderProgressive (retired)
+        syntax == EXS_JPEGProcess14    ||  // DJDecoderLossless
+        syntax == EXS_JPEGProcess14SV1)    // DJDecoderP14SV1
+    {
+      // http://support.dcmtk.org/docs-snapshot/djutils_8h.html#a2a9695e5b6b0f5c45a64c7f072c1eb9d
+      DJCodecParameter parameters(
+        ECC_lossyYCbCr,  // Mode for color conversion for compression, Unused for decompression
+        EDC_photometricInterpretation,  // Perform color space conversion from YCbCr to RGB if DICOM photometric interpretation indicates YCbCr
+        EUC_default,     // Mode for UID creation, unused for decompression
+        EPC_default);    // Automatically determine whether color-by-plane is required from the SOP Class UID and decompressed photometric interpretation
+      DJ_RPLossy representationParameter;
+      std::unique_ptr decoder;
+
+      switch (syntax)
+      {
+        case EXS_JPEGProcess1:
+          LOG(INFO) << "Decoding a JPEG baseline (process 1) DICOM image";
+          decoder.reset(new DJDecoderBaseline);
+          break;
+          
+        case EXS_JPEGProcess2_4 :
+          LOG(INFO) << "Decoding a JPEG baseline (processes 2 and 4) DICOM image";
+          decoder.reset(new DJDecoderExtended);
+          break;
+          
+        case EXS_JPEGProcess6_8:   // Retired
+          LOG(INFO) << "Decoding a JPEG spectral section, nonhierarchical (processes 6 and 8) DICOM image";
+          decoder.reset(new DJDecoderSpectralSelection);
+          break;
+          
+        case EXS_JPEGProcess10_12:   // Retired
+          LOG(INFO) << "Decoding a JPEG full progression, nonhierarchical (processes 10 and 12) DICOM image";
+          decoder.reset(new DJDecoderProgressive);
+          break;
+          
+        case EXS_JPEGProcess14:
+          LOG(INFO) << "Decoding a JPEG lossless, nonhierarchical (process 14) DICOM image";
+          decoder.reset(new DJDecoderLossless);
+          break;
+          
+        case EXS_JPEGProcess14SV1:
+          LOG(INFO) << "Decoding a JPEG lossless, nonhierarchical, first-order prediction (process 14 selection value 1) DICOM image";
+          decoder.reset(new DJDecoderP14SV1);
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      std::unique_ptr result(ApplyCodec(*decoder, parameters, representationParameter, dataset, frame));
+      UndoBigEndianSwapping(*result);  // New in Orthanc 1.9.1 to decode on big-endian architectures
+      return result.release();
+    }
+#endif
+
+
+    if (syntax == EXS_RLELossless)
+    {
+      LOG(INFO) << "Decoding a RLE lossless DICOM image";
+      DcmRLECodecParameter parameters;
+      DcmRLECodecDecoder decoder;
+      DcmRLERepresentationParameter representationParameter;
+      return ApplyCodec(decoder, parameters, representationParameter, dataset, frame);
+    }
+
+
+    /**
+     * This DICOM image format is not natively supported by
+     * Orthanc. As a last resort, try and decode it through DCMTK by
+     * converting its transfer syntax to Little Endian. This will
+     * result in higher memory consumption. This is actually the
+     * second example of the following page:
+     * http://support.dcmtk.org/docs/mod_dcmjpeg.html#Examples
+     **/
+    
+    {
+      LOG(INFO) << "Trying to decode a compressed image by transcoding it to Little Endian Explicit";
+
+      std::unique_ptr converted(dynamic_cast(dataset.clone()));
+      converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
+
+      if (converted->canWriteXfer(EXS_LittleEndianExplicit))
+      {
+        return DecodeUncompressedImage(*converted, frame);
+      }
+    }
+
+    DicomTransferSyntax s;
+    if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, dataset.getCurrentXfer()))
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The built-in DCMTK decoder cannot decode some DICOM instance "
+                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)), false /* don't log here*/);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The built-in DCMTK decoder cannot decode some DICOM instance", false /* don't log here*/);
+    }
+  }
+
+
+  static bool IsColorImage(PixelFormat format)
+  {
+    return (format == PixelFormat_RGB24 ||
+            format == PixelFormat_RGBA32);
+  }
+
+
+  bool DicomImageDecoder::TruncateDecodedImage(std::unique_ptr& image,
+                                               PixelFormat format,
+                                               bool allowColorConversion)
+  {
+    // If specified, prevent the conversion between color and
+    // grayscale images
+    bool isSourceColor = IsColorImage(image->GetFormat());
+    bool isTargetColor = IsColorImage(format);
+
+    if (!allowColorConversion)
+    {
+      if (isSourceColor ^ isTargetColor)
+      {
+        return false;
+      }
+    }
+
+    if (image->GetFormat() != format)
+    {
+      // A conversion is required
+      std::unique_ptr target
+        (new Image(format, image->GetWidth(), image->GetHeight(), false));
+      ImageProcessing::Convert(*target, *image);
+
+#if __cplusplus < 201103L
+      image.reset(target.release());
+#else
+      image = std::move(target);
+#endif
+    }
+
+    return true;
+  }
+
+
+  bool DicomImageDecoder::PreviewDecodedImage(std::unique_ptr& image)
+  {
+    switch (image->GetFormat())
+    {
+      case PixelFormat_RGB24:
+      {
+        // Directly return color images without modification (RGB)
+        return true;
+      }
+
+      case PixelFormat_RGB48:
+      {
+        std::unique_ptr target
+          (new Image(PixelFormat_RGB24, image->GetWidth(), image->GetHeight(), false));
+        ImageProcessing::Convert(*target, *image);
+
+#if __cplusplus < 201103L
+        image.reset(target.release());
+#else
+        image = std::move(target);
+#endif
+
+        return true;
+      }
+
+      case PixelFormat_Grayscale8:
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+      {
+        // Grayscale image: Stretch its dynamics to the [0,255] range
+        int64_t a, b;
+        ImageProcessing::GetMinMaxIntegerValue(a, b, *image);
+
+        if (a == b)
+        {
+          ImageProcessing::Set(*image, 0);
+        }
+        else
+        {
+          ImageProcessing::ShiftScale(*image, static_cast(-a),
+                                      255.0f / static_cast(b - a),
+                                      true /* TODO - Consider using "false" to speed up */);
+        }
+
+        // If the source image is not grayscale 8bpp, convert it
+        if (image->GetFormat() != PixelFormat_Grayscale8)
+        {
+          std::unique_ptr target
+            (new Image(PixelFormat_Grayscale8, image->GetWidth(), image->GetHeight(), false));
+          ImageProcessing::Convert(*target, *image);
+
+#if __cplusplus < 201103L
+          image.reset(target.release());
+#else
+          image = std::move(target);
+#endif
+        }
+
+        return true;
+      }
+      
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void DicomImageDecoder::ApplyExtractionMode(std::unique_ptr& image,
+                                              ImageExtractionMode mode,
+                                              bool invert)
+  {
+    if (image.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    bool ok = false;
+
+    switch (mode)
+    {
+      case ImageExtractionMode_UInt8:
+        ok = TruncateDecodedImage(image, PixelFormat_Grayscale8, false);
+        break;
+
+      case ImageExtractionMode_UInt16:
+        ok = TruncateDecodedImage(image, PixelFormat_Grayscale16, false);
+        break;
+
+      case ImageExtractionMode_Int16:
+        ok = TruncateDecodedImage(image, PixelFormat_SignedGrayscale16, false);
+        break;
+
+      case ImageExtractionMode_Preview:
+        ok = PreviewDecodedImage(image);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (ok)
+    {
+      assert(image.get() != NULL);
+
+      if (invert)
+      {
+        ImageProcessing::Invert(*image);
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void DicomImageDecoder::ExtractPamImage(std::string& result,
+                                          std::unique_ptr& image,
+                                          ImageExtractionMode mode,
+                                          bool invert)
+  {
+    ApplyExtractionMode(image, mode, invert);
+
+    PamWriter writer;
+    IImageWriter::WriteToMemory(writer, result, *image);
+  }
+
+#if ORTHANC_ENABLE_PNG == 1
+  void DicomImageDecoder::ExtractPngImage(std::string& result,
+                                          std::unique_ptr& image,
+                                          ImageExtractionMode mode,
+                                          bool invert)
+  {
+    ApplyExtractionMode(image, mode, invert);
+
+    PngWriter writer;
+    IImageWriter::WriteToMemory(writer, result, *image);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_JPEG == 1
+  void DicomImageDecoder::ExtractJpegImage(std::string& result,
+                                           std::unique_ptr& image,
+                                           ImageExtractionMode mode,
+                                           bool invert,
+                                           uint8_t quality)
+  {
+    if (mode != ImageExtractionMode_UInt8 &&
+        mode != ImageExtractionMode_Preview)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    ApplyExtractionMode(image, mode, invert);
+
+    JpegWriter writer;
+    writer.SetQuality(quality);
+    IImageWriter::WriteToMemory(writer, result, *image);
+  }
+#endif
+
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+  ImageAccessor *DicomImageDecoder::Decode(ParsedDicomFile& dataset,
+                                           unsigned int frame)
+  {
+    return Decode(*dataset.GetDcmtkObject().getDataset(), frame);
+  }
+#endif
+}
diff --git a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h
new file mode 100644
index 0000000..c6902b1
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h
@@ -0,0 +1,124 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../../Compatibility.h"
+#include "../../Images/ImageAccessor.h"
+
+#include 
+
+#if !defined(ORTHANC_ENABLE_JPEG)
+#  error The macro ORTHANC_ENABLE_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_PNG)
+#  error The macro ORTHANC_ENABLE_PNG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS)
+#  error The macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined
+#endif
+
+
+class DcmDataset;
+class DcmCodec;
+class DcmCodecParameter;
+class DcmRepresentationParameter;
+
+namespace Orthanc
+{
+  class ParsedDicomFile;
+  
+  class ORTHANC_PUBLIC DicomImageDecoder : public boost::noncopyable
+  {
+  private:
+    class ImageSource;
+
+    DicomImageDecoder()   // This is a fully abstract class, no constructor
+    {
+    }
+
+    static ImageAccessor* CreateImage(DcmDataset& dataset,
+                                      bool ignorePhotometricInterpretation);
+
+    static ImageAccessor* DecodeUncompressedImage(DcmDataset& dataset,
+                                                  unsigned int frame);
+
+    static ImageAccessor* ApplyCodec(const DcmCodec& codec,
+                                     const DcmCodecParameter& parameters,
+                                     const DcmRepresentationParameter& representationParameter,
+                                     DcmDataset& dataset,
+                                     unsigned int frame);
+
+    static bool TruncateDecodedImage(std::unique_ptr& image,
+                                     PixelFormat format,
+                                     bool allowColorConversion);
+
+    static bool PreviewDecodedImage(std::unique_ptr& image);
+
+    static void ApplyExtractionMode(std::unique_ptr& image,
+                                    ImageExtractionMode mode,
+                                    bool invert);
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
+    static ImageAccessor *Decode(ParsedDicomFile& dataset,
+                                 unsigned int frame);
+#endif
+
+  public:
+    static bool IsPsmctRle1(DcmDataset& dataset);
+
+    static bool DecodePsmctRle1(std::string& output,
+                                DcmDataset& dataset);
+
+    static ImageAccessor *Decode(DcmDataset& dataset,
+                                 unsigned int frame);
+
+    static void ExtractPamImage(std::string& result,
+                                std::unique_ptr& image,
+                                ImageExtractionMode mode,
+                                bool invert);
+
+#if ORTHANC_ENABLE_PNG == 1
+    static void ExtractPngImage(std::string& result,
+                                std::unique_ptr& image,
+                                ImageExtractionMode mode,
+                                bool invert);
+#endif
+
+#if ORTHANC_ENABLE_JPEG == 1
+    static void ExtractJpegImage(std::string& result,
+                                 std::unique_ptr& image,
+                                 ImageExtractionMode mode,
+                                 bool invert,
+                                 uint8_t quality);
+#endif
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp
new file mode 100644
index 0000000..f8b19c7
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp
@@ -0,0 +1,108 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryBufferTranscoder.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "FromDcmtkBridge.h"
+
+#if !defined(NDEBUG)  // For debugging
+#  include "ParsedDicomFile.h"
+#endif
+
+
+namespace Orthanc
+{
+  static void CheckTargetSyntax(const std::string& transcoded,
+                                const std::set& allowedSyntaxes)
+  {
+#if !defined(NDEBUG)
+    // Debug mode
+    ParsedDicomFile parsed(transcoded);
+
+    DicomTransferSyntax a, b;
+    if (!const_cast(parsed).LookupTransferSyntax(b) ||
+        !FromDcmtkBridge::LookupOrthancTransferSyntax(a, parsed.GetDcmtkObject()) ||
+        a != b ||
+        allowedSyntaxes.find(a) == allowedSyntaxes.end())
+    {
+      throw OrthancException(
+        ErrorCode_Plugin,
+        "DEBUG - The transcoding plugin has not written to one of the allowed transfer syntaxes");
+    }
+#endif
+  }
+    
+  bool MemoryBufferTranscoder::Transcode(DicomImage& target,
+                                         DicomImage& source,
+                                         const std::set& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid,
+                                         unsigned int lossyQualityNotUsed)
+  {
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+  }
+
+  bool MemoryBufferTranscoder::Transcode(DicomImage& target,
+                                         DicomImage& source,
+                                         const std::set& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid)
+  {
+    target.Clear();
+    
+#if !defined(NDEBUG)
+    // Don't run this code in release mode, as it implies parsing the DICOM file
+    DicomTransferSyntax sourceSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
+    {
+      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
+      return false;
+    }
+    
+    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
+#endif
+
+    std::string buffer;
+    if (TranscodeBuffer(buffer, source.GetBufferData(), source.GetBufferSize(),
+                        allowedSyntaxes, allowNewSopInstanceUid))
+    {
+      CheckTargetSyntax(buffer, allowedSyntaxes);  // For debug only
+
+      target.AcquireBuffer(buffer);
+      
+#if !defined(NDEBUG)
+      // Only run the sanity check in debug mode
+      CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
+                       allowedSyntaxes, allowNewSopInstanceUid);
+#endif
+
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h
new file mode 100644
index 0000000..09c5a16
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h
@@ -0,0 +1,53 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IDicomTranscoder.h"
+
+namespace Orthanc
+{
+  // This is the basis class for transcoding plugins
+  class MemoryBufferTranscoder : public IDicomTranscoder
+  {
+  protected:
+    virtual bool TranscodeBuffer(std::string& target,
+                                 const void* buffer,
+                                 size_t size,
+                                 const std::set& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid) = 0;
+    
+  public:
+    virtual bool Transcode(DicomImage& target /* out */,
+                           DicomImage& source,
+                           const std::set& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+
+    virtual bool Transcode(DicomImage& target /* out */,
+                           DicomImage& source,
+                           const std::set& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQualityNotUsed) ORTHANC_OVERRIDE;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp
new file mode 100644
index 0000000..687109d
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.cpp
@@ -0,0 +1,222 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "ParsedDicomCache.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  class ParsedDicomCache::Item : public ICacheable
+  {
+  private:
+    std::unique_ptr  dicom_;
+    size_t                            fileSize_;
+
+  public:
+    Item(ParsedDicomFile* dicom,
+         size_t fileSize) :
+      dicom_(dicom),
+      fileSize_(fileSize)
+    {
+      if (dicom == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    virtual size_t GetMemoryUsage() const ORTHANC_OVERRIDE
+    {
+      return fileSize_;
+    }
+
+    ParsedDicomFile& GetDicom() const
+    {
+      assert(dicom_.get() != NULL);
+      return *dicom_;
+    }
+  };
+
+
+  ParsedDicomCache::ParsedDicomCache(size_t size) :
+    cacheSize_(size),
+    largeSize_(0)
+  {
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  size_t ParsedDicomCache::GetNumberOfItems()
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(mutex_);
+#endif
+
+    if (cache_.get() == NULL)
+    {
+      return (largeDicom_.get() == NULL ? 0 : 1);
+    }
+    else
+    {
+      assert(largeDicom_.get() == NULL);
+      assert(largeSize_ == 0);
+      return cache_->GetNumberOfItems();
+    }
+  }
+
+
+  size_t ParsedDicomCache::GetCurrentSize()
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(mutex_);
+#endif
+
+    if (cache_.get() == NULL)
+    {
+      return largeSize_;
+    }
+    else
+    {
+      assert(largeDicom_.get() == NULL);
+      assert(largeSize_ == 0);
+      return cache_->GetCurrentSize();
+    }
+  }
+
+  
+  void ParsedDicomCache::Invalidate(const std::string& id)
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(mutex_);
+#endif
+      
+    if (cache_.get() != NULL)
+    {
+      cache_->Invalidate(id);
+    }
+
+    if (largeId_ == id)
+    {
+      largeDicom_.reset(NULL);
+      largeSize_ = 0;
+    }
+  }
+
+  
+  void ParsedDicomCache::Acquire(const std::string& id,
+                                 ParsedDicomFile* dicom,  // Takes ownership
+                                 size_t fileSize)
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(mutex_);
+#endif
+      
+    if (fileSize >= cacheSize_)
+    {
+      cache_.reset(NULL);
+      largeDicom_.reset(dicom);
+      largeId_ = id;
+      largeSize_ = fileSize;
+    }
+    else
+    {
+      largeDicom_.reset(NULL);
+      largeSize_ = 0;
+
+      if (cache_.get() == NULL)
+      {
+        cache_.reset(new MemoryObjectCache);
+        cache_->SetMaximumSize(cacheSize_);
+      }
+
+      cache_->Acquire(id, new Item(dicom, fileSize));
+    }
+  }
+
+
+  ParsedDicomCache::Accessor::Accessor(ParsedDicomCache& that,
+                                       const std::string& id) :
+#if !defined(__EMSCRIPTEN__)
+    lock_(that.mutex_),
+#endif
+    id_(id),
+    file_(NULL),
+    fileSize_(0)
+  {
+    if (that.largeDicom_.get() != NULL &&
+        that.largeId_ == id)
+    {
+      file_ = that.largeDicom_.get();
+      fileSize_ = that.largeSize_;
+    }
+    else if (that.cache_.get() != NULL)
+    {
+      accessor_.reset(new MemoryObjectCache::Accessor(
+                        *that.cache_, id, true /* unique */));
+      if (accessor_->IsValid())
+      {            
+        const Item& item = dynamic_cast(accessor_->GetValue());
+        file_ = &item.GetDicom();
+        fileSize_ = item.GetMemoryUsage();
+      }
+    }
+  }
+
+
+  bool ParsedDicomCache::Accessor::IsValid() const
+  {
+    return file_ != NULL;
+  }
+
+
+  ParsedDicomFile& ParsedDicomCache::Accessor::GetDicom() const
+  {
+    if (IsValid())
+    {
+      assert(file_ != NULL);
+      return *file_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  size_t ParsedDicomCache::Accessor::GetFileSize() const
+  {
+    if (IsValid())
+    {
+      return fileSize_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h
new file mode 100644
index 0000000..8f9244d
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h
@@ -0,0 +1,84 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Cache/MemoryObjectCache.h"
+#include "ParsedDicomFile.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ParsedDicomCache : public boost::noncopyable
+  {
+  private:
+    class Item;
+
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex                        mutex_;
+#endif
+    
+    size_t                              cacheSize_;
+    std::unique_ptr  cache_;
+    std::unique_ptr    largeDicom_;
+    std::string                         largeId_;
+    size_t                              largeSize_;
+
+  public:
+    explicit ParsedDicomCache(size_t size);
+
+    size_t GetNumberOfItems();  // For unit tests only
+
+    size_t GetCurrentSize();  // For unit tests only
+
+    void Invalidate(const std::string& id);
+
+    void Acquire(const std::string& id,
+                 ParsedDicomFile* dicom,  // Takes ownership
+                 size_t fileSize);
+
+    class ORTHANC_PUBLIC Accessor : public boost::noncopyable
+    {
+    private:
+#if !defined(__EMSCRIPTEN__)
+      boost::mutex::scoped_lock  lock_;
+#endif
+      
+      std::string                id_;
+      ParsedDicomFile*           file_;
+      size_t                     fileSize_;
+
+      std::unique_ptr  accessor_;
+      
+    public:
+      Accessor(ParsedDicomCache& that,
+               const std::string& id);
+
+      bool IsValid() const;
+
+      ParsedDicomFile& GetDicom() const;
+
+      size_t GetFileSize() const;
+    };
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp
new file mode 100644
index 0000000..0758657
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp
@@ -0,0 +1,198 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "ParsedDicomDir.h"
+
+#include "../Compatibility.h"
+#include "../OrthancException.h"
+#include "ParsedDicomFile.h"
+#include "FromDcmtkBridge.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  void ParsedDicomDir::Clear()
+  {
+    for (size_t i = 0; i < content_.size(); i++)
+    {
+      assert(content_[i] != NULL);
+      delete content_[i];
+    }
+  }
+
+  
+  bool ParsedDicomDir::LookupIndexOfOffset(size_t& target,
+                                           unsigned int offset) const
+  {
+    if (offset == 0)
+    {
+      return false;
+    }
+
+    OffsetToIndex::const_iterator found = offsetToIndex_.find(offset);
+    if (found == offsetToIndex_.end())
+    {
+      // Error in the algorithm that computes the offsets
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      target = found->second;
+      return true;
+    }
+  }
+
+
+  ParsedDicomDir::ParsedDicomDir(const std::string& content)
+  {
+    ParsedDicomFile dicom(content);
+
+    DcmSequenceOfItems* sequence = NULL;
+    if (dicom.GetDcmtkObject().getDataset() == NULL ||
+        !dicom.GetDcmtkObject().getDataset()->findAndGetSequence(DCM_DirectoryRecordSequence, sequence).good() ||
+        sequence == NULL)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Not a DICOMDIR");
+    }
+
+    content_.resize(sequence->card());
+    nextOffsets_.resize(content_.size());
+    lowerOffsets_.resize(content_.size());
+
+    // Manually reconstruct the list of all the available offsets of
+    // "DcmItem", as "fStartPosition" is a protected member in DCMTK
+    // API
+    std::set availableOffsets;
+    availableOffsets.insert(0);
+
+
+    for (unsigned long i = 0; i < sequence->card(); i++)
+    {
+      DcmItem* item = sequence->getItem(i);
+      if (item == NULL)
+      {
+        Clear();
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      Uint32 next, lower;
+      if (!item->findAndGetUint32(DCM_OffsetOfTheNextDirectoryRecord, next).good() ||
+          !item->findAndGetUint32(DCM_OffsetOfReferencedLowerLevelDirectoryEntity, lower).good())
+      {
+        item->writeXML(std::cout);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing offsets in DICOMDIR");
+      }          
+
+      nextOffsets_[i] = next;
+      lowerOffsets_[i] = lower;
+
+      std::unique_ptr entry(new DicomMap);
+
+      std::set ignoreTagLength;
+      FromDcmtkBridge::ExtractDicomSummary(*entry, *item, 0 /* don't truncate tags */, ignoreTagLength);
+
+      if (next != 0)
+      {
+        availableOffsets.insert(next);
+      }
+
+      if (lower != 0)
+      {
+        availableOffsets.insert(lower);
+      }
+
+      content_[i] = entry.release();
+    }
+
+    if (content_.size() != availableOffsets.size())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Inconsistent offsets in DICOMDIR");
+    }
+
+    unsigned int index = 0;
+    for (std::set::const_iterator it = availableOffsets.begin();
+         it != availableOffsets.end(); ++it)
+    {
+      offsetToIndex_[*it] = index;
+      index ++;
+    }    
+  }
+
+  ParsedDicomDir::~ParsedDicomDir()
+  {
+    Clear();
+  }
+
+  size_t ParsedDicomDir::GetSize() const
+  {
+    return content_.size();
+  }
+
+
+  const DicomMap& ParsedDicomDir::GetItem(size_t i) const
+  {
+    if (i >= content_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(content_[i] != NULL);
+      return *content_[i];
+    }
+  }
+
+
+  bool ParsedDicomDir::LookupNext(size_t& target,
+                                  size_t index) const
+  {
+    if (index >= nextOffsets_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return LookupIndexOfOffset(target, nextOffsets_[index]);
+    }
+  }
+
+
+  bool ParsedDicomDir::LookupLower(size_t& target,
+                                   size_t index) const
+  {
+    if (index >= lowerOffsets_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return LookupIndexOfOffset(target, lowerOffsets_[index]);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h
new file mode 100644
index 0000000..1343e34
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h
@@ -0,0 +1,65 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1 to use this file
+#endif
+
+#include "../DicomFormat/DicomMap.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ParsedDicomDir : public boost::noncopyable
+  {
+  private:
+    typedef std::map  OffsetToIndex;
+
+    std::vector  content_;
+    std::vector     nextOffsets_;
+    std::vector     lowerOffsets_;
+    OffsetToIndex           offsetToIndex_;
+
+    void Clear();
+
+    bool LookupIndexOfOffset(size_t& target,
+                             unsigned int offset) const;
+
+  public:
+    explicit ParsedDicomDir(const std::string& content);
+
+    ~ParsedDicomDir();
+
+    size_t GetSize() const;
+
+    const DicomMap& GetItem(size_t i) const;
+
+    bool LookupNext(size_t& target,
+                    size_t index) const;
+
+    bool LookupLower(size_t& target,
+                     size_t index) const;
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp
new file mode 100644
index 0000000..8cb7af8
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp
@@ -0,0 +1,2349 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: GDCM (Grassroots DICOM). A DICOM library
+  Module:  http://gdcm.sourceforge.net/Copyright.html
+
+  Copyright (c) 2006-2011 Mathieu Malaterre
+  Copyright (c) 1993-2005 CREATIS
+  (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image)
+  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+  * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any
+  contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to
+  endorse or promote products derived from this software without specific
+  prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
+  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+  =========================================================================*/
+
+
+#include "../PrecompiledHeaders.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "ParsedDicomFile.h"
+
+#include "FromDcmtkBridge.h"
+#include "Internals/DicomFrameIndex.h"
+#include "Internals/DicomImageDecoder.h"
+#include "ToDcmtkBridge.h"
+
+#include "../DicomFormat/DicomImageInformation.h"
+#include "../Images/Image.h"
+#include "../Images/ImageProcessing.h"
+#include "../Images/PamReader.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+#if ORTHANC_ENABLE_JPEG == 1
+#  include "../Images/JpegReader.h"
+#endif
+
+#if ORTHANC_ENABLE_PNG == 1
+#  include "../Images/PngReader.h"
+#endif
+
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+
+#include 
+#include 
+#include 
+
+
+#if DCMTK_VERSION_NUMBER <= 360
+#  define EXS_JPEGProcess1      EXS_JPEGProcess1TransferSyntax
+#endif
+
+
+
+namespace Orthanc
+{
+  struct ParsedDicomFile::PImpl
+  {
+    std::unique_ptr file_;
+    std::unique_ptr  frameIndex_;
+  };
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  static void ParseTagAndGroup(DcmTagKey& key,
+                               const std::string& tag)
+  {
+    DicomTag t = FromDcmtkBridge::ParseTag(tag);
+    key = DcmTagKey(t.GetGroup(), t.GetElement());
+  }
+
+  
+  static unsigned int GetPixelDataBlockCount(DcmPixelData& pixelData,
+                                             E_TransferSyntax transferSyntax)
+  {
+    DcmPixelSequence* pixelSequence = NULL;
+    if (pixelData.getEncapsulatedRepresentation
+        (transferSyntax, NULL, pixelSequence).good() && pixelSequence)
+    {
+      return pixelSequence->card();
+    }
+    else
+    {
+      return 1;
+    }
+  }
+
+  
+  static void SendPathValueForDictionary(RestApiOutput& output,
+                                         DcmItem& dicom)
+  {
+    Json::Value v = Json::arrayValue;
+
+    for (unsigned long i = 0; i < dicom.card(); i++)
+    {
+      DcmElement* element = dicom.getElement(i);
+      if (element)
+      {
+        char buf[16];
+        sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag());
+        v.append(buf);
+      }
+    }
+
+    output.AnswerJson(v);
+  }
+
+
+  static void SendSequence(RestApiOutput& output,
+                           DcmSequenceOfItems& sequence)
+  {
+    // This element is a sequence
+    Json::Value v = Json::arrayValue;
+
+    for (unsigned long i = 0; i < sequence.card(); i++)
+    {
+      v.append(boost::lexical_cast(i));
+    }
+
+    output.AnswerJson(v);
+  }
+
+
+  namespace
+  {
+    class DicomFieldStream : public IHttpStreamAnswer
+    {
+    private:
+      DcmElement&  element_;
+      uint32_t     length_;
+      uint32_t     offset_;
+      std::string  chunk_;
+      size_t       chunkSize_;
+      
+    public:
+      DicomFieldStream(DcmElement& element,
+                       E_TransferSyntax transferSyntax) :
+        element_(element),
+        length_(element.getLength(transferSyntax)),
+        offset_(0),
+        chunkSize_(0)
+      {
+        static const size_t CHUNK_SIZE = 64 * 1024;  // Use chunks of max 64KB
+        chunk_.resize(CHUNK_SIZE);
+      }
+
+      virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/,
+                                                   bool /*deflateAllowed*/)
+        ORTHANC_OVERRIDE
+      {
+        // No support for compression
+        return HttpCompression_None;
+      }
+
+      virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE
+      {
+        return false;
+      }
+
+      virtual std::string GetContentType() ORTHANC_OVERRIDE
+      {
+        return EnumerationToString(MimeType_Binary);
+      }
+
+      virtual uint64_t  GetContentLength() ORTHANC_OVERRIDE
+      {
+        return length_;
+      }
+ 
+      virtual bool ReadNextChunk() ORTHANC_OVERRIDE
+      {
+        assert(offset_ <= length_);
+
+        if (offset_ == length_)
+        {
+          return false;
+        }
+        else
+        {
+          if (length_ - offset_ < chunk_.size())
+          {
+            chunkSize_ = length_ - offset_;
+          }
+          else
+          {
+            chunkSize_ = chunk_.size();
+          }
+
+          OFCondition cond = element_.getPartialValue(&chunk_[0], offset_, chunkSize_);
+
+          offset_ += chunkSize_;
+
+          if (!cond.good())
+          {
+            throw OrthancException(ErrorCode_InternalError,
+                                   "Error while sending a DICOM field: " +
+                                   std::string(cond.text()));
+          }
+
+          return true;
+        }
+      }
+ 
+      virtual const char *GetChunkContent() ORTHANC_OVERRIDE
+      {
+        return chunk_.c_str();
+      }
+ 
+      virtual size_t GetChunkSize() ORTHANC_OVERRIDE
+      {
+        return chunkSize_;
+      }
+    };
+  }
+
+
+  static bool AnswerPixelData(RestApiOutput& output,
+                              DcmItem& dicom,
+                              E_TransferSyntax transferSyntax,
+                              const std::string* blockUri)
+  {
+    DcmTag k(DICOM_TAG_PIXEL_DATA.GetGroup(),
+             DICOM_TAG_PIXEL_DATA.GetElement());
+
+    DcmElement *element = NULL;
+    if (!dicom.findAndGetElement(k, element).good() ||
+        element == NULL)
+    {
+      return false;
+    }
+
+    try
+    {
+      DcmPixelData& pixelData = dynamic_cast(*element);
+      if (blockUri == NULL)
+      {
+        // The user asks how many blocks are present in this pixel data
+        unsigned int blocks = GetPixelDataBlockCount(pixelData, transferSyntax);
+
+        Json::Value result(Json::arrayValue);
+        for (unsigned int i = 0; i < blocks; i++)
+        {
+          result.append(boost::lexical_cast(i));
+        }
+        
+        output.AnswerJson(result);
+        return true;
+      }
+
+
+      unsigned int block = boost::lexical_cast(*blockUri);
+
+      if (block < GetPixelDataBlockCount(pixelData, transferSyntax))
+      {
+        DcmPixelSequence* pixelSequence = NULL;
+        if (pixelData.getEncapsulatedRepresentation
+            (transferSyntax, NULL, pixelSequence).good() && pixelSequence)
+        {
+          // This is the case for JPEG transfer syntaxes
+          if (block < pixelSequence->card())
+          {
+            DcmPixelItem* pixelItem = NULL;
+            if (pixelSequence->getItem(pixelItem, block).good() && pixelItem)
+            {
+              if (pixelItem->getLength() == 0)
+              {
+                output.AnswerBuffer(NULL, 0, MimeType_Binary);
+                return true;
+              }
+
+              Uint8* buffer = NULL;
+              if (pixelItem->getUint8Array(buffer).good() && buffer)
+              {
+                output.AnswerBuffer(buffer, pixelItem->getLength(), MimeType_Binary);
+                return true;
+              }
+            }
+          }
+        }
+        else
+        {
+          // This is the case for raw, uncompressed image buffers
+          assert(*blockUri == "0");
+          DicomFieldStream stream(*element, transferSyntax);
+          output.AnswerStream(stream);
+        }
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      // The URI entered by the user is not a number
+    }
+    catch (std::bad_cast&)
+    {
+      // This should never happen
+    }
+
+    return false;
+  }
+
+
+  static void SendPathValueForLeaf(RestApiOutput& output,
+                                   const std::string& tag,
+                                   DcmItem& dicom,
+                                   E_TransferSyntax transferSyntax)
+  {
+    DcmTagKey k;
+    ParseTagAndGroup(k, tag);
+
+    DcmSequenceOfItems* sequence = NULL;
+    if (dicom.findAndGetSequence(k, sequence).good() && 
+        sequence != NULL &&
+        sequence->getVR() == EVR_SQ)
+    {
+      SendSequence(output, *sequence);
+      return;
+    }
+
+    DcmElement* element = NULL;
+    if (dicom.findAndGetElement(k, element).good() && 
+        element != NULL &&
+        //element->getVR() != EVR_UNKNOWN &&  // This would forbid private tags
+        element->getVR() != EVR_SQ)
+    {
+      DicomFieldStream stream(*element, transferSyntax);
+      output.AnswerStream(stream);
+    }
+  }
+#endif
+
+  
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void ParsedDicomFile::SendPathValue(RestApiOutput& output,
+                                      const UriComponents& uri) const
+  {
+    DcmItem* dicom = GetDcmtkObjectConst().getDataset();
+    E_TransferSyntax transferSyntax = GetDcmtkObjectConst().getDataset()->getCurrentXfer();
+
+    // Special case: Accessing the pixel data
+    if (uri.size() == 1 || 
+        uri.size() == 2)
+    {
+      DcmTagKey tag;
+      ParseTagAndGroup(tag, uri[0]);
+
+      if (tag.getGroup() == DICOM_TAG_PIXEL_DATA.GetGroup() &&
+          tag.getElement() == DICOM_TAG_PIXEL_DATA.GetElement())
+      {
+        AnswerPixelData(output, *dicom, transferSyntax, uri.size() == 1 ? NULL : &uri[1]);
+        return;
+      }
+    }        
+
+    // Go down in the tag hierarchy according to the URI
+    for (size_t pos = 0; pos < uri.size() / 2; pos++)
+    {
+      size_t index;
+      try
+      {
+        index = boost::lexical_cast(uri[2 * pos + 1]);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return;
+      }
+
+      DcmTagKey k;
+      DcmItem *child = NULL;
+      ParseTagAndGroup(k, uri[2 * pos]);
+      if (!dicom->findAndGetSequenceItem(k, child, index).good() ||
+          child == NULL)
+      {
+        return;
+      }
+
+      dicom = child;
+    }
+
+    // We have reached the end of the URI
+    if (uri.size() % 2 == 0)
+    {
+      SendPathValueForDictionary(output, *dicom);
+    }
+    else
+    {
+      SendPathValueForLeaf(output, uri.back(), *dicom, transferSyntax);
+    }
+  }
+#endif
+  
+
+  void ParsedDicomFile::Remove(const DicomTag& tag)
+  {
+    RemovePath(DicomPath(tag));
+  }
+
+
+  void ParsedDicomFile::Clear(const DicomTag& tag,
+                              bool onlyIfExists)
+  {
+    ClearPath(DicomPath(tag), onlyIfExists);
+  }
+
+
+  void ParsedDicomFile::RemovePrivateTagsInternal(const std::set* toKeep)
+  {
+    InvalidateCache();
+
+    DcmDataset& dataset = *GetDcmtkObject().getDataset();
+
+    // Loop over the dataset to detect its private tags
+    typedef std::list Tags;
+    Tags privateTags;
+
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      DcmTag tag(element->getTag());
+
+      // Is this a private tag?
+      if (tag.isPrivate())
+      {
+        bool remove = true;
+
+        // Check whether this private tag is to be kept
+        if (toKeep != NULL)
+        {
+          DicomTag tmp = FromDcmtkBridge::Convert(tag);
+          if (toKeep->find(tmp) != toKeep->end())
+          {
+            remove = false;  // Keep it
+          }
+        }
+            
+        if (remove)
+        {
+          privateTags.push_back(element);
+        }
+      }
+    }
+
+    // Loop over the detected private tags to remove them
+    for (Tags::iterator it = privateTags.begin(); 
+         it != privateTags.end(); ++it)
+    {
+      DcmElement* tmp = dataset.remove(*it);
+      if (tmp != NULL)
+      {
+        delete tmp;
+      }
+    }
+  }
+
+
+  static void InsertInternal(DcmDataset& dicom,
+                             DcmElement* element)
+  {
+    OFCondition cond = dicom.insert(element, false, false);
+    if (!cond.good())
+    {
+      // This field already exists
+      delete element;
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void ParsedDicomFile::Insert(const DicomTag& tag,
+                               const Json::Value& value,
+                               bool decodeDataUriScheme,
+                               const std::string& privateCreator)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    if (GetDcmtkObject().getDataset()->tagExists(ToDcmtkBridge::Convert(tag)))
+    {
+      throw OrthancException(ErrorCode_AlreadyExistingTag);
+    }
+
+    if (decodeDataUriScheme &&
+        value.type() == Json::stringValue &&
+        (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT ||
+         tag == DICOM_TAG_PIXEL_DATA))
+    {
+      if (EmbedContentInternal(value.asString()))
+      {
+        return;
+      }
+    }
+
+    InvalidateCache();
+
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions);
+    std::unique_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
+    InsertInternal(*GetDcmtkObject().getDataset(), element.release());
+  }
+
+
+  void ParsedDicomFile::ReplacePlainString(const DicomTag& tag,
+                                           const std::string& utf8Value)
+  {
+    if (tag.IsPrivate())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot apply this function to private tags: " + tag.Format());
+    }
+    else
+    {
+      Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent,
+              "" /* not a private tag, so no private creator */);
+    }
+  }
+
+
+  void ParsedDicomFile::SetIfAbsent(const DicomTag& tag,
+                                    const std::string& utf8Value)
+  {
+    std::string currentValue;
+    if (!GetTagValue(currentValue, tag))
+    {
+      ReplacePlainString(tag, utf8Value);
+    }
+  }
+
+  void ParsedDicomFile::RemovePrivateTags()
+  {
+    RemovePrivateTagsInternal(NULL);
+  }
+
+  void ParsedDicomFile::RemovePrivateTags(const std::set &toKeep)
+  {
+    RemovePrivateTagsInternal(&toKeep);
+  }
+
+
+  static bool CanReplaceProceed(DcmDataset& dicom,
+                                const DcmTagKey& tag,
+                                DicomReplaceMode mode)
+  {
+    if (dicom.findAndDeleteElement(tag).good())
+    {
+      // This tag was existing, it has been deleted
+      return true;
+    }
+    else
+    {
+      // This tag was absent, act wrt. the specified "mode"
+      switch (mode)
+      {
+        case DicomReplaceMode_InsertIfAbsent:
+          return true;
+
+        case DicomReplaceMode_ThrowIfAbsent:
+          throw OrthancException(ErrorCode_InexistentItem, "Cannot replace inexistent tag: " +
+                                 FromDcmtkBridge::GetTagName(DicomTag(tag.getGroup(), tag.getElement()), ""));
+
+        case DicomReplaceMode_IgnoreIfAbsent:
+          return false;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+
+
+  void ParsedDicomFile::UpdateStorageUid(const DicomTag& tag,
+                                         const std::string& utf8Value,
+                                         bool decodeDataUriScheme)
+  {
+    if (tag != DICOM_TAG_SOP_CLASS_UID &&
+        tag != DICOM_TAG_SOP_INSTANCE_UID)
+    {
+      return;
+    }
+
+    std::string binary;
+    const std::string* decoded = &utf8Value;
+
+    if (decodeDataUriScheme &&
+        boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
+    {
+      std::string mime;
+      if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      decoded = &binary;
+    }
+    else
+    {
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      if (encoding != Encoding_Utf8)
+      {
+        binary = Toolbox::ConvertFromUtf8(utf8Value, encoding);
+        decoded = &binary;
+      }
+    }
+
+    /**
+     * dcmodify will automatically correct 'Media Storage SOP Class
+     * UID' and 'Media Storage SOP Instance UID' in the metaheader, if
+     * you make changes to the related tags in the dataset ('SOP Class
+     * UID' and 'SOP Instance UID') via insert or modify mode
+     * options. You can disable this behaviour by using the -nmu
+     * option.
+     **/
+
+    if (tag == DICOM_TAG_SOP_CLASS_UID)
+    {
+      ReplacePlainString(DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, *decoded);
+    }
+
+    if (tag == DICOM_TAG_SOP_INSTANCE_UID)
+    {
+      ReplacePlainString(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID, *decoded);
+    }    
+  }
+
+
+  void ParsedDicomFile::Replace(const DicomTag& tag,
+                                const std::string& utf8Value,
+                                bool decodeDataUriScheme,
+                                DicomReplaceMode mode,
+                                const std::string& privateCreator)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+    else
+    {
+      InvalidateCache();
+
+      DcmDataset& dicom = *GetDcmtkObject().getDataset();
+      if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode))
+      {
+        // Either the tag was previously existing (and now removed), or
+        // the replace mode was set to "InsertIfAbsent"
+
+        if (decodeDataUriScheme &&
+            (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT ||
+             tag == DICOM_TAG_PIXEL_DATA))
+        {
+          if (EmbedContentInternal(utf8Value))
+          {
+            return;
+          }
+        }
+
+        std::unique_ptr element(FromDcmtkBridge::CreateElementForTag(tag, privateCreator));
+
+        if (!utf8Value.empty())
+        {
+          bool hasCodeExtensions;
+          Encoding encoding = DetectEncoding(hasCodeExtensions);
+          FromDcmtkBridge::FillElementWithString(*element, utf8Value, decodeDataUriScheme, encoding);
+        }
+
+        InsertInternal(dicom, element.release());
+
+        if (tag == DICOM_TAG_SOP_CLASS_UID ||
+            tag == DICOM_TAG_SOP_INSTANCE_UID)
+        {
+          if (decodeDataUriScheme &&
+              boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
+          {
+            std::string mime, decoded;
+            if (!Toolbox::DecodeDataUriScheme(mime, decoded, utf8Value))
+            {
+              throw OrthancException(ErrorCode_BadFileFormat);
+            }
+            else
+            {
+              UpdateStorageUid(tag, decoded, false);
+            }
+          }
+          else
+          {
+            UpdateStorageUid(tag, utf8Value, false);
+          }
+        }
+      }
+    }
+  }
+
+    
+  void ParsedDicomFile::Replace(const DicomTag& tag,
+                                const Json::Value& value,
+                                bool decodeDataUriScheme,
+                                DicomReplaceMode mode,
+                                const std::string& privateCreator)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+    else if (value.type() == Json::stringValue)
+    {
+      Replace(tag, value.asString(), decodeDataUriScheme, mode, privateCreator);
+    }
+    else
+    {
+      if (tag == DICOM_TAG_SOP_CLASS_UID ||
+          tag == DICOM_TAG_SOP_INSTANCE_UID)
+      {
+        // Must be a string
+        throw OrthancException(ErrorCode_BadParameterType);
+      }
+
+      InvalidateCache();
+
+      DcmDataset& dicom = *GetDcmtkObject().getDataset();
+      if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode))
+      {
+        // Either the tag was previously existing (and now removed), or
+        // the replace mode was set to "InsertIfAbsent"
+
+        bool hasCodeExtensions;
+        Encoding encoding = DetectEncoding(hasCodeExtensions);
+        InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
+      }
+    }
+  }
+
+    
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void ParsedDicomFile::Answer(RestApiOutput& output) const
+  {
+    std::string serialized;
+    if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *GetDcmtkObjectConst().getDataset()))
+    {
+      output.AnswerBuffer(serialized, MimeType_Dicom);
+    }
+  }
+#endif
+
+
+  bool ParsedDicomFile::GetTagValue(std::string& value,
+                                    const DicomTag& tag) const
+  {
+    DcmTagKey k(tag.GetGroup(), tag.GetElement());
+    DcmDataset& dataset = *GetDcmtkObjectConst().getDataset();
+
+    if (tag.IsPrivate() ||
+        FromDcmtkBridge::IsUnknownTag(tag) ||
+        tag == DICOM_TAG_PIXEL_DATA ||
+        tag == DICOM_TAG_ENCAPSULATED_DOCUMENT)
+    {
+      const Uint8* data = NULL;   // This is freed in the destructor of the dataset
+      long unsigned int count = 0;
+
+      if (dataset.findAndGetUint8Array(k, data, &count).good())
+      {
+        if (count > 0)
+        {
+          assert(data != NULL);
+          value.assign(reinterpret_cast(data), count);
+        }
+        else
+        {
+          value.clear();
+        }
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else
+    {
+      DcmElement* element = NULL;
+      if (!dataset.findAndGetElement(k, element).good() ||
+          element == NULL)
+      {
+        return false;
+      }
+
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      
+      std::set tmp;
+      std::unique_ptr v(FromDcmtkBridge::ConvertLeafElement
+                                    (*element, DicomToJsonFlags_Default, 
+                                     0, encoding, hasCodeExtensions, tmp));
+      
+      if (v.get() == NULL ||
+          v->IsNull())
+      {
+        value = "";
+      }
+      else
+      {
+        // TODO v->IsBinary()
+        value = v->GetContent();
+      }
+      
+      return true;
+    }
+  }
+
+
+  DicomInstanceHasher ParsedDicomFile::GetHasher() const
+  {
+    std::string patientId, studyUid, seriesUid, instanceUid;
+
+    if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID))
+    {
+      /**
+       * If "PatientID" is absent, be tolerant by considering it
+       * equals the empty string, then proceed. In Orthanc <= 1.5.6,
+       * an exception "Bad file format" was generated.
+       * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ
+       * https://orthanc.uclouvain.be/hg/orthanc/rev/4c45e018bd3de3cfa21d6efc6734673aaaee4435
+       **/
+      patientId.clear();
+    }        
+    
+    if (!GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) ||
+        !GetTagValue(seriesUid, DICOM_TAG_SERIES_INSTANCE_UID) ||
+        !GetTagValue(instanceUid, DICOM_TAG_SOP_INSTANCE_UID))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
+    }
+
+    return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid);
+  }
+
+
+  void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
+  {
+    std::string errorMessage;
+    if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset(), errorMessage))
+    {
+      throw OrthancException(ErrorCode_InternalError, "Cannot write DICOM file to memory, DCMTK error: " + errorMessage);
+    }
+  }
+
+
+#if ORTHANC_SANDBOXED == 0
+  void ParsedDicomFile::SaveToFile(const std::string& path)
+  {
+    // TODO Avoid using a temporary memory buffer, write directly on disk
+    std::string content;
+    SaveToMemoryBuffer(content);
+    SystemToolbox::WriteFile(content, path);
+  }
+#endif
+
+
+  ParsedDicomFile::ParsedDicomFile(bool createIdentifiers) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat);
+
+    if (createIdentifiers)
+    {
+      ReplacePlainString(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient));
+      ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study));
+      ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series));
+      ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+    }
+  }
+
+
+  void ParsedDicomFile::CreateFromDicomMap(const DicomMap& source,
+                                           Encoding defaultEncoding,
+                                           bool permissive,
+                                           const std::string& defaultPrivateCreator,
+                                           const std::map& privateCreators)
+  {
+    pimpl_->file_.reset(new DcmFileFormat);
+    InvalidateCache();
+
+    const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
+    if (tmp == NULL)
+    {
+      SetEncoding(defaultEncoding);
+    }
+    else if (tmp->IsBinary())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Invalid binary string in the SpecificCharacterSet (0008,0005) tag");
+    }
+    else if (tmp->IsNull() ||
+             tmp->GetContent().empty())
+    {
+      SetEncoding(defaultEncoding);
+    }
+    else
+    {
+      Encoding encoding;
+
+      if (GetDicomEncoding(encoding, tmp->GetContent().c_str()))
+      {
+        SetEncoding(encoding);
+      }
+      else if (permissive)
+      {
+        SetEncoding(defaultEncoding);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \"" +
+                               tmp->GetContent() + "\"");
+      }
+    }
+
+    for (DicomMap::Content::const_iterator 
+           it = source.content_.begin(); it != source.content_.end(); ++it)
+    {
+      if (it->first != DICOM_TAG_SPECIFIC_CHARACTER_SET &&
+          !it->second->IsNull())
+      {
+        try
+        {
+          // Same as "ReplacePlainString()", but with support for private creator
+          const std::string& utf8Value = it->second->GetContent();
+
+          std::map::const_iterator found = privateCreators.find(it->first.GetGroup());
+          
+          if (it->first.IsPrivate() &&
+              found != privateCreators.end())
+          {
+            Replace(it->first, utf8Value, false, DicomReplaceMode_InsertIfAbsent, found->second);
+          }
+          else
+          {
+            Replace(it->first, utf8Value, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
+          }
+        }
+        catch (OrthancException&)
+        {
+          if (!permissive)
+          {
+            throw;
+          }
+        }
+      }
+    }
+  }
+
+  ParsedDicomFile::ParsedDicomFile(const DicomMap& map,
+                                   Encoding defaultEncoding,
+                                   bool permissive) :
+    pimpl_(new PImpl)
+  {
+    std::map noPrivateCreators;
+    CreateFromDicomMap(map, defaultEncoding, permissive, "" /* no default private creator */, noPrivateCreators);
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const DicomMap& map,
+                                   Encoding defaultEncoding,
+                                   bool permissive,
+                                   const std::string& defaultPrivateCreator,
+                                   const std::map& privateCreators) :
+    pimpl_(new PImpl)
+  {
+    CreateFromDicomMap(map, defaultEncoding, permissive, defaultPrivateCreator, privateCreators);
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const void* content, 
+                                   size_t size) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(content, size));
+  }
+
+  ParsedDicomFile::ParsedDicomFile(const std::string& content) : pimpl_(new PImpl)
+  {
+    if (content.size() == 0)
+    {
+      pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(NULL, 0));
+    }
+    else
+    {
+      pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(&content[0], content.size()));
+    }
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const ParsedDicomFile& other,
+                                   bool keepSopInstanceUid) : 
+    pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(dynamic_cast(other.GetDcmtkObjectConst().clone()));
+
+    if (!keepSopInstanceUid)
+    {
+      // Create a new instance-level identifier
+      ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+    }
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(DcmDataset& dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat(&dicom));
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(DcmFileFormat& dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat(dicom));
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(DcmFileFormat* dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(dicom);  // No cloning
+  }
+
+
+  DcmFileFormat& ParsedDicomFile::GetDcmtkObjectConst() const
+  {
+    if (pimpl_->file_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "ReleaseDcmtkObject() was called");
+    }
+    else
+    {
+      return *pimpl_->file_;
+    }
+  }
+
+  ParsedDicomFile *ParsedDicomFile::AcquireDcmtkObject(DcmFileFormat *dicom)  // No clone here
+  {
+    return new ParsedDicomFile(dicom);
+  }
+
+  DcmFileFormat &ParsedDicomFile::GetDcmtkObject()
+  {
+    return GetDcmtkObjectConst();
+  }
+
+
+  DcmFileFormat* ParsedDicomFile::ReleaseDcmtkObject()
+  {
+    if (pimpl_->file_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "ReleaseDcmtkObject() was called");
+    }
+    else
+    {
+      pimpl_->frameIndex_.reset(NULL);
+      return pimpl_->file_.release();
+    }
+  }
+
+
+  ParsedDicomFile* ParsedDicomFile::Clone(bool keepSopInstanceUid) const
+  {
+    return new ParsedDicomFile(*this, keepSopInstanceUid);
+  }
+
+
+  bool ParsedDicomFile::EmbedContentInternal(const std::string& dataUriScheme)
+  {
+    std::string mimeString, content;
+    if (!Toolbox::DecodeDataUriScheme(mimeString, content, dataUriScheme))
+    {
+      return false;
+    }
+
+    Toolbox::ToLowerCase(mimeString);
+    MimeType mime = StringToMimeType(mimeString);
+
+    switch (mime)
+    {
+      case MimeType_Png:
+#if ORTHANC_ENABLE_PNG == 1
+        EmbedImage(mime, content);
+        break;
+#else
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Orthanc was compiled without support of PNG");
+#endif
+
+      case MimeType_Jpeg:
+#if ORTHANC_ENABLE_JPEG == 1
+        EmbedImage(mime, content);
+        break;
+#else
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Orthanc was compiled without support of JPEG");
+#endif
+
+      case MimeType_Pam:
+        EmbedImage(mime, content);
+        break;
+
+      case MimeType_Binary:
+        EmbedImage(mime, content);
+        break;
+
+      case MimeType_Pdf:
+      {
+        if (content.size() < 5 ||  // (*)
+            strncmp("%PDF-", content.c_str(), 5) != 0)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file");
+        }
+        
+        EncapsulateDocument(MimeType_Pdf, content);
+
+        // In Orthanc <= 1.9.7, the "Modality" would have always be overwritten as "OT"
+        // https://groups.google.com/g/orthanc-users/c/eNSddNrQDtM/m/wc1HahimAAAJ
+
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage);
+        SetIfAbsent(DICOM_TAG_MODALITY, "OT");
+        SetIfAbsent(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD");
+        //SetIfAbsent(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
+
+        break;
+      }
+
+      case MimeType_Mtl:
+        EncapsulateDocument(mime, content);
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.5");
+        SetIfAbsent(DICOM_TAG_MODALITY, "M3D");
+        break;
+
+      case MimeType_Obj:
+        EncapsulateDocument(mime, content);
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.4");
+        SetIfAbsent(DICOM_TAG_MODALITY, "M3D");
+        break;
+
+      case MimeType_Stl:
+        EncapsulateDocument(mime, content);
+        SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.3");
+        SetIfAbsent(DICOM_TAG_MODALITY, "M3D");
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Unsupported MIME type for the content of a new DICOM file: " +
+                               std::string(EnumerationToString(mime)));
+    }
+
+    return true;
+  }
+
+
+  void ParsedDicomFile::EmbedContent(const std::string& dataUriScheme)
+  {
+    if (!EmbedContentInternal(dataUriScheme))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void ParsedDicomFile::EmbedImage(MimeType mime,
+                                   const std::string& content)
+  {
+    switch (mime)
+    {
+    
+#if ORTHANC_ENABLE_JPEG == 1
+      case MimeType_Jpeg:
+      {
+        JpegReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+#endif
+    
+#if ORTHANC_ENABLE_PNG == 1
+      case MimeType_Png:
+      {
+        PngReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+#endif
+
+      case MimeType_Pam:
+      {
+        // "true" means "enforce memory alignment": This is slower,
+        // but possibly avoids crash related to non-aligned memory access
+        PamReader reader(true);
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+
+      case MimeType_Binary:
+        EmbedRawPixelData(content);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  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_RGBA64)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    InvalidateCache();
+
+    if (accessor.GetFormat() == PixelFormat_RGBA32 ||
+        accessor.GetFormat() == PixelFormat_RGBA64)
+    {
+      LOG(WARNING) << "Getting rid of the alpha channel when embedding a RGBA image inside DICOM";
+    }
+
+    // http://dicomiseasy.blogspot.be/2012/08/chapter-12-pixel-data.html
+
+    Remove(DICOM_TAG_PIXEL_DATA);
+    ReplacePlainString(DICOM_TAG_COLUMNS, boost::lexical_cast(accessor.GetWidth()));
+    ReplacePlainString(DICOM_TAG_ROWS, boost::lexical_cast(accessor.GetHeight()));
+    ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "1");
+
+    // The "Number of frames" must only be present in multi-frame images
+    //ReplacePlainString(DICOM_TAG_NUMBER_OF_FRAMES, "1");
+
+    if (accessor.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "1");
+    }
+    else
+    {
+      ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "0");  // Unsigned pixels
+    }
+
+    bytesPerPixel = 0;
+
+    switch (accessor.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        // By default, grayscale images are MONOCHROME2
+        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+
+        ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8");
+        ReplacePlainString(DICOM_TAG_BITS_STORED, "8");
+        ReplacePlainString(DICOM_TAG_HIGH_BIT, "7");
+        bytesPerPixel = 1;
+        break;
+
+      case PixelFormat_RGB24:
+      case PixelFormat_RGBA32:
+        ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB");
+        ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "3");
+        ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8");
+        ReplacePlainString(DICOM_TAG_BITS_STORED, "8");
+        ReplacePlainString(DICOM_TAG_HIGH_BIT, "7");
+        bytesPerPixel = 3;
+
+        // "Planar configuration" must only present if "Samples per
+        // Pixel" is greater than 1
+        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");
+        ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "16");
+        ReplacePlainString(DICOM_TAG_BITS_STORED, "16");
+        ReplacePlainString(DICOM_TAG_HIGH_BIT, "15");
+        bytesPerPixel = 6;
+
+        // "Planar configuration" must only present if "Samples per
+        // Pixel" is greater than 1
+        ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
+
+        break;
+
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+        // By default, grayscale images are MONOCHROME2
+        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+
+        ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "16");
+        ReplacePlainString(DICOM_TAG_BITS_STORED, "16");
+        ReplacePlainString(DICOM_TAG_HIGH_BIT, "15");
+        bytesPerPixel = 2;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    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());
+
+    std::unique_ptr pixels(new DcmPixelData(key));
+
+    unsigned int pitch = accessor.GetWidth() * bytesPerPixel;
+    Uint8* target = NULL;
+    pixels->createUint8Array(accessor.GetHeight() * pitch, target);
+
+    const unsigned int height = accessor.GetHeight();
+    const unsigned int width = accessor.GetWidth();
+
+    {
+      Uint8* q = target;
+      for (unsigned int y = 0; y < height; y++)
+      {
+        switch (accessor.GetFormat())
+        {
+          case PixelFormat_RGB24:
+          case PixelFormat_Grayscale8:
+          case PixelFormat_Grayscale16:
+          case PixelFormat_SignedGrayscale16:
+          {
+            memcpy(q, reinterpret_cast(accessor.GetConstRow(y)), pitch);
+            q += pitch;
+            break;
+          }
+
+          case PixelFormat_RGBA32:
+          {
+            // The alpha channel is not supported by the DICOM standard
+            const Uint8* source = reinterpret_cast(accessor.GetConstRow(y));
+            for (unsigned int x = 0; x < width; x++, q += 3, source += 4)
+            {
+              q[0] = source[0];
+              q[1] = source[1];
+              q[2] = source[2];
+            }
+
+            break;
+          }
+
+          case PixelFormat_RGBA64:
+          {
+            // The alpha channel is not supported by the DICOM standard
+            const Uint8* source = reinterpret_cast(accessor.GetConstRow(y));
+            for (unsigned int x = 0; x < width; x++, q += 6, source += 8)
+            {
+              q[0] = source[0];
+              q[1] = source[1];
+              q[2] = source[2];
+              q[3] = source[3];
+              q[4] = source[4];
+              q[5] = source[5];
+            }
+
+            break;
+          }
+          
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+      }
+    }
+
+    static const Endianness ENDIANNESS = Toolbox::DetectEndianness();
+    if (ENDIANNESS == Endianness_Big &&
+        (accessor.GetFormat() == PixelFormat_Grayscale16 ||
+         accessor.GetFormat() == PixelFormat_SignedGrayscale16))
+    {
+      // New in Orthanc 1.9.1
+      assert(pitch % 2 == 0);
+      swapBytes(target, accessor.GetHeight() * pitch, sizeof(uint16_t));
+    }
+
+    if (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }    
+  }
+
+  void ParsedDicomFile::EmbedRawPixelData(const std::string& content)
+  {
+    DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), 
+               DICOM_TAG_PIXEL_DATA.GetElement());
+
+    std::unique_ptr pixels(new DcmPixelData(key));
+
+    Uint8* target = NULL;
+    pixels->createUint8Array(content.size(), target);
+    memcpy(target, content.c_str(), content.size());
+
+    if (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const
+  {
+    return FromDcmtkBridge::DetectEncoding(hasCodeExtensions,
+                                           *GetDcmtkObjectConst().getDataset(),
+                                           GetDefaultDicomEncoding());
+  }
+
+
+  void ParsedDicomFile::SetEncoding(Encoding encoding)
+  {
+    if (encoding == Encoding_Windows1251)
+    {
+      // This Cyrillic codepage is not officially supported by the
+      // DICOM standard. Do not set the SpecificCharacterSet tag.
+      return;
+    }
+
+    std::string s = GetDicomSpecificCharacterSet(encoding);
+    ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, s);
+  }
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target, 
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength) const
+  {
+    std::set ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObjectConst().getDataset(),
+                                        format, flags, maxStringLength, ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target, 
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength,
+                                      const std::set& ignoreTagLength) const
+  {
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObjectConst().getDataset(),
+                                        format, flags, maxStringLength, ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::HeaderToJson(Json::Value& target, 
+                                     DicomToJsonFormat format) const
+  {
+    FromDcmtkBridge::ExtractHeaderAsJson(target, *GetDcmtkObjectConst().getMetaInfo(), format, DicomToJsonFlags_None, 0);
+  }
+
+
+  bool ParsedDicomFile::HasTag(const DicomTag& tag) const
+  {
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    return GetDcmtkObjectConst().getDataset()->tagExists(key);
+  }
+
+
+  void ParsedDicomFile::EncapsulateDocument(MimeType mime,
+                                            const std::string& document)
+  {
+    InvalidateCache();
+
+    ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), EnumerationToString(mime));
+
+    std::unique_ptr element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument));
+
+    size_t s = document.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);
+    }
+
+    if (s > 0)
+    {
+      bytes[s - 1] = 0;
+    }
+
+    memcpy(bytes, document.c_str(), document.size());
+      
+    DcmPolymorphOBOW* obj = element.release();
+    result = GetDcmtkObject().getDataset()->insert(obj);
+
+    if (!result.good())
+    {
+      delete obj;
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+  }
+
+
+  bool ParsedDicomFile::ExtractPdf(std::string& pdf) const
+  {
+    std::string sop, mime;
+    
+    if (!GetTagValue(sop, DICOM_TAG_SOP_CLASS_UID) ||
+        !GetTagValue(mime, FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument)) ||
+        sop != UID_EncapsulatedPDFStorage ||
+        mime != MIME_PDF)
+    {
+      return false;
+    }
+
+    if (!GetTagValue(pdf, DICOM_TAG_ENCAPSULATED_DOCUMENT))
+    {
+      return false;
+    }
+
+    // Strip the possible pad byte at the end of file, because the
+    // encapsulated documents must always have an even length. The PDF
+    // format expects files to end with %%EOF followed by CR/LF. If
+    // the last character of the file is not a CR or LF, we assume it
+    // is a pad byte and remove it.
+    if (pdf.size() > 0)
+    {
+      char last = *pdf.rbegin();
+
+      if (last != 10 && last != 13)
+      {
+        pdf.resize(pdf.size() - 1);
+      }
+    }
+
+    return true;
+  }
+
+
+  ParsedDicomFile* ParsedDicomFile::CreateFromJson(const Json::Value& json,
+                                                   DicomFromJsonFlags flags,
+                                                   const std::string& privateCreator)
+  {
+    const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers) ? true : false;
+    const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme) ? true : false;
+
+    std::unique_ptr result(new ParsedDicomFile(generateIdentifiers));
+    result->SetEncoding(FromDcmtkBridge::ExtractEncoding(json, GetDefaultDicomEncoding()));
+
+    const Json::Value::Members tags = json.getMemberNames();
+    
+    for (size_t i = 0; i < tags.size(); i++)
+    {
+      DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]);
+      const Json::Value& value = json[tags[i]];
+
+      if (tag == DICOM_TAG_PIXEL_DATA ||
+          tag == DICOM_TAG_ENCAPSULATED_DOCUMENT)
+      {
+        if (value.type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+        else
+        {
+          result->EmbedContent(value.asString());
+        }
+      }
+      else if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        result->Replace(tag, value, decodeDataUriScheme, DicomReplaceMode_InsertIfAbsent, privateCreator);
+      }
+    }
+
+    return result.release();
+  }
+
+
+  void ParsedDicomFile::GetRawFrame(std::string& target,
+                                    MimeType& mime,
+                                    unsigned int frameId) const
+  {
+    if (pimpl_->frameIndex_.get() == NULL)
+    {
+      assert(pimpl_->file_ != NULL &&
+             GetDcmtkObjectConst().getDataset() != NULL);
+      pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObjectConst().getDataset()));
+    }
+
+    pimpl_->frameIndex_->GetRawFrame(target, frameId);
+
+    E_TransferSyntax transferSyntax = GetDcmtkObjectConst().getDataset()->getCurrentXfer();
+    switch (transferSyntax)
+    {
+      case EXS_JPEGProcess1:
+        mime = MimeType_Jpeg;
+        break;
+       
+      case EXS_JPEG2000LosslessOnly:
+      case EXS_JPEG2000:
+        mime = MimeType_Jpeg2000;
+        break;
+
+      default:
+        mime = MimeType_Binary;
+        break;
+    }
+  }
+
+
+  void ParsedDicomFile::InvalidateCache()
+  {
+    pimpl_->frameIndex_.reset(NULL);
+  }
+
+
+  unsigned int ParsedDicomFile::GetFramesCount() const
+  {
+    assert(pimpl_->file_ != NULL &&
+           GetDcmtkObjectConst().getDataset() != NULL);
+    return DicomFrameIndex::GetFramesCount(*GetDcmtkObjectConst().getDataset());
+  }
+
+
+  void ParsedDicomFile::ChangeEncoding(Encoding target)
+  {
+    bool hasCodeExtensions;
+    Encoding source = DetectEncoding(hasCodeExtensions);
+
+    if (source != target)  // Avoid unnecessary conversion
+    {
+      ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target));
+      FromDcmtkBridge::ChangeStringEncoding(*GetDcmtkObject().getDataset(), source, hasCodeExtensions, target);
+    }
+  }
+
+
+  void ParsedDicomFile::ExtractDicomSummary(DicomMap& target,
+                                            unsigned int maxTagLength) const
+  {
+    std::set ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObjectConst().getDataset(),
+                                         maxTagLength, ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::ExtractDicomSummary(DicomMap& target,
+                                            unsigned int maxTagLength,
+                                            const std::set& ignoreTagLength) const
+  {
+    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObjectConst().getDataset(),
+                                         maxTagLength, ignoreTagLength);
+  }
+
+
+  bool ParsedDicomFile::LookupTransferSyntax(DicomTransferSyntax& result) const
+  {
+    return FromDcmtkBridge::LookupOrthancTransferSyntax(result, GetDcmtkObjectConst());
+  }
+
+
+  bool ParsedDicomFile::LookupPhotometricInterpretation(PhotometricInterpretation& result) const
+  {
+    DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(),
+                DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement());
+
+    DcmDataset& dataset = *GetDcmtkObjectConst().getDataset();
+
+    const char *c = NULL;
+    if (dataset.findAndGetString(k, c).good() &&
+        c != NULL)
+    {
+      result = StringToPhotometricInterpretation(c);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void ParsedDicomFile::Apply(ITagVisitor& visitor) const
+  {
+    FromDcmtkBridge::Apply(*GetDcmtkObjectConst().getDataset(), visitor, GetDefaultDicomEncoding());
+  }
+
+
+  ImageAccessor* ParsedDicomFile::DecodeFrame(unsigned int frame) const
+  {
+    if (GetDcmtkObjectConst().getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      return DicomImageDecoder::Decode(*GetDcmtkObjectConst().getDataset(), frame);
+    }
+  }
+
+
+  static bool HasGenericGroupLength(const DicomPath& path)
+  {
+    for (size_t i = 0; i < path.GetPrefixLength(); i++)
+    {
+      if (path.GetPrefixTag(i).GetElement() == 0x0000)
+      {
+        return true;
+      }
+    }
+    
+    return (path.GetFinalTag().GetElement() == 0x0000);
+  }
+  
+
+  void ParsedDicomFile::ReplacePath(const DicomPath& path,
+                                    const Json::Value& value,
+                                    bool decodeDataUriScheme,
+                                    DicomReplaceMode mode,
+                                    const std::string& privateCreator)
+  {
+    if (HasGenericGroupLength(path))
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+    else if (path.GetPrefixLength() == 0)
+    {
+      Replace(path.GetFinalTag(), value, decodeDataUriScheme, mode, privateCreator);
+    }
+    else
+    {
+      InvalidateCache();
+
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      std::unique_ptr element(
+        FromDcmtkBridge::FromJson(path.GetFinalTag(), value, decodeDataUriScheme, encoding, privateCreator));
+
+      FromDcmtkBridge::ReplacePath(*GetDcmtkObject().getDataset(), path, *element, mode);
+    }
+  }
+  
+
+  void ParsedDicomFile::RemovePath(const DicomPath& path)
+  {
+    InvalidateCache();
+    FromDcmtkBridge::RemovePath(*GetDcmtkObject().getDataset(), path);
+  }
+
+
+  void ParsedDicomFile::ClearPath(const DicomPath& path,
+                                  bool onlyIfExists)
+  {
+    if (HasGenericGroupLength(path))
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+    else
+    {
+      InvalidateCache();
+      FromDcmtkBridge::ClearPath(*GetDcmtkObject().getDataset(), path, onlyIfExists);
+    }
+  }
+
+
+  bool ParsedDicomFile::LookupSequenceItem(DicomMap& target,
+                                           const DicomPath& path,
+                                           size_t sequenceIndex) const
+  {
+    DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset();
+    return FromDcmtkBridge::LookupSequenceItem(target, dataset, path, sequenceIndex);
+  }
+  
+
+  void ParsedDicomFile::GetDefaultWindowing(double& windowCenter,
+                                            double& windowWidth,
+                                            unsigned int frame) const
+  {
+    DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset();
+
+    const char* wc = NULL;
+    const char* ww = NULL;
+    DcmItem *item1 = NULL;
+    DcmItem *item2 = NULL;
+
+    if (dataset.findAndGetString(DCM_WindowCenter, wc).good() &&
+        dataset.findAndGetString(DCM_WindowWidth, ww).good() &&
+        wc != NULL &&
+        ww != NULL &&
+        SerializationToolbox::ParseFirstDouble(windowCenter, wc) &&
+        SerializationToolbox::ParseFirstDouble(windowWidth, ww))
+    {
+      return;  // OK
+    }
+    else if (dataset.findAndGetSequenceItem(DCM_PerFrameFunctionalGroupsSequence, item1, frame).good() &&
+             item1 != NULL &&
+             item1->findAndGetSequenceItem(DCM_FrameVOILUTSequence, item2, 0).good() &&
+             item2 != NULL &&
+             item2->findAndGetString(DCM_WindowCenter, wc).good() &&
+             item2->findAndGetString(DCM_WindowWidth, ww).good() &&
+             wc != NULL &&
+             ww != NULL &&
+             SerializationToolbox::ParseFirstDouble(windowCenter, wc) &&
+             SerializationToolbox::ParseFirstDouble(windowWidth, ww))
+    {
+      // New in Orthanc 1.9.7, to deal with Philips multiframe images
+      // (cf. private mail from Tomas Kenda on 2021-08-17)
+      return;  // OK
+    }
+    else
+    {
+      Uint16 bitsStored = 0;
+      if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good() ||
+          bitsStored == 0)
+      {
+        bitsStored = 8;  // Rough assumption
+      }
+
+      windowWidth = static_cast(1 << bitsStored);
+      windowCenter = windowWidth / 2.0;
+    }
+  }
+
+  
+  void ParsedDicomFile::GetRescale(double& rescaleIntercept,
+                                   double& rescaleSlope,
+                                   unsigned int frame) const
+  {
+    DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset();
+
+    const char* sopClassUid = NULL;
+    const char* intercept = NULL;
+    const char* slope = NULL;
+    DcmItem *item1 = NULL;
+    DcmItem *item2 = NULL;
+
+    if (dataset.findAndGetString(DCM_SOPClassUID, sopClassUid).good() &&
+        sopClassUid != NULL &&
+        std::string(sopClassUid) == std::string(UID_RTDoseStorage))
+    {
+      // We must not take the rescale value into account in the case of doses
+      rescaleIntercept = 0;
+      rescaleSlope = 1;
+    }
+    else if (dataset.findAndGetString(DCM_RescaleIntercept, intercept).good() &&
+             dataset.findAndGetString(DCM_RescaleSlope, slope).good() &&
+             intercept != NULL &&
+             slope != NULL &&
+             SerializationToolbox::ParseDouble(rescaleIntercept, intercept) &&
+             SerializationToolbox::ParseDouble(rescaleSlope, slope))
+    {
+      return;  // OK
+    }
+    else if (dataset.findAndGetSequenceItem(DCM_PerFrameFunctionalGroupsSequence, item1, frame).good() &&
+             item1 != NULL &&
+             item1->findAndGetSequenceItem(DCM_PixelValueTransformationSequence, item2, 0).good() &&
+             item2 != NULL &&
+             item2->findAndGetString(DCM_RescaleIntercept, intercept).good() &&
+             item2->findAndGetString(DCM_RescaleSlope, slope).good() &&
+             intercept != NULL &&
+             slope != NULL &&
+             SerializationToolbox::ParseDouble(rescaleIntercept, intercept) &&
+             SerializationToolbox::ParseDouble(rescaleSlope, slope))
+    {
+      // New in Orthanc 1.9.7, to deal with Philips multiframe images
+      // (cf. private mail from Tomas Kenda on 2021-08-17)
+      return;  // OK
+    }
+    else
+    {
+      rescaleIntercept = 0;
+      rescaleSlope = 1;
+    }
+  }
+
+
+  void ParsedDicomFile::ListOverlays(std::set& groups) const
+  {
+    DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset();
+
+    // "Repeating Groups shall only be allowed in the even Groups (6000-601E,eeee)"
+    // https://dicom.nema.org/medical/dicom/2021e/output/chtml/part05/sect_7.6.html
+
+    for (uint16_t group = 0x6000; group <= 0x601e; group += 2)
+    {
+      if (dataset.tagExists(DcmTagKey(group, 0x0010)))
+      {
+        groups.insert(group);
+      }
+    }
+  }
+
+
+  static unsigned int Ceiling(unsigned int a,
+                              unsigned int b)
+  {
+    if (a % b == 0)
+    {
+      return a / b;
+    }
+    else
+    {
+      return a / b + 1;
+    }
+  }
+  
+
+  ImageAccessor* ParsedDicomFile::DecodeOverlay(int& originX,
+                                                int& originY,
+                                                uint16_t group) const
+  {
+    // https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.9.2.html
+
+    DcmDataset& dataset = *const_cast(*this).GetDcmtkObject().getDataset();
+
+    Uint16 rows, columns, bitsAllocated, bitPosition;
+    const Sint16* origin = NULL;
+    unsigned long originSize = 0;
+    DcmElement* overlayElement = NULL;
+    Uint8* overlayData = NULL;
+    
+    if (dataset.findAndGetUint16(DcmTagKey(group, 0x0010), rows).good() &&
+        dataset.findAndGetUint16(DcmTagKey(group, 0x0011), columns).good() &&
+        dataset.findAndGetSint16Array(DcmTagKey(group, 0x0050), origin, &originSize).good() &&
+        origin != NULL &&
+        originSize == 2 &&
+        dataset.findAndGetUint16(DcmTagKey(group, 0x0100), bitsAllocated).good() &&
+        bitsAllocated == 1 &&
+        dataset.findAndGetUint16(DcmTagKey(group, 0x0102), bitPosition).good() &&
+        bitPosition == 0 &&
+        dataset.findAndGetElement(DcmTagKey(group, 0x3000), overlayElement).good() &&
+        overlayElement != NULL &&
+        overlayElement->getUint8Array(overlayData).good() &&
+        overlayData != NULL)
+    {
+      /**
+       * WARNING - It might seem easier to use
+       * "dataset.findAndGetUint8Array()" that directly gives the size
+       * of the overlay data (using the "count" parameter), instead of
+       * "dataset.findAndGetElement()". Unfortunately, this does *not*
+       * work with Emscripten/WebAssembly, that reports a "count" that
+       * is half the number of bytes, presumably because of
+       * discrepancies in the way sizeof are computed inside DCMTK.
+       * The method "getLengthField()" reports the correct number of
+       * bytes, even if targeting WebAssembly.
+       **/
+
+      unsigned int expectedSize = Ceiling(rows * columns, 8);
+      if (overlayElement->getLengthField() < expectedSize)
+      {
+        throw OrthancException(ErrorCode_CorruptedFile, "Overlay doesn't have a valid number of bits");
+      }
+      
+      originX = origin[1];
+      originY = origin[0];
+
+      std::unique_ptr overlay(new Image(Orthanc::PixelFormat_Grayscale8, columns, rows, false));
+
+      unsigned int posBit = 0;
+      for (int y = 0; y < rows; y++)
+      {
+        uint8_t* target = reinterpret_cast(overlay->GetRow(y));
+        
+        for (int x = 0; x < columns; x++)
+        {
+          uint8_t source = overlayData[posBit / 8];
+          uint8_t mask = 1 << (posBit % 8);
+
+          *target = ((source & mask) ? 255 : 0);
+
+          target++;
+          posBit++;
+        }
+      }
+      
+      return overlay.release();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_CorruptedFile, "Invalid overlay");
+    }
+  }
+
+  
+  ImageAccessor* ParsedDicomFile::DecodeAllOverlays(int& originX,
+                                                    int& originY) const
+  {
+    std::set groups;
+    ListOverlays(groups);
+
+    if (groups.empty())
+    {
+      originX = 0;
+      originY = 0;
+      return new Image(PixelFormat_Grayscale8, 0, 0, false);
+    }
+    else
+    {
+      std::set::const_iterator it = groups.begin();
+      assert(it != groups.end());
+      
+      std::unique_ptr result(DecodeOverlay(originX, originY, *it));
+      assert(result.get() != NULL);
+      ++it;
+
+      int right = originX + static_cast(result->GetWidth());
+      int bottom = originY + static_cast(result->GetHeight());
+
+      while (it != groups.end())
+      {
+        int ox, oy;
+        std::unique_ptr overlay(DecodeOverlay(ox, oy, *it));
+        assert(overlay.get() != NULL);
+
+        int mergedX = std::min(originX, ox);
+        int mergedY = std::min(originY, oy);
+        right = std::max(right, ox + static_cast(overlay->GetWidth()));
+        bottom = std::max(bottom, oy + static_cast(overlay->GetHeight()));
+
+        assert(right >= mergedX && bottom >= mergedY);
+        unsigned int width = static_cast(right - mergedX);
+        unsigned int height = static_cast(bottom - mergedY);
+        
+        std::unique_ptr merged(new Image(PixelFormat_Grayscale8, width, height, false));
+        ImageProcessing::Set(*merged, 0);
+
+        ImageAccessor a;
+        merged->GetRegion(a, originX - mergedX, originY - mergedY, result->GetWidth(), result->GetHeight());
+        ImageProcessing::Maximum(a, *result);
+
+        merged->GetRegion(a, ox - mergedX, oy - mergedY, overlay->GetWidth(), overlay->GetHeight());
+        ImageProcessing::Maximum(a, *overlay);
+
+        originX = mergedX;
+        originY = mergedY;
+        result.reset(merged.release());
+        
+        ++it;
+      }
+
+      return result.release();
+    }
+  }
+
+  
+  void ParsedDicomFile::InjectEmptyPixelData(ValueRepresentation vr)
+  {
+    DcmItem& dataset = *GetDcmtkObject().getDataset();
+
+    DcmElement *element = NULL;
+    if (!dataset.findAndGetElement(DCM_PixelData, element).good() ||
+        element == NULL)
+    {
+      // The pixel data is indeed nonexistent, insert it now
+      switch (vr)
+      {
+        case ValueRepresentation_OtherByte:
+          if (!dataset.putAndInsertUint8Array(DCM_PixelData, NULL, 0).good())
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        case ValueRepresentation_OtherWord:
+          if (!dataset.putAndInsertUint16Array(DCM_PixelData, NULL, 0).good())
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+
+
+  void ParsedDicomFile::RemoveFromPixelData()
+  {
+    DcmItem& dataset = *GetDcmtkObject().getDataset();
+
+    // We need to go backward, otherwise "dataset.card()" is invalidated
+    for (unsigned long i = dataset.card(); i > 0; i--)
+    {
+      DcmElement* element = dataset.getElement(i - 1);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (element->getTag().getGroup() > DCM_PixelData.getGroup() ||
+          (element->getTag().getGroup() == DCM_PixelData.getGroup() &&
+           element->getTag().getElement() >= DCM_PixelData.getElement()))
+      {
+        std::unique_ptr removal(dataset.remove(i - 1));
+      }
+    }
+  }
+  
+
+  ValueRepresentation ParsedDicomFile::GuessPixelDataValueRepresentation() const
+  {
+    DicomTransferSyntax ts;
+    if (LookupTransferSyntax(ts))
+    {
+      DcmItem& dataset = *GetDcmtkObjectConst().getDataset();
+
+      uint16_t bitsAllocated;
+      if (!dataset.findAndGetUint16(DCM_BitsAllocated, bitsAllocated).good())
+      {
+        bitsAllocated = 8;
+      }
+
+      return DicomImageInformation::GuessPixelDataValueRepresentation(ts, bitsAllocated);
+    }
+    else
+    {
+      // Assume "OB" if the transfer syntax is unknown
+      return ValueRepresentation_OtherByte;
+    }
+  }
+
+
+  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(reinterpret_cast(content.c_str()));
+
+      DcmOffsetList offsetList;
+
+      std::unique_ptr 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 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,
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength)
+  {
+    return const_cast(*this).DatasetToJson(target, format, flags, maxStringLength);
+  }
+
+  DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const
+  {
+    return const_cast(*this).GetDcmtkObject();
+  }
+
+  void ParsedDicomFile::Apply(ITagVisitor& visitor)
+  {
+    const_cast(*this).Apply(visitor);
+  }
+
+  ParsedDicomFile* ParsedDicomFile::Clone(bool keepSopInstanceUid)
+  {
+    return const_cast(*this).Clone(keepSopInstanceUid);
+  }
+  
+  bool ParsedDicomFile::LookupTransferSyntax(std::string& result)
+  {
+    return const_cast(*this).LookupTransferSyntax(result);
+  }
+  
+  bool ParsedDicomFile::LookupTransferSyntax(std::string& result) const
+  {
+    DicomTransferSyntax s;
+    if (LookupTransferSyntax(s))
+    {
+      result = GetTransferSyntaxUid(s);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool ParsedDicomFile::GetTagValue(std::string& value,
+                                    const DicomTag& tag)
+  {
+    return const_cast(*this).GetTagValue(value, tag);
+  }
+#endif
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h
new file mode 100644
index 0000000..ec064c5
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h
@@ -0,0 +1,328 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+
+#if !defined(ORTHANC_ENABLE_JPEG)
+#  error Macro ORTHANC_ENABLE_JPEG must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_PNG)
+#  error Macro ORTHANC_ENABLE_PNG must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_CIVETWEB)
+#  error Macro ORTHANC_ENABLE_CIVETWEB must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_MONGOOSE)
+#  error Macro ORTHANC_ENABLE_MONGOOSE must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1 to use this file
+#endif
+
+#include "ITagVisitor.h"
+#include "../DicomFormat/DicomInstanceHasher.h"
+#include "../DicomFormat/DicomPath.h"
+#include "../Images/ImageAccessor.h"
+#include "../IDynamicObject.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../RestApi/RestApiOutput.h"
+#endif
+
+#include 
+
+
+class DcmDataset;
+class DcmFileFormat;
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ParsedDicomFile : public IDynamicObject
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr pimpl_;
+
+    ParsedDicomFile(const ParsedDicomFile& other,
+                    bool keepSopInstanceUid);
+
+    void CreateFromDicomMap(const DicomMap& source,
+                            Encoding defaultEncoding,
+                            bool permissive,
+                            const std::string& defaultPrivateCreator,
+                            const std::map& privateCreators);
+
+    void RemovePrivateTagsInternal(const std::set* toKeep);
+
+    void UpdateStorageUid(const DicomTag& tag,
+                          const std::string& value,
+                          bool decodeDataUriScheme);
+
+    void InvalidateCache();
+
+    bool EmbedContentInternal(const std::string& dataUriScheme);
+
+    void EncapsulateDocument(MimeType mime,
+                             const std::string& document);
+
+    // For internal use only, in order to provide const-correctness on
+    // 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
+    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
+    void DatasetToJson(Json::Value& target, 
+                       DicomToJsonFormat format,
+                       DicomToJsonFlags flags,
+                       unsigned int maxStringLength);    
+    DcmFileFormat& GetDcmtkObject() const;
+    void Apply(ITagVisitor& visitor);
+    ParsedDicomFile* Clone(bool keepSopInstanceUid);
+    bool LookupTransferSyntax(std::string& result);
+    bool LookupTransferSyntax(std::string& result) const;
+    bool GetTagValue(std::string& value,
+                     const DicomTag& tag);
+#endif
+
+  public:
+    explicit ParsedDicomFile(bool createIdentifiers);  // Create a minimal DICOM instance
+
+    ParsedDicomFile(const DicomMap& map,
+                    Encoding defaultEncoding,
+                    bool permissive);
+
+    ParsedDicomFile(const DicomMap& map,
+                    Encoding defaultEncoding,
+                    bool permissive,
+                    const std::string& defaultPrivateCreator,
+                    const std::map& privateCreators);
+
+    ParsedDicomFile(const void* content,
+                    size_t size);
+
+    explicit ParsedDicomFile(const std::string& content);
+
+    explicit ParsedDicomFile(DcmDataset& dicom);  // This clones the DCMTK object
+
+    explicit ParsedDicomFile(DcmFileFormat& dicom);  // This clones the DCMTK object
+
+    static ParsedDicomFile* AcquireDcmtkObject(DcmFileFormat* dicom);
+
+    DcmFileFormat& GetDcmtkObject();
+
+    // The "ParsedDicomFile" object cannot be used after calling this method
+    DcmFileFormat* ReleaseDcmtkObject();
+
+    ParsedDicomFile* Clone(bool keepSopInstanceUid) const;
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+    void SendPathValue(RestApiOutput& output,
+                       const UriComponents& uri) const;
+
+    void Answer(RestApiOutput& output) const;
+#endif
+
+    void Remove(const DicomTag& tag);
+
+    // Replace the DICOM tag as a NULL/empty value (e.g. for anonymization)
+    void Clear(const DicomTag& tag,
+               bool onlyIfExists);
+
+    void Replace(const DicomTag& tag,
+                 const std::string& utf8Value,
+                 bool decodeDataUriScheme,
+                 DicomReplaceMode mode,
+                 const std::string& privateCreator /* used only for private tags */);
+
+    void Replace(const DicomTag& tag,
+                 const Json::Value& value,  // Assumed to be encoded with UTF-8
+                 bool decodeDataUriScheme,
+                 DicomReplaceMode mode,
+                 const std::string& privateCreator /* used only for private tags */);
+
+    void Insert(const DicomTag& tag,
+                const Json::Value& value,   // Assumed to be encoded with UTF-8
+                bool decodeDataUriScheme,
+                const std::string& privateCreator /* used only for private tags */);
+
+    // Cannot be applied to private tags
+    void ReplacePlainString(const DicomTag& tag,
+                            const std::string& utf8Value);
+
+    // Cannot be applied to private tags
+    void SetIfAbsent(const DicomTag& tag,
+                     const std::string& utf8Value);
+
+    void RemovePrivateTags();
+
+    void RemovePrivateTags(const std::set& toKeep);
+
+    // WARNING: This function handles the decoding of strings to UTF8
+    bool GetTagValue(std::string& value,
+                     const DicomTag& tag) const;
+
+    DicomInstanceHasher GetHasher() const;
+
+    // The "Save" methods are not tagged as "const", as the internal
+    // representation might be changed after serialization
+    void SaveToMemoryBuffer(std::string& buffer);
+
+#if ORTHANC_SANDBOXED == 0
+    void SaveToFile(const std::string& path);
+#endif
+
+    // This method must only be used on the PixelData and EncapsulatedDocument tags
+    void EmbedContent(const std::string& dataUriScheme);
+
+    void EmbedImage(const ImageAccessor& accessor);
+
+    void EmbedImage(MimeType mime,
+                    const std::string& content);
+
+    void EmbedRawPixelData(const std::string& content);
+
+    Encoding DetectEncoding(bool& hasCodeExtensions) const;
+
+    // WARNING: This function only sets the encoding, it will not
+    // convert the encoding of the tags. Use "ChangeEncoding()" if need be.
+    void SetEncoding(Encoding encoding);
+
+    void DatasetToJson(Json::Value& target, 
+                       DicomToJsonFormat format,
+                       DicomToJsonFlags flags,
+                       unsigned int maxStringLength) const;
+
+    void DatasetToJson(Json::Value& target, 
+                       DicomToJsonFormat format,
+                       DicomToJsonFlags flags,
+                       unsigned int maxStringLength,
+                       const std::set& ignoreTagLength) const;
+      
+    void HeaderToJson(Json::Value& target, 
+                      DicomToJsonFormat format) const;
+
+    bool HasTag(const DicomTag& tag) const;
+
+    bool ExtractPdf(std::string& pdf) const;
+
+    void GetRawFrame(std::string& target, // OUT
+                     MimeType& mime,   // OUT
+                     unsigned int frameId) const;  // IN
+
+    unsigned int GetFramesCount() const;
+
+    static ParsedDicomFile* CreateFromJson(const Json::Value& value,
+                                           DicomFromJsonFlags flags,
+                                           const std::string& privateCreator);
+
+    void ChangeEncoding(Encoding target);
+
+    /**
+     * The DICOM tags with a string whose size is greater than
+     * "maxTagLength", are replaced by a DicomValue whose type is
+     * "DicomValue_Null". If "maxTagLength" is zero, all the leaf tags
+     * are included, independently of their length.
+     **/
+    void ExtractDicomSummary(DicomMap& target,
+                             unsigned int maxTagLength) const;
+
+    /**
+     * This flavor can be used to bypass the "maxTagLength" limitation
+     * on a selected set of DICOM tags.
+     **/
+    void ExtractDicomSummary(DicomMap& target,
+                             unsigned int maxTagLength,
+                             const std::set& ignoreTagLength) const;
+
+    bool LookupTransferSyntax(DicomTransferSyntax& result) const;
+
+    bool LookupPhotometricInterpretation(PhotometricInterpretation& result) const;
+
+    void Apply(ITagVisitor& visitor) const;
+
+    // Decode the given frame, using the built-in DICOM decoder of Orthanc
+    ImageAccessor* DecodeFrame(unsigned int frame) const;
+
+    void ReplacePath(const DicomPath& path,
+                     const Json::Value& value,  // Assumed to be encoded with UTF-8
+                     bool decodeDataUriScheme,
+                     DicomReplaceMode mode,
+                     const std::string& privateCreator /* used only for private tags */);
+
+    void RemovePath(const DicomPath& path);
+
+    void ClearPath(const DicomPath& path,
+                   bool onlyIfExists);
+
+    bool LookupSequenceItem(DicomMap& target,
+                            const DicomPath& path,
+                            size_t sequenceIndex) const;
+
+    void GetDefaultWindowing(double& windowCenter,
+                             double& windowWidth,
+                             unsigned int frame) const;
+
+    void GetRescale(double& rescaleIntercept,
+                    double& rescaleSlope,
+                    unsigned int frame) const;
+
+    void ListOverlays(std::set& groups) const;
+
+    ImageAccessor* DecodeOverlay(int& originX,
+                                 int& originY,
+                                 uint16_t group) const;
+
+    ImageAccessor* DecodeAllOverlays(int& originX,
+                                     int& originY) const;
+
+    void InjectEmptyPixelData(ValueRepresentation vr);
+
+    // Remove all the tags after pixel data
+    void RemoveFromPixelData();
+
+    ValueRepresentation GuessPixelDataValueRepresentation() const;
+
+    void EncapsulatePixelData(const std::string& dataUriScheme);
+  };
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp
new file mode 100644
index 0000000..44edb60
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp
@@ -0,0 +1,140 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "ToDcmtkBridge.h"
+
+#include 
+
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  DcmEVR ToDcmtkBridge::Convert(ValueRepresentation vr)
+  {
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:
+        return EVR_AE;
+
+      case ValueRepresentation_AgeString:
+        return EVR_AS;
+
+      case ValueRepresentation_AttributeTag:
+        return EVR_AT;
+
+      case ValueRepresentation_CodeString:
+        return EVR_CS;
+
+      case ValueRepresentation_Date:
+        return EVR_DA;
+
+      case ValueRepresentation_DecimalString:
+        return EVR_DS;
+
+      case ValueRepresentation_DateTime:
+        return EVR_DT;
+
+      case ValueRepresentation_FloatingPointSingle:
+        return EVR_FL;
+
+      case ValueRepresentation_FloatingPointDouble:
+        return EVR_FD;
+
+      case ValueRepresentation_IntegerString:
+        return EVR_IS;
+
+      case ValueRepresentation_LongString:
+        return EVR_LO;
+
+      case ValueRepresentation_LongText:
+        return EVR_LT;
+
+      case ValueRepresentation_OtherByte:
+        return EVR_OB;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_OtherDouble:
+          return EVR_OD;*/
+
+      case ValueRepresentation_OtherFloat:
+        return EVR_OF;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_OtherLong:
+          return EVR_OL;*/
+
+      case ValueRepresentation_OtherWord:
+        return EVR_OW;
+
+      case ValueRepresentation_PersonName:
+        return EVR_PN;
+
+      case ValueRepresentation_ShortString:
+        return EVR_SH;
+
+      case ValueRepresentation_SignedLong:
+        return EVR_SL;
+
+      case ValueRepresentation_Sequence:
+        return EVR_SQ;
+
+      case ValueRepresentation_SignedShort:
+        return EVR_SS;
+
+      case ValueRepresentation_ShortText:
+        return EVR_ST;
+
+      case ValueRepresentation_Time:
+        return EVR_TM;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_UnlimitedCharacters:
+          return EVR_UC;*/
+
+      case ValueRepresentation_UniqueIdentifier:
+        return EVR_UI;
+
+      case ValueRepresentation_UnsignedLong:
+        return EVR_UL;
+
+      case ValueRepresentation_Unknown:
+        return EVR_UN;
+
+        // Not supported as of DCMTK 3.6.0
+        /*case ValueRepresentation_UniversalResource:
+          return EVR_UR;*/
+
+      case ValueRepresentation_UnsignedShort:
+        return EVR_US;
+
+      case ValueRepresentation_UnlimitedText:
+        return EVR_UT;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h
new file mode 100644
index 0000000..9e920d4
--- /dev/null
+++ b/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h
@@ -0,0 +1,46 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1
+#endif
+
+#include "../DicomFormat/DicomMap.h"
+#include 
+
+namespace Orthanc
+{
+  class ToDcmtkBridge
+  {
+  public:
+    static DcmTagKey Convert(const DicomTag& tag)
+    {
+      return DcmTagKey(tag.GetGroup(), tag.GetElement());
+    }
+
+    static DcmEVR Convert(ValueRepresentation vr);
+  };
+}
diff --git a/OrthancFramework/Sources/Endianness.h b/OrthancFramework/Sources/Endianness.h
new file mode 100644
index 0000000..747aed9
--- /dev/null
+++ b/OrthancFramework/Sources/Endianness.h
@@ -0,0 +1,211 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+
+/********************************************************************
+ ** LINUX-LIKE ARCHITECTURES
+ ********************************************************************/
+
+#if defined(__LSB_VERSION__)
+// Linux Standard Base (LSB) does not come with be16toh, be32toh, and
+// be64toh
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 0
+#  include 
+#elif defined(__linux__) || defined(__EMSCRIPTEN__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include 
+#endif
+
+
+/********************************************************************
+ ** WINDOWS ARCHITECTURES
+ **
+ ** On Windows x86, "host" will always be little-endian ("le").
+ ********************************************************************/
+
+#if defined(_WIN32)
+#  if defined(_MSC_VER)
+//   Visual Studio - http://msdn.microsoft.com/en-us/library/a3140177.aspx
+#    define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#    define be16toh(x) _byteswap_ushort(x)
+#    define be32toh(x) _byteswap_ulong(x)
+#    define be64toh(x) _byteswap_uint64(x)
+#  elif (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))
+//   MinGW >= 4.3 - Use builtin intrinsic for byte swapping
+#    define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#    define be16toh(x) __builtin_bswap16(x)
+#    define be32toh(x) __builtin_bswap32(x)
+#    define be64toh(x) __builtin_bswap64(x)
+#  else
+//   MinGW <= 4.2, we must manually implement the byte swapping (*)
+#    define ORTHANC_HAS_BUILTIN_BYTE_SWAP 0
+#    define be16toh(x) __orthanc_bswap16(x)
+#    define be32toh(x) __orthanc_bswap32(x)
+#    define be64toh(x) __orthanc_bswap64(x)
+#  endif
+
+#  define htobe16(x) be16toh(x)
+#  define htobe32(x) be32toh(x)
+#  define htobe64(x) be64toh(x)
+
+#  define htole16(x) x
+#  define htole32(x) x
+#  define htole64(x) x
+
+#  define le16toh(x) x
+#  define le32toh(x) x
+#  define le64toh(x) x
+#endif
+
+
+/********************************************************************
+ ** FREEBSD ARCHITECTURES
+ ********************************************************************/
+
+#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include 
+#endif
+
+
+/********************************************************************
+ ** OPENBSD ARCHITECTURES
+ ********************************************************************/
+
+#if defined(__OpenBSD__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include 
+#endif
+
+
+/********************************************************************
+ ** APPLE ARCHITECTURES (including OS X)
+ ********************************************************************/
+
+#if defined(__APPLE__)
+#  define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1
+#  include 
+#  define be16toh(x) OSSwapBigToHostInt16(x)
+#  define be32toh(x) OSSwapBigToHostInt32(x)
+#  define be64toh(x) OSSwapBigToHostInt64(x)
+
+#  define htobe16(x) OSSwapHostToBigInt16(x)
+#  define htobe32(x) OSSwapHostToBigInt32(x)
+#  define htobe64(x) OSSwapHostToBigInt64(x)
+
+#  define htole16(x) OSSwapHostToLittleInt16(x)
+#  define htole32(x) OSSwapHostToLittleInt32(x)
+#  define htole64(x) OSSwapHostToLittleInt64(x)
+
+#  define le16toh(x) OSSwapLittleToHostInt16(x)
+#  define le32toh(x) OSSwapLittleToHostInt32(x)
+#  define le64toh(x) OSSwapLittleToHostInt64(x)
+#endif
+
+
+/********************************************************************
+ ** PORTABLE (BUT SLOW) IMPLEMENTATION OF BYTE-SWAPPING
+ ********************************************************************/
+
+#if ORTHANC_HAS_BUILTIN_BYTE_SWAP != 1
+
+#include 
+
+static inline uint16_t __orthanc_bswap16(uint16_t a)
+{
+  /**
+   * Note that an alternative implementation was included in Orthanc
+   * 1.4.0 and 1.4.1:
+   * 
+   *  # hg log -p -r 2706
+   *
+   * This alternative implementation only hid an underlying problem
+   * with pointer alignment on some architectures, and was thus
+   * reverted. Check out issue #99:
+   * https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=99
+   **/
+  return (a << 8) | (a >> 8);
+}
+
+static inline uint32_t __orthanc_bswap32(uint32_t a)
+{
+  const uint8_t* p = reinterpret_cast(&a);
+  return (static_cast(p[0]) << 24 |
+          static_cast(p[1]) << 16 |
+          static_cast(p[2]) << 8 |
+          static_cast(p[3]));
+}
+
+static inline uint64_t __orthanc_bswap64(uint64_t a)
+{
+  const uint8_t* p = reinterpret_cast(&a);
+  return (static_cast(p[0]) << 56 |
+          static_cast(p[1]) << 48 |
+          static_cast(p[2]) << 40 |
+          static_cast(p[3]) << 32 |
+          static_cast(p[4]) << 24 |
+          static_cast(p[5]) << 16 |
+          static_cast(p[6]) << 8 |
+          static_cast(p[7]));
+}
+
+#if defined(_WIN32)
+// Implemented above (*)
+#elif defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && defined(__BIG_ENDIAN)
+#  if __BYTE_ORDER == __LITTLE_ENDIAN
+#    define be16toh(x) __orthanc_bswap16(x)
+#    define be32toh(x) __orthanc_bswap32(x)
+#    define be64toh(x) __orthanc_bswap64(x)
+#    define htobe16(x) __orthanc_bswap16(x)
+#    define htobe32(x) __orthanc_bswap32(x)
+#    define htobe64(x) __orthanc_bswap64(x)
+#    define htole16(x) x
+#    define htole32(x) x
+#    define htole64(x) x
+#    define le16toh(x) x
+#    define le32toh(x) x
+#    define le64toh(x) x
+#  elif __BYTE_ORDER == __BIG_ENDIAN
+#    define be16toh(x) x
+#    define be32toh(x) x
+#    define be64toh(x) x
+#    define htobe16(x) x
+#    define htobe32(x) x
+#    define htobe64(x) x
+#    define htole16(x) __orthanc_bswap16(x)
+#    define htole32(x) __orthanc_bswap32(x)
+#    define htole64(x) __orthanc_bswap64(x)
+#    define le16toh(x) __orthanc_bswap16(x)
+#    define le32toh(x) __orthanc_bswap32(x)
+#    define le64toh(x) __orthanc_bswap64(x)
+#  else
+#    error Please support your platform here
+#  endif
+#else
+#  error Please support your platform here
+#endif
+
+#endif
diff --git a/OrthancFramework/Sources/EnumerationDictionary.h b/OrthancFramework/Sources/EnumerationDictionary.h
new file mode 100644
index 0000000..0c0647b
--- /dev/null
+++ b/OrthancFramework/Sources/EnumerationDictionary.h
@@ -0,0 +1,115 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "OrthancException.h"
+
+#include "Toolbox.h"
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  template 
+  class EnumerationDictionary
+  {
+  private:
+    typedef std::map  EnumerationToString;
+    typedef std::map  StringToEnumeration;
+
+    EnumerationToString enumerationToString_;
+    StringToEnumeration stringToEnumeration_;
+
+  public:
+    void Clear()
+    {
+      enumerationToString_.clear();
+      stringToEnumeration_.clear();
+    }
+
+    bool Contains(Enumeration value) const
+    {
+      return enumerationToString_.find(value) != enumerationToString_.end();
+    }
+
+    void Add(Enumeration value, const std::string& str)
+    {
+      // Check if these values are free
+      if (enumerationToString_.find(value) != enumerationToString_.end() ||
+          stringToEnumeration_.find(str) != stringToEnumeration_.end() ||
+          Toolbox::IsInteger(str) /* Prevent the registration of a number */)
+      {
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      // OK, the string is free and is not a number
+      enumerationToString_[value] = str;
+      stringToEnumeration_[str] = value;
+      stringToEnumeration_[boost::lexical_cast(static_cast(value))] = value;
+    }
+
+    Enumeration Translate(const std::string& str) const
+    {
+      if (Toolbox::IsInteger(str))
+      {
+        return static_cast(boost::lexical_cast(str));
+      }
+
+      typename StringToEnumeration::const_iterator
+        found = stringToEnumeration_.find(str);
+
+      if (found == stringToEnumeration_.end())
+      {
+        throw OrthancException(ErrorCode_InexistentItem);
+      }
+      else
+      {
+        return found->second;
+      }
+    }
+
+    std::string Translate(Enumeration e) const
+    {
+      typename EnumerationToString::const_iterator
+        found = enumerationToString_.find(e);
+
+      if (found == enumerationToString_.end())
+      {
+        // No name for this item
+        return boost::lexical_cast(static_cast(e));
+      }
+      else
+      {
+        return found->second;
+      }
+    }
+
+    const std::map& GetAllEntries() const
+    {
+      return stringToEnumeration_;
+    }
+  };
+}
diff --git a/OrthancFramework/Sources/Enumerations.cpp b/OrthancFramework/Sources/Enumerations.cpp
new file mode 100644
index 0000000..0e94629
--- /dev/null
+++ b/OrthancFramework/Sources/Enumerations.cpp
@@ -0,0 +1,2540 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "Enumerations.h"
+
+#include "Logging.h"
+#include "MultiThreading/Mutex.h"
+#include "OrthancException.h"
+#include "Toolbox.h"
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  // This function is autogenerated by the script
+  // "Resources/CodeGeneration/GenerateErrorCodes.py"
+  const char* EnumerationToString(ErrorCode error)
+  {
+    switch (error)
+    {
+      case ErrorCode_InternalError:
+        return "Internal error";
+
+      case ErrorCode_Success:
+        return "Success";
+
+      case ErrorCode_Plugin:
+        return "Error encountered within the plugin engine";
+
+      case ErrorCode_NotImplemented:
+        return "Not implemented yet";
+
+      case ErrorCode_ParameterOutOfRange:
+        return "Parameter out of range";
+
+      case ErrorCode_NotEnoughMemory:
+        return "The server hosting Orthanc is running out of memory";
+
+      case ErrorCode_BadParameterType:
+        return "Bad type for a parameter";
+
+      case ErrorCode_BadSequenceOfCalls:
+        return "Bad sequence of calls";
+
+      case ErrorCode_InexistentItem:
+        return "Accessing an inexistent item";
+
+      case ErrorCode_BadRequest:
+        return "Bad request";
+
+      case ErrorCode_NetworkProtocol:
+        return "Error in the network protocol";
+
+      case ErrorCode_SystemCommand:
+        return "Error while calling a system command";
+
+      case ErrorCode_Database:
+        return "Error with the database engine";
+
+      case ErrorCode_UriSyntax:
+        return "Badly formatted URI";
+
+      case ErrorCode_InexistentFile:
+        return "Inexistent file";
+
+      case ErrorCode_CannotWriteFile:
+        return "Cannot write to file";
+
+      case ErrorCode_BadFileFormat:
+        return "Bad file format";
+
+      case ErrorCode_Timeout:
+        return "Timeout";
+
+      case ErrorCode_UnknownResource:
+        return "Unknown resource";
+
+      case ErrorCode_IncompatibleDatabaseVersion:
+        return "Incompatible version of the database";
+
+      case ErrorCode_FullStorage:
+        return "The file storage is full";
+
+      case ErrorCode_CorruptedFile:
+        return "Corrupted file (e.g. inconsistent MD5 hash)";
+
+      case ErrorCode_InexistentTag:
+        return "Inexistent tag";
+
+      case ErrorCode_ReadOnly:
+        return "Cannot modify a read-only data structure";
+
+      case ErrorCode_IncompatibleImageFormat:
+        return "Incompatible format of the images";
+
+      case ErrorCode_IncompatibleImageSize:
+        return "Incompatible size of the images";
+
+      case ErrorCode_SharedLibrary:
+        return "Error while using a shared library (plugin)";
+
+      case ErrorCode_UnknownPluginService:
+        return "Plugin invoking an unknown service";
+
+      case ErrorCode_UnknownDicomTag:
+        return "Unknown DICOM tag";
+
+      case ErrorCode_BadJson:
+        return "Cannot parse a JSON document";
+
+      case ErrorCode_Unauthorized:
+        return "Bad credentials were provided to an HTTP request";
+
+      case ErrorCode_BadFont:
+        return "Badly formatted font file";
+
+      case ErrorCode_DatabasePlugin:
+        return "The plugin implementing a custom database back-end does not fulfill the proper interface";
+
+      case ErrorCode_StorageAreaPlugin:
+        return "Error in the plugin implementing a custom storage area";
+
+      case ErrorCode_EmptyRequest:
+        return "The request is empty";
+
+      case ErrorCode_NotAcceptable:
+        return "Cannot send a response which is acceptable according to the Accept HTTP header";
+
+      case ErrorCode_NullPointer:
+        return "Cannot handle a NULL pointer";
+
+      case ErrorCode_DatabaseUnavailable:
+        return "The database is currently not available (probably a transient situation)";
+
+      case ErrorCode_CanceledJob:
+        return "This job was canceled";
+
+      case ErrorCode_BadGeometry:
+        return "Geometry error encountered in Stone";
+
+      case ErrorCode_SslInitialization:
+        return "Cannot initialize SSL encryption, check out your certificates";
+
+      case ErrorCode_DiscontinuedAbi:
+        return "Calling a function that has been removed from the Orthanc Framework";
+
+      case ErrorCode_BadRange:
+        return "Incorrect range request";
+
+      case ErrorCode_DatabaseCannotSerialize:
+        return "Database could not serialize access due to concurrent update, the transaction should be retried";
+
+      case ErrorCode_Revision:
+        return "A bad revision number was provided, which might indicate conflict between multiple writers";
+
+      case ErrorCode_MainDicomTagsMultiplyDefined:
+        return "A main DICOM Tag has been defined multiple times for the same resource level";
+
+      case ErrorCode_ForbiddenAccess:
+        return "Access to a resource is forbidden";
+
+      case ErrorCode_DuplicateResource:
+        return "Duplicate resource";
+
+      case ErrorCode_IncompatibleConfigurations:
+        return "Your configuration file contains configuration that are mutually incompatible";
+
+      case ErrorCode_SQLiteNotOpened:
+        return "SQLite: The database is not opened";
+
+      case ErrorCode_SQLiteAlreadyOpened:
+        return "SQLite: Connection is already open";
+
+      case ErrorCode_SQLiteCannotOpen:
+        return "SQLite: Unable to open the database";
+
+      case ErrorCode_SQLiteStatementAlreadyUsed:
+        return "SQLite: This cached statement is already being referred to";
+
+      case ErrorCode_SQLiteExecute:
+        return "SQLite: Cannot execute a command";
+
+      case ErrorCode_SQLiteRollbackWithoutTransaction:
+        return "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)";
+
+      case ErrorCode_SQLiteCommitWithoutTransaction:
+        return "SQLite: Committing a nonexistent transaction";
+
+      case ErrorCode_SQLiteRegisterFunction:
+        return "SQLite: Unable to register a function";
+
+      case ErrorCode_SQLiteFlush:
+        return "SQLite: Unable to flush the database";
+
+      case ErrorCode_SQLiteCannotRun:
+        return "SQLite: Cannot run a cached statement";
+
+      case ErrorCode_SQLiteCannotStep:
+        return "SQLite: Cannot step over a cached statement";
+
+      case ErrorCode_SQLiteBindOutOfRange:
+        return "SQLite: Bind a value while out of range (serious error)";
+
+      case ErrorCode_SQLitePrepareStatement:
+        return "SQLite: Cannot prepare a cached statement";
+
+      case ErrorCode_SQLiteTransactionAlreadyStarted:
+        return "SQLite: Beginning the same transaction twice";
+
+      case ErrorCode_SQLiteTransactionCommit:
+        return "SQLite: Failure when committing the transaction";
+
+      case ErrorCode_SQLiteTransactionBegin:
+        return "SQLite: Cannot start a transaction";
+
+      case ErrorCode_DirectoryOverFile:
+        return "The directory to be created is already occupied by a regular file";
+
+      case ErrorCode_FileStorageCannotWrite:
+        return "Unable to create a subdirectory or a file in the file storage";
+
+      case ErrorCode_DirectoryExpected:
+        return "The specified path does not point to a directory";
+
+      case ErrorCode_HttpPortInUse:
+        return "The TCP port of the HTTP server is privileged or already in use";
+
+      case ErrorCode_DicomPortInUse:
+        return "The TCP port of the DICOM server is privileged or already in use";
+
+      case ErrorCode_BadHttpStatusInRest:
+        return "This HTTP status is not allowed in a REST API";
+
+      case ErrorCode_RegularFileExpected:
+        return "The specified path does not point to a regular file";
+
+      case ErrorCode_PathToExecutable:
+        return "Unable to get the path to the executable";
+
+      case ErrorCode_MakeDirectory:
+        return "Cannot create a directory";
+
+      case ErrorCode_BadApplicationEntityTitle:
+        return "An application entity title (AET) cannot be empty or be longer than 16 characters";
+
+      case ErrorCode_NoCFindHandler:
+        return "No request handler factory for DICOM C-FIND SCP";
+
+      case ErrorCode_NoCMoveHandler:
+        return "No request handler factory for DICOM C-MOVE SCP";
+
+      case ErrorCode_NoCStoreHandler:
+        return "No request handler factory for DICOM C-STORE SCP";
+
+      case ErrorCode_NoApplicationEntityFilter:
+        return "No application entity filter";
+
+      case ErrorCode_NoSopClassOrInstance:
+        return "DicomUserConnection: Unable to find the SOP class and instance";
+
+      case ErrorCode_NoPresentationContext:
+        return "DicomUserConnection: No acceptable presentation context for modality";
+
+      case ErrorCode_DicomFindUnavailable:
+        return "DicomUserConnection: The C-FIND command is not supported by the remote SCP";
+
+      case ErrorCode_DicomMoveUnavailable:
+        return "DicomUserConnection: The C-MOVE command is not supported by the remote SCP";
+
+      case ErrorCode_CannotStoreInstance:
+        return "Cannot store an instance";
+
+      case ErrorCode_CreateDicomNotString:
+        return "Only string values are supported when creating DICOM instances";
+
+      case ErrorCode_CreateDicomOverrideTag:
+        return "Trying to override a value inherited from a parent module";
+
+      case ErrorCode_CreateDicomUseContent:
+        return "Use \"Content\" to inject an image into a new DICOM instance";
+
+      case ErrorCode_CreateDicomNoPayload:
+        return "No payload is present for one instance in the series";
+
+      case ErrorCode_CreateDicomUseDataUriScheme:
+        return "The payload of the DICOM instance must be specified according to Data URI scheme";
+
+      case ErrorCode_CreateDicomBadParent:
+        return "Trying to attach a new DICOM instance to an inexistent resource";
+
+      case ErrorCode_CreateDicomParentIsInstance:
+        return "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)";
+
+      case ErrorCode_CreateDicomParentEncoding:
+        return "Unable to get the encoding of the parent resource";
+
+      case ErrorCode_UnknownModality:
+        return "Unknown modality";
+
+      case ErrorCode_BadJobOrdering:
+        return "Bad ordering of filters in a job";
+
+      case ErrorCode_JsonToLuaTable:
+        return "Cannot convert the given JSON object to a Lua table";
+
+      case ErrorCode_CannotCreateLua:
+        return "Cannot create the Lua context";
+
+      case ErrorCode_CannotExecuteLua:
+        return "Cannot execute a Lua command";
+
+      case ErrorCode_LuaAlreadyExecuted:
+        return "Arguments cannot be pushed after the Lua function is executed";
+
+      case ErrorCode_LuaBadOutput:
+        return "The Lua function does not give the expected number of outputs";
+
+      case ErrorCode_NotLuaPredicate:
+        return "The Lua function is not a predicate (only true/false outputs allowed)";
+
+      case ErrorCode_LuaReturnsNoString:
+        return "The Lua function does not return a string";
+
+      case ErrorCode_StorageAreaAlreadyRegistered:
+        return "Another plugin has already registered a custom storage area";
+
+      case ErrorCode_DatabaseBackendAlreadyRegistered:
+        return "Another plugin has already registered a custom database back-end";
+
+      case ErrorCode_DatabaseNotInitialized:
+        return "Plugin trying to call the database during its initialization";
+
+      case ErrorCode_SslDisabled:
+        return "Orthanc has been built without SSL support";
+
+      case ErrorCode_CannotOrderSlices:
+        return "Unable to order the slices of the series";
+
+      case ErrorCode_NoWorklistHandler:
+        return "No request handler factory for DICOM C-Find Modality SCP";
+
+      case ErrorCode_AlreadyExistingTag:
+        return "Cannot override the value of a tag that already exists";
+
+      case ErrorCode_NoStorageCommitmentHandler:
+        return "No request handler factory for DICOM N-ACTION SCP (storage commitment)";
+
+      case ErrorCode_NoCGetHandler:
+        return "No request handler factory for DICOM C-GET SCP";
+
+      case ErrorCode_DicomGetUnavailable:
+        return "DicomUserConnection: The C-GET command is not supported by the remote SCP";
+
+      case ErrorCode_UnsupportedMediaType:
+        return "Unsupported media type";
+
+      default:
+        if (error >= ErrorCode_START_PLUGINS)
+        {
+          return "Error encountered within some plugin";
+        }
+        else
+        {
+          return "Unknown error code";
+        }
+    }
+  }
+
+
+  const char* EnumerationToString(HttpMethod method)
+  {
+    switch (method)
+    {
+      case HttpMethod_Get:
+        return "GET";
+
+      case HttpMethod_Post:
+        return "POST";
+
+      case HttpMethod_Delete:
+        return "DELETE";
+
+      case HttpMethod_Put:
+        return "PUT";
+
+      default:
+        return "?";
+    }
+  }
+
+
+  const char* EnumerationToString(HttpStatus status)
+  {
+    switch (status)
+    {
+    case HttpStatus_100_Continue:
+      return "Continue";
+
+    case HttpStatus_101_SwitchingProtocols:
+      return "Switching Protocols";
+
+    case HttpStatus_102_Processing:
+      return "Processing";
+
+    case HttpStatus_200_Ok:
+      return "OK";
+
+    case HttpStatus_201_Created:
+      return "Created";
+
+    case HttpStatus_202_Accepted:
+      return "Accepted";
+
+    case HttpStatus_203_NonAuthoritativeInformation:
+      return "Non-Authoritative Information";
+
+    case HttpStatus_204_NoContent:
+      return "No Content";
+
+    case HttpStatus_205_ResetContent:
+      return "Reset Content";
+
+    case HttpStatus_206_PartialContent:
+      return "Partial Content";
+
+    case HttpStatus_207_MultiStatus:
+      return "Multi-Status";
+
+    case HttpStatus_208_AlreadyReported:
+      return "Already Reported";
+
+    case HttpStatus_226_IMUsed:
+      return "IM Used";
+
+    case HttpStatus_300_MultipleChoices:
+      return "Multiple Choices";
+
+    case HttpStatus_301_MovedPermanently:
+      return "Moved Permanently";
+
+    case HttpStatus_302_Found:
+      return "Found";
+
+    case HttpStatus_303_SeeOther:
+      return "See Other";
+
+    case HttpStatus_304_NotModified:
+      return "Not Modified";
+
+    case HttpStatus_305_UseProxy:
+      return "Use Proxy";
+
+    case HttpStatus_307_TemporaryRedirect:
+      return "Temporary Redirect";
+
+    case HttpStatus_400_BadRequest:
+      return "Bad Request";
+
+    case HttpStatus_401_Unauthorized:
+      return "Unauthorized";
+
+    case HttpStatus_402_PaymentRequired:
+      return "Payment Required";
+
+    case HttpStatus_403_Forbidden:
+      return "Forbidden";
+
+    case HttpStatus_404_NotFound:
+      return "Not Found";
+
+    case HttpStatus_405_MethodNotAllowed:
+      return "Method Not Allowed";
+
+    case HttpStatus_406_NotAcceptable:
+      return "Not Acceptable";
+
+    case HttpStatus_407_ProxyAuthenticationRequired:
+      return "Proxy Authentication Required";
+
+    case HttpStatus_408_RequestTimeout:
+      return "Request Timeout";
+
+    case HttpStatus_409_Conflict:
+      return "Conflict";
+
+    case HttpStatus_410_Gone:
+      return "Gone";
+
+    case HttpStatus_411_LengthRequired:
+      return "Length Required";
+
+    case HttpStatus_412_PreconditionFailed:
+      return "Precondition Failed";
+
+    case HttpStatus_413_RequestEntityTooLarge:
+      return "Request Entity Too Large";
+
+    case HttpStatus_414_RequestUriTooLong:
+      return "Request-URI Too Long";
+
+    case HttpStatus_415_UnsupportedMediaType:
+      return "Unsupported Media Type";
+
+    case HttpStatus_416_RequestedRangeNotSatisfiable:
+      return "Requested Range Not Satisfiable";
+
+    case HttpStatus_417_ExpectationFailed:
+      return "Expectation Failed";
+
+    case HttpStatus_422_UnprocessableEntity:
+      return "Unprocessable Entity";
+
+    case HttpStatus_423_Locked:
+      return "Locked";
+
+    case HttpStatus_424_FailedDependency:
+      return "Failed Dependency";
+
+    case HttpStatus_426_UpgradeRequired:
+      return "Upgrade Required";
+
+    case HttpStatus_500_InternalServerError:
+      return "Internal Server Error";
+
+    case HttpStatus_501_NotImplemented:
+      return "Not Implemented";
+
+    case HttpStatus_502_BadGateway:
+      return "Bad Gateway";
+
+    case HttpStatus_503_ServiceUnavailable:
+      return "Service Unavailable";
+
+    case HttpStatus_504_GatewayTimeout:
+      return "Gateway Timeout";
+
+    case HttpStatus_505_HttpVersionNotSupported:
+      return "HTTP Version Not Supported";
+
+    case HttpStatus_506_VariantAlsoNegotiates:
+      return "Variant Also Negotiates";
+
+    case HttpStatus_507_InsufficientStorage:
+      return "Insufficient Storage";
+
+    case HttpStatus_509_BandwidthLimitExceeded:
+      return "Bandwidth Limit Exceeded";
+
+    case HttpStatus_510_NotExtended:
+      return "Not Extended";
+
+    default:
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return "Patient";
+
+      case ResourceType_Study:
+        return "Study";
+
+      case ResourceType_Series:
+        return "Series";
+
+      case ResourceType_Instance:
+        return "Instance";
+      
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ImageFormat format)
+  {
+    switch (format)
+    {
+      case ImageFormat_Png:
+        return "Png";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(Encoding encoding)
+  {
+    switch (encoding)
+    {
+      case Encoding_Ascii:
+        return "Ascii";
+
+      case Encoding_Utf8:
+        return "Utf8";
+
+      case Encoding_Latin1:
+        return "Latin1";
+
+      case Encoding_Latin2:
+        return "Latin2";
+
+      case Encoding_Latin3:
+        return "Latin3";
+
+      case Encoding_Latin4:
+        return "Latin4";
+
+      case Encoding_Latin5:
+        return "Latin5";
+
+      case Encoding_Cyrillic:
+        return "Cyrillic";
+
+      case Encoding_Windows1251:
+        return "Windows1251";
+
+      case Encoding_Arabic:
+        return "Arabic";
+
+      case Encoding_Greek:
+        return "Greek";
+
+      case Encoding_Hebrew:
+        return "Hebrew";
+
+      case Encoding_Thai:
+        return "Thai";
+
+      case Encoding_Japanese:
+        return "Japanese";
+
+      case Encoding_Chinese:
+        return "Chinese";
+
+      case Encoding_Korean:
+        return "Korean";
+
+      case Encoding_JapaneseKanji:
+        return "JapaneseKanji";
+
+      case Encoding_SimplifiedChinese:
+        return "SimplifiedChinese";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(PhotometricInterpretation photometric)
+  {
+    switch (photometric)
+    {
+      case PhotometricInterpretation_RGB:
+        return "RGB";
+
+      case PhotometricInterpretation_Monochrome1:
+        return "MONOCHROME1";
+
+      case PhotometricInterpretation_Monochrome2:
+        return "MONOCHROME2";
+
+      case PhotometricInterpretation_ARGB:
+        return "ARGB";
+
+      case PhotometricInterpretation_CMYK:
+        return "CMYK";
+
+      case PhotometricInterpretation_HSV:
+        return "HSV";
+
+      case PhotometricInterpretation_Palette:
+        return "PALETTE COLOR";
+
+      case PhotometricInterpretation_YBRFull:
+        return "YBR_FULL";
+
+      case PhotometricInterpretation_YBRFull422:
+        return "YBR_FULL_422";
+
+      case PhotometricInterpretation_YBRPartial420:
+        return "YBR_PARTIAL_420"; 
+
+      case PhotometricInterpretation_YBRPartial422:
+        return "YBR_PARTIAL_422"; 
+
+      case PhotometricInterpretation_YBR_ICT:
+        return "YBR_ICT"; 
+
+      case PhotometricInterpretation_YBR_RCT:
+        return "YBR_RCT"; 
+
+      case PhotometricInterpretation_Unknown:
+        return "Unknown";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(RequestOrigin origin)
+  {
+    switch (origin)
+    {
+      case RequestOrigin_Unknown:
+        return "Unknown";
+
+      case RequestOrigin_DicomProtocol:
+        return "DicomProtocol";
+
+      case RequestOrigin_RestApi:
+        return "RestApi";
+
+      case RequestOrigin_Plugins:
+        return "Plugins";
+
+      case RequestOrigin_Lua:
+        return "Lua";
+
+      case RequestOrigin_WebDav:
+        return "WebDav";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(PixelFormat format)
+  {
+    switch (format)
+    {
+      case PixelFormat_RGB24:
+        return "RGB24";
+
+      case PixelFormat_RGBA32:
+        return "RGBA32";
+
+      case PixelFormat_BGRA32:
+        return "BGRA32";
+
+      case PixelFormat_Grayscale8:
+        return "Grayscale (unsigned 8bpp)";
+
+      case PixelFormat_Grayscale16:
+        return "Grayscale (unsigned 16bpp)";
+
+      case PixelFormat_SignedGrayscale16:
+        return "Grayscale (signed 16bpp)";
+
+      case PixelFormat_Float32:
+        return "Grayscale (float 32bpp)";
+
+      case PixelFormat_Grayscale32:
+        return "Grayscale (unsigned 32bpp)";
+
+      case PixelFormat_Grayscale64:
+        return "Grayscale (unsigned 64bpp)";
+
+      case PixelFormat_RGB48:
+        return "RGB48";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ModalityManufacturer manufacturer)
+  {
+    switch (manufacturer)
+    {
+      case ModalityManufacturer_Generic:
+        return "Generic";
+
+      case ModalityManufacturer_GenericNoWildcardInDates:
+        return "GenericNoWildcardInDates";
+
+      case ModalityManufacturer_GenericNoUniversalWildcard:
+        return "GenericNoUniversalWildcard";
+
+      case ModalityManufacturer_Vitrea:
+        return "Vitrea";
+      
+      case ModalityManufacturer_GE:
+        return "GE";
+      
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(DicomRequestType type)
+  {
+    switch (type)
+    {
+      case DicomRequestType_Echo:
+        return "Echo";
+        break;
+
+      case DicomRequestType_Find:
+        return "Find";
+        break;
+
+      case DicomRequestType_FindWorklist:
+        return "FindWorklist";
+        break;
+
+      case DicomRequestType_Get:
+        return "Get";
+        break;
+
+      case DicomRequestType_Move:
+        return "Move";
+        break;
+
+      case DicomRequestType_Store:
+        return "Store";
+        break;
+
+      case DicomRequestType_NAction:
+        return "N-ACTION";
+        break;
+
+      case DicomRequestType_NEventReport:
+        return "N-EVENT-REPORT";
+        break;
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(DicomVersion version)
+  {
+    switch (version)
+    {
+      case DicomVersion_2008:
+        return "2008";
+
+      case DicomVersion_2017c:
+        return "2017c";
+
+      case DicomVersion_2021b:
+        return "2021b";
+
+      case DicomVersion_2023b:
+        return "2023b";
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ValueRepresentation vr)
+  {
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:     // AE
+        return "AE";
+
+      case ValueRepresentation_AgeString:             // AS
+        return "AS";
+
+      case ValueRepresentation_AttributeTag:          // AT (2 x uint16_t)
+        return "AT";
+
+      case ValueRepresentation_CodeString:            // CS
+        return "CS";
+
+      case ValueRepresentation_Date:                  // DA
+        return "DA";
+
+      case ValueRepresentation_DecimalString:         // DS
+        return "DS";
+
+      case ValueRepresentation_DateTime:              // DT
+        return "DT";
+
+      case ValueRepresentation_FloatingPointSingle:   // FL (float)
+        return "FL";
+
+      case ValueRepresentation_FloatingPointDouble:   // FD (double)
+        return "FD";
+
+      case ValueRepresentation_IntegerString:         // IS
+        return "IS";
+
+      case ValueRepresentation_LongString:            // LO
+        return "LO";
+
+      case ValueRepresentation_LongText:              // LT
+        return "LT";
+
+      case ValueRepresentation_OtherByte:             // OB
+        return "OB";
+
+      case ValueRepresentation_OtherDouble:           // OD
+        return "OD";
+
+      case ValueRepresentation_OtherFloat:            // OF
+        return "OF";
+
+      case ValueRepresentation_OtherLong:             // OL
+        return "OL";
+
+      case ValueRepresentation_OtherWord:             // OW
+        return "OW";
+
+      case ValueRepresentation_PersonName:            // PN
+        return "PN";
+
+      case ValueRepresentation_ShortString:           // SH
+        return "SH";
+
+      case ValueRepresentation_SignedLong:            // SL (int32_t)
+        return "SL";
+
+      case ValueRepresentation_Sequence:              // SQ
+        return "SQ";
+
+      case ValueRepresentation_SignedShort:           // SS (int16_t)
+        return "SS";
+
+      case ValueRepresentation_ShortText:             // ST
+        return "ST";
+
+      case ValueRepresentation_Time:                  // TM
+        return "TM";
+
+      case ValueRepresentation_UnlimitedCharacters:   // UC
+        return "UC";
+
+      case ValueRepresentation_UniqueIdentifier:      // UI (UID)
+        return "UI";
+
+      case ValueRepresentation_UnsignedLong:          // UL (uint32_t)
+        return "UL";
+
+      case ValueRepresentation_Unknown:               // UN
+        return "UN";
+
+      case ValueRepresentation_UniversalResource:     // UR (URI or URL)
+        return "UR";
+
+      case ValueRepresentation_UnsignedShort:         // US (uint16_t)
+        return "US";
+
+      case ValueRepresentation_UnlimitedText:         // UT
+        return "UT";
+
+      case ValueRepresentation_NotSupported:
+        return "Not supported";
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(JobState state)
+  {
+    switch (state)
+    {
+      case JobState_Pending:
+        return "Pending";
+        
+      case JobState_Running:
+        return "Running";
+        
+      case JobState_Success:
+        return "Success";
+        
+      case JobState_Failure:
+        return "Failure";
+        
+      case JobState_Paused:
+        return "Paused";
+        
+      case JobState_Retry:
+        return "Retry";
+        
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(MimeType mime)
+  {
+    switch (mime)
+    {
+      case MimeType_Binary:
+        return MIME_BINARY;
+        
+      case MimeType_Dicom:
+        return MIME_DICOM;
+        
+      case MimeType_Jpeg:
+        return MIME_JPEG;
+        
+      case MimeType_Jpeg2000:
+        return MIME_JPEG2000;
+        
+      case MimeType_Json:
+        return MIME_JSON;
+        
+      case MimeType_Pdf:
+        return MIME_PDF;
+        
+      case MimeType_Png:
+        return MIME_PNG;
+        
+      case MimeType_Xml:
+        return MIME_XML;
+        
+      case MimeType_PlainText:
+        return MIME_PLAIN_TEXT;
+                
+      case MimeType_Pam:
+        return MIME_PAM;
+                
+      case MimeType_Html:
+        return MIME_HTML;
+                
+      case MimeType_Gzip:
+        return MIME_GZIP;
+                
+      case MimeType_JavaScript:
+        return MIME_JAVASCRIPT;
+                
+      case MimeType_Css:
+        return MIME_CSS;
+                
+      case MimeType_WebAssembly:
+        return MIME_WEB_ASSEMBLY;
+                
+      case MimeType_Gif:
+        return MIME_GIF;
+                
+      case MimeType_Zip:
+        return MIME_ZIP;
+                
+      case MimeType_NaCl:
+        return MIME_NACL;
+                
+      case MimeType_PNaCl:
+        return MIME_PNACL;
+                
+      case MimeType_Svg:
+        return MIME_SVG;
+                
+      case MimeType_Woff:
+        return MIME_WOFF;
+
+      case MimeType_Woff2:
+        return MIME_WOFF2;
+
+      case MimeType_PrometheusText:
+        // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
+        return "text/plain; version=0.0.4";
+
+      case MimeType_DicomWebJson:
+        return MIME_DICOM_WEB_JSON;
+                
+      case MimeType_DicomWebXml:
+        return MIME_DICOM_WEB_XML;
+
+      case MimeType_Ico:
+        return MIME_ICO;
+
+      case MimeType_Obj:
+        return MIME_OBJ;
+
+      case MimeType_Mtl:
+        return MIME_MTL;
+
+      case MimeType_Stl:
+        return MIME_STL;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
+  const char* EnumerationToString(Endianness endianness)
+  {
+    switch (endianness)
+    {
+      case Endianness_Little:
+        return "Little-endian";
+
+      case Endianness_Big:
+        return "Big-endian";
+
+      case Endianness_Unknown:
+        return "Unknown endianness";
+                
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(StorageCommitmentFailureReason reason)
+  {
+    switch (reason)
+    {
+      case StorageCommitmentFailureReason_Success:
+        return "Success";
+
+      case StorageCommitmentFailureReason_ProcessingFailure:
+        return "A general failure in processing the operation was encountered";
+
+      case StorageCommitmentFailureReason_NoSuchObjectInstance:
+        return "One or more of the elements in the Referenced SOP "
+          "Instance Sequence was not available";
+        
+      case StorageCommitmentFailureReason_ResourceLimitation:
+        return "The SCP does not currently have enough resources to "
+          "store the requested SOP Instance(s)";
+
+      case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
+        return "Storage Commitment has been requested for a SOP Instance "
+          "with a SOP Class that is not supported by the SCP";
+
+      case StorageCommitmentFailureReason_ClassInstanceConflict:
+        return "The SOP Class of an element in the Referenced SOP Instance Sequence "
+          "did not correspond to the SOP class registered for this SOP Instance at the SCP";
+
+      case StorageCommitmentFailureReason_DuplicateTransactionUID:
+        return "The Transaction UID of the Storage Commitment Request is already in use";
+
+      default:
+        return "Unknown failure reason";
+    }
+  }
+
+
+  const char* EnumerationToString(DicomToJsonFormat format)
+  {
+    switch (format)
+    {
+      case DicomToJsonFormat_Full:
+        return "Full";
+
+      case DicomToJsonFormat_Human:
+        return "Simplify";
+
+      case DicomToJsonFormat_Short:
+        return "Short";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  Encoding StringToEncoding(const char* encoding)
+  {
+    std::string s(encoding);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "UTF8")
+    {
+      return Encoding_Utf8;
+    }
+
+    if (s == "ASCII")
+    {
+      return Encoding_Ascii;
+    }
+
+    if (s == "LATIN1")
+    {
+      return Encoding_Latin1;
+    }
+
+    if (s == "LATIN2")
+    {
+      return Encoding_Latin2;
+    }
+
+    if (s == "LATIN3")
+    {
+      return Encoding_Latin3;
+    }
+
+    if (s == "LATIN4")
+    {
+      return Encoding_Latin4;
+    }
+
+    if (s == "LATIN5")
+    {
+      return Encoding_Latin5;
+    }
+
+    if (s == "CYRILLIC")
+    {
+      return Encoding_Cyrillic;
+    }
+
+    if (s == "WINDOWS1251")
+    {
+      return Encoding_Windows1251;
+    }
+
+    if (s == "ARABIC")
+    {
+      return Encoding_Arabic;
+    }
+
+    if (s == "GREEK")
+    {
+      return Encoding_Greek;
+    }
+
+    if (s == "HEBREW")
+    {
+      return Encoding_Hebrew;
+    }
+
+    if (s == "THAI")
+    {
+      return Encoding_Thai;
+    }
+
+    if (s == "JAPANESE")
+    {
+      return Encoding_Japanese;
+    }
+
+    if (s == "CHINESE")
+    {
+      return Encoding_Chinese;
+    }
+
+    if (s == "KOREAN")
+    {
+      return Encoding_Korean;
+    }
+
+    if (s == "JAPANESEKANJI")
+    {
+      return Encoding_JapaneseKanji;
+    }
+
+    if (s == "SIMPLIFIEDCHINESE")
+    {
+      return Encoding_SimplifiedChinese;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  ResourceType StringToResourceType(const char* type)
+  {
+    std::string s(type);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "PATIENT" || s == "PATIENTS")
+    {
+      return ResourceType_Patient;
+    }
+    else if (s == "STUDY" || s == "STUDIES")
+    {
+      return ResourceType_Study;
+    }
+    else if (s == "SERIES")
+    {
+      return ResourceType_Series;
+    }
+    else if (s == "INSTANCE"  || s == "IMAGE" || 
+             s == "INSTANCES" || s == "IMAGES")
+    {
+      return ResourceType_Instance;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange, std::string("Invalid resource type '") + type + "'");
+  }
+
+  const char* ResourceTypeToDicomQueryRetrieveLevel(ResourceType type)
+  {
+    if (type == ResourceType_Patient)
+    {
+      return "PATIENT";
+    }
+    else if (type == ResourceType_Study)
+    {
+      return "STUDY";
+    }
+    else if (type == ResourceType_Series)
+    {
+      return "SERIES";
+    }
+    else if (type == ResourceType_Instance)
+    {
+      return "IMAGE";
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+  ImageFormat StringToImageFormat(const char* format)
+  {
+    std::string s(format);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "PNG")
+    {
+      return ImageFormat_Png;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  ValueRepresentation StringToValueRepresentation(const std::string& vr,
+                                                  bool throwIfUnsupported)
+  {
+    if (vr == "AE")
+    {
+      return ValueRepresentation_ApplicationEntity;
+    }
+    else if (vr == "AS")
+    {
+      return ValueRepresentation_AgeString;
+    }
+    else if (vr == "AT")
+    {
+      return ValueRepresentation_AttributeTag;
+    }
+    else if (vr == "CS")
+    {
+      return ValueRepresentation_CodeString;
+    }
+    else if (vr == "DA")
+    {
+      return ValueRepresentation_Date;
+    }
+    else if (vr == "DS")
+    {
+      return ValueRepresentation_DecimalString;
+    }
+    else if (vr == "DT")
+    {
+      return ValueRepresentation_DateTime;
+    }
+    else if (vr == "FL")
+    {
+      return ValueRepresentation_FloatingPointSingle;
+    }
+    else if (vr == "FD")
+    {
+      return ValueRepresentation_FloatingPointDouble;
+    }
+    else if (vr == "IS")
+    {
+      return ValueRepresentation_IntegerString;
+    }
+    else if (vr == "LO")
+    {
+      return ValueRepresentation_LongString;
+    }
+    else if (vr == "LT")
+    {
+      return ValueRepresentation_LongText;
+    }
+    else if (vr == "OB")
+    {
+      return ValueRepresentation_OtherByte;
+    }
+    else if (vr == "OD")
+    {
+      return ValueRepresentation_OtherDouble;
+    }
+    else if (vr == "OF")
+    {
+      return ValueRepresentation_OtherFloat;
+    }
+    else if (vr == "OL")
+    {
+      return ValueRepresentation_OtherLong;
+    }
+    else if (vr == "OW")
+    {
+      return ValueRepresentation_OtherWord;
+    }
+    else if (vr == "PN")
+    {
+      return ValueRepresentation_PersonName;
+    }
+    else if (vr == "SH")
+    {
+      return ValueRepresentation_ShortString;
+    }
+    else if (vr == "SL")
+    {
+      return ValueRepresentation_SignedLong;
+    }
+    else if (vr == "SQ")
+    {
+      return ValueRepresentation_Sequence;
+    }
+    else if (vr == "SS")
+    {
+      return ValueRepresentation_SignedShort;
+    }
+    else if (vr == "ST")
+    {
+      return ValueRepresentation_ShortText;
+    }
+    else if (vr == "TM")
+    {
+      return ValueRepresentation_Time;
+    }
+    else if (vr == "UC")
+    {
+      return ValueRepresentation_UnlimitedCharacters;
+    }
+    else if (vr == "UI")
+    {
+      return ValueRepresentation_UniqueIdentifier;
+    }
+    else if (vr == "UL")
+    {
+      return ValueRepresentation_UnsignedLong;
+    }
+    else if (vr == "UN")
+    {
+      return ValueRepresentation_Unknown;
+    }
+    else if (vr == "UR")
+    {
+      return ValueRepresentation_UniversalResource;
+    }
+    else if (vr == "US")
+    {
+      return ValueRepresentation_UnsignedShort;
+    }
+    else if (vr == "UT")
+    {
+      return ValueRepresentation_UnlimitedText;
+    }
+    else
+    {
+      std::string s = "Unsupported value representation encountered: " + vr;
+
+      if (throwIfUnsupported)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, s);
+      }
+      else
+      {
+        LOG(INFO) << s;
+        return ValueRepresentation_NotSupported;
+      }
+    }
+  }
+
+
+  PhotometricInterpretation StringToPhotometricInterpretation(const char* value)
+  {
+    // http://dicom.nema.org/medical/dicom/2017a/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2
+    std::string s(value);
+
+    if (s == "MONOCHROME1")
+    {
+      return PhotometricInterpretation_Monochrome1;
+    }
+    
+    if (s == "MONOCHROME2")
+    {
+      return PhotometricInterpretation_Monochrome2;
+    }
+
+    if (s == "PALETTE COLOR")
+    {
+      return PhotometricInterpretation_Palette;
+    }
+    
+    if (s == "RGB")
+    {
+      return PhotometricInterpretation_RGB;
+    }
+    
+    if (s == "HSV")
+    {
+      return PhotometricInterpretation_HSV;
+    }
+    
+    if (s == "ARGB")
+    {
+      return PhotometricInterpretation_ARGB;
+    }    
+
+    if (s == "CMYK")
+    {
+      return PhotometricInterpretation_CMYK;
+    }    
+
+    if (s == "YBR_FULL")
+    {
+      return PhotometricInterpretation_YBRFull;
+    }
+    
+    if (s == "YBR_FULL_422")
+    {
+      return PhotometricInterpretation_YBRFull422;
+    }
+    
+    if (s == "YBR_PARTIAL_422")
+    {
+      return PhotometricInterpretation_YBRPartial422;
+    }
+    
+    if (s == "YBR_PARTIAL_420")
+    {
+      return PhotometricInterpretation_YBRPartial420;
+    }
+    
+    if (s == "YBR_ICT")
+    {
+      return PhotometricInterpretation_YBR_ICT;
+    }
+    
+    if (s == "YBR_RCT")
+    {
+      return PhotometricInterpretation_YBR_RCT;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+  
+
+  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer)
+  {
+    ModalityManufacturer result;
+    bool obsolete = false;
+    
+    if (manufacturer == "Generic")
+    {
+      return ModalityManufacturer_Generic;
+    }
+    else if (manufacturer == "GenericNoWildcardInDates")
+    {
+      return ModalityManufacturer_GenericNoWildcardInDates;
+    }
+    else if (manufacturer == "GenericNoUniversalWildcard")
+    {
+      return ModalityManufacturer_GenericNoUniversalWildcard;
+    }
+    else if (manufacturer == "Vitrea")
+    {
+      return ModalityManufacturer_Vitrea;
+    }
+    else if (manufacturer == "GE")
+    {
+      return ModalityManufacturer_GE;
+    }
+    else if (manufacturer == "AgfaImpax" ||
+             manufacturer == "SyngoVia")
+    {
+      result = ModalityManufacturer_GenericNoWildcardInDates;
+      obsolete = true;
+    }
+    else if (manufacturer == "EFilm2" ||
+             manufacturer == "MedInria" ||
+             manufacturer == "ClearCanvas" ||
+             manufacturer == "Dcm4Chee"
+             )
+    {
+      result = ModalityManufacturer_Generic;
+      obsolete = true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unknown modality manufacturer: \"" + manufacturer + "\"");
+    }
+
+    if (obsolete)
+    {
+      LOG(WARNING) << "The \"" << manufacturer << "\" manufacturer is now obsolete. "
+                   << "To guarantee compatibility with future Orthanc "
+                   << "releases, you should replace it by \""
+                   << EnumerationToString(result)
+                   << "\" in your configuration file.";
+    }
+
+    return result;
+  }
+
+
+  DicomVersion StringToDicomVersion(const std::string& version)
+  {
+    if (version == "2008")
+    {
+      return DicomVersion_2008;
+    }
+    else if (version == "2017c")
+    {
+      return DicomVersion_2017c;
+    }
+    else if (version == "2021b")
+    {
+      return DicomVersion_2021b;
+    }
+    else if (version == "2023b")
+    {
+      return DicomVersion_2023b;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unknown specific version of the DICOM standard: " + version);
+    }
+  }
+
+
+  JobState StringToJobState(const std::string& state)
+  {
+    if (state == "Pending")
+    {
+      return JobState_Pending;
+    }
+    else if (state == "Running")
+    {
+      return JobState_Running;
+    }
+    else if (state == "Success")
+    {
+      return JobState_Success;
+    }
+    else if (state == "Failure")
+    {
+      return JobState_Failure;
+    }
+    else if (state == "Paused")
+    {
+      return JobState_Paused;
+    }
+    else if (state == "Retry")
+    {
+      return JobState_Retry;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  RequestOrigin StringToRequestOrigin(const std::string& origin)
+  {
+    if (origin == "Unknown")
+    {
+      return RequestOrigin_Unknown;
+    }
+    else if (origin == "DicomProtocol")
+    {
+      return RequestOrigin_DicomProtocol;
+    }
+    else if (origin == "RestApi")
+    {
+      return RequestOrigin_RestApi;
+    }
+    else if (origin == "Plugins")
+    {
+      return RequestOrigin_Plugins;
+    }
+    else if (origin == "Lua")
+    {
+      return RequestOrigin_Lua;
+    }
+    else if (origin == "WebDav")
+    {
+      return RequestOrigin_WebDav;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool LookupMimeType(MimeType& target,
+                      const std::string& source)
+  {
+    if (source == MIME_BINARY)
+    {
+      target = MimeType_Binary;
+      return true;
+    }
+    else if (source == MIME_DICOM)
+    {
+      target = MimeType_Dicom;
+      return true;
+    }
+    else if (source == MIME_JPEG ||
+             source == "image/jpg")
+    {
+      // Note the tolerance for "image/jpg", which is *not* a standard MIME type
+      // https://groups.google.com/g/orthanc-users/c/Y5x37UFKiDg/m/1zI260KTAwAJ
+      // https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
+      target = MimeType_Jpeg;
+      return true;
+    }
+    else if (source == MIME_JPEG2000)
+    {
+      target = MimeType_Jpeg2000;
+      return true;
+    }
+    else if (source == MIME_JSON)
+    {
+      target = MimeType_Json;
+      return true;
+    }
+    else if (source == MIME_PDF)
+    {
+      target = MimeType_Pdf;
+      return true;
+    }
+    else if (source == MIME_PNG)
+    {
+      target = MimeType_Png;
+      return true;
+    }
+    else if (source == MIME_XML ||
+             source == MIME_XML_2)
+    {
+      target = MimeType_Xml;
+      return true;
+    }
+    else if (source == MIME_PLAIN_TEXT)
+    {
+      target = MimeType_PlainText;
+      return true;
+    }
+    else if (source == MIME_PAM)
+    {
+      target = MimeType_Pam;
+      return true;
+    }
+    else if (source == MIME_HTML)
+    {
+      target = MimeType_Html;
+      return true;
+    }
+    else if (source == MIME_GZIP)
+    {
+      target = MimeType_Gzip;
+      return true;
+    }
+    else if (source == MIME_JAVASCRIPT)
+    {
+      target = MimeType_JavaScript;
+      return true;
+    }
+    else if (source == MIME_CSS)
+    {
+      target = MimeType_Css;
+      return true;
+    }
+    else if (source == MIME_WEB_ASSEMBLY)
+    {
+      target = MimeType_WebAssembly;
+      return true;
+    }
+    else if (source == MIME_GIF)
+    {
+      target = MimeType_Gif;
+      return true;
+    }
+    else if (source == MIME_ZIP)
+    {
+      target = MimeType_Zip;
+      return true;
+    }
+    else if (source == MIME_NACL)
+    {
+      target = MimeType_NaCl;
+      return true;
+    }
+    else if (source == MIME_PNACL)
+    {
+      target = MimeType_PNaCl;
+      return true;
+    }
+    else if (source == MIME_SVG)
+    {
+      target = MimeType_Svg;
+      return true;
+    }
+    else if (source == MIME_WOFF)
+    {
+      target = MimeType_Woff;
+      return true;
+    }
+    else if (source == MIME_WOFF2)
+    {
+      target = MimeType_Woff2;
+      return true;
+    }
+    else if (source == MIME_DICOM_WEB_JSON)
+    {
+      target = MimeType_DicomWebJson;
+      return true;
+    }
+    else if (source == MIME_DICOM_WEB_XML)
+    {
+      target = MimeType_DicomWebXml;
+      return true;
+    }
+    else if (source == MIME_ICO)
+    {
+      target = MimeType_Ico;
+      return true;
+    }
+    else if (source == MIME_OBJ)
+    {
+      target = MimeType_Obj;
+      return true;
+    }
+    else if (source == MIME_MTL)
+    {
+      target = MimeType_Mtl;
+      return true;
+    }
+    else if (source == MIME_STL)
+    {
+      target = MimeType_Stl;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  
+  MimeType StringToMimeType(const std::string& mime)
+  {
+    MimeType result;
+    if (LookupMimeType(result, mime))
+    {
+      return result;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
+  DicomToJsonFormat StringToDicomToJsonFormat(const std::string& format)
+  {
+    if (format == "Full")
+    {
+      return DicomToJsonFormat_Full;
+    }
+    else if (format == "Short")
+    {
+      return DicomToJsonFormat_Short;
+    }
+    else if (format == "Simplify")
+    {
+      return DicomToJsonFormat_Human;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  unsigned int GetBytesPerPixel(PixelFormat format)
+  {
+    switch (format)
+    {
+      case PixelFormat_Grayscale8:
+        return 1;
+
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+        return 2;
+
+      case PixelFormat_RGB24:
+        return 3;
+
+      case PixelFormat_RGBA32:
+      case PixelFormat_BGRA32:
+      case PixelFormat_Grayscale32:
+        return 4;
+
+      case PixelFormat_Float32:
+        assert(sizeof(float) == 4);
+        return 4;
+
+      case PixelFormat_RGB48:
+        return 6;
+
+      case PixelFormat_Grayscale64:
+      case PixelFormat_RGBA64:
+        return 8;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool GetDicomEncoding(Encoding& encoding,
+                        const char* specificCharacterSet)
+  {
+    std::string s = Toolbox::StripSpaces(specificCharacterSet);
+    Toolbox::ToUpperCase(s);
+
+    // handle common spelling mistakes
+    boost::replace_all(s, "ISO_IR_", "ISO_IR ");
+    boost::replace_all(s, "ISO_2022_IR_", "ISO 2022 IR ");
+
+
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
+    // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-core/src/main/java/org/dcm4che3/data/SpecificCharacterSet.java
+    if (s == "ISO_IR 6" ||
+        s == "ISO 2022 IR 6")
+    {
+      encoding = Encoding_Ascii;
+    }
+    else if (s == "ISO_IR 192")
+    {
+      encoding = Encoding_Utf8;
+    }
+    else if (s == "ISO_IR 100" ||
+             s == "ISO 2022 IR 100")
+    {
+      encoding = Encoding_Latin1;
+    }
+    else if (s == "ISO_IR 101" ||
+             s == "ISO 2022 IR 101")
+    {
+      encoding = Encoding_Latin2;
+    }
+    else if (s == "ISO_IR 109" ||
+             s == "ISO 2022 IR 109")
+    {
+      encoding = Encoding_Latin3;
+    }
+    else if (s == "ISO_IR 110" ||
+             s == "ISO 2022 IR 110")
+    {
+      encoding = Encoding_Latin4;
+    }
+    else if (s == "ISO_IR 148" ||
+             s == "ISO 2022 IR 148")
+    {
+      encoding = Encoding_Latin5;
+    }
+    else if (s == "ISO_IR 144" ||
+             s == "ISO 2022 IR 144")
+    {
+      encoding = Encoding_Cyrillic;
+    }
+    else if (s == "ISO_IR 127" ||
+             s == "ISO 2022 IR 127")
+    {
+      encoding = Encoding_Arabic;
+    }
+    else if (s == "ISO_IR 126" ||
+             s == "ISO 2022 IR 126")
+    {
+      encoding = Encoding_Greek;
+    }
+    else if (s == "ISO_IR 138" ||
+             s == "ISO 2022 IR 138")
+    {
+      encoding = Encoding_Hebrew;
+    }
+    else if (s == "ISO_IR 166" ||
+             s == "ISO 2022 IR 166")
+    {
+      encoding = Encoding_Thai;
+    }
+    else if (s == "ISO_IR 13" ||
+             s == "ISO 2022 IR 13")
+    {
+      encoding = Encoding_Japanese;
+    }
+    else if (s == "GB18030" || s == "GBK")
+    {
+      /**
+       * According to tumashu@163.com, "In China, many dicom file's
+       * 0008,0005 tag is set as "GBK", instead of "GB18030", GBK is a
+       * subset of GB18030, and which is used frequently in China,
+       * suggest support it."
+       * https://groups.google.com/d/msg/orthanc-users/WMM8LMbjpUc/02-1f_yFCgAJ
+       **/
+      encoding = Encoding_Chinese;
+    }
+    else if (s == "ISO 2022 IR 149")
+    {
+      encoding = Encoding_Korean;
+    }
+    else if (s == "ISO 2022 IR 87")
+    {
+      encoding = Encoding_JapaneseKanji;
+    }
+    else if (s == "ISO 2022 IR 58")
+    {
+      encoding = Encoding_SimplifiedChinese;
+    }
+    /*
+      else if (s == "ISO 2022 IR 159")
+      {
+      TODO - Supplementary Kanji set
+      }
+    */
+    else
+    {
+      return false;
+    }
+
+    // The encoding was properly detected
+    return true;
+  }
+
+
+  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);
+    }
+  }
+
+
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference)
+  {
+    switch (reference)
+    {
+      case ResourceType_Patient:
+        return (level == ResourceType_Patient);
+
+      case ResourceType_Study:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study);
+
+      case ResourceType_Series:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series);
+
+      case ResourceType_Instance:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series ||
+                level == ResourceType_Instance);
+
+      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);
+    }
+  }
+
+
+
+  const char* GetDicomSpecificCharacterSet(Encoding encoding)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
+    switch (encoding)
+    {
+      case Encoding_Ascii:
+        return "ISO_IR 6";
+
+      case Encoding_Utf8:
+        return "ISO_IR 192";
+
+      case Encoding_Latin1:
+        return "ISO_IR 100";
+
+      case Encoding_Latin2:
+        return "ISO_IR 101";
+
+      case Encoding_Latin3:
+        return "ISO_IR 109";
+
+      case Encoding_Latin4:
+        return "ISO_IR 110";
+
+      case Encoding_Latin5:
+        return "ISO_IR 148";
+
+      case Encoding_Cyrillic:
+        return "ISO_IR 144";
+
+      case Encoding_Arabic:
+        return "ISO_IR 127";
+
+      case Encoding_Greek:
+        return "ISO_IR 126";
+
+      case Encoding_Hebrew:
+        return "ISO_IR 138";
+
+      case Encoding_Japanese:
+        return "ISO_IR 13";
+
+      case Encoding_Chinese:
+        return "GB18030";
+
+      case Encoding_Thai:
+        return "ISO_IR 166";
+
+      case Encoding_Korean:
+        return "ISO 2022 IR 149";
+
+      case Encoding_JapaneseKanji:
+        return "ISO 2022 IR 87";
+
+      case Encoding_SimplifiedChinese:
+        return "ISO 2022 IR 58";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  // This function is autogenerated by the script
+  // "Resources/CodeGeneration/GenerateErrorCodes.py"
+  HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error)
+  {
+    switch (error)
+    {
+      case ErrorCode_Success:
+        return HttpStatus_200_Ok;
+
+      case ErrorCode_ParameterOutOfRange:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_BadParameterType:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_InexistentItem:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_BadRequest:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_UriSyntax:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_InexistentFile:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_BadFileFormat:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_UnknownResource:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_InexistentTag:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_BadJson:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_Unauthorized:
+        return HttpStatus_401_Unauthorized;
+
+      case ErrorCode_NotAcceptable:
+        return HttpStatus_406_NotAcceptable;
+
+      case ErrorCode_DatabaseUnavailable:
+        return HttpStatus_503_ServiceUnavailable;
+
+      case ErrorCode_BadRange:
+        return HttpStatus_416_RequestedRangeNotSatisfiable;
+
+      case ErrorCode_DatabaseCannotSerialize:
+        return HttpStatus_503_ServiceUnavailable;
+
+      case ErrorCode_Revision:
+        return HttpStatus_409_Conflict;
+
+      case ErrorCode_ForbiddenAccess:
+        return HttpStatus_403_Forbidden;
+
+      case ErrorCode_DuplicateResource:
+        return HttpStatus_409_Conflict;
+
+      case ErrorCode_CreateDicomNotString:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomOverrideTag:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomUseContent:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomNoPayload:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomUseDataUriScheme:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomBadParent:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomParentIsInstance:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_UnsupportedMediaType:
+        return HttpStatus_415_UnsupportedMediaType;
+
+      default:
+        return HttpStatus_500_InternalServerError;
+    }
+  }
+
+
+  bool IsUserContentType(FileContentType type)
+  {
+    return (type >= FileContentType_StartUser &&
+            type <= FileContentType_EndUser);
+  }
+
+
+  bool IsBinaryValueRepresentation(ValueRepresentation vr)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:     // AE
+      case ValueRepresentation_AgeString:             // AS
+      case ValueRepresentation_CodeString:            // CS
+      case ValueRepresentation_Date:                  // DA
+      case ValueRepresentation_DecimalString:         // DS
+      case ValueRepresentation_DateTime:              // DT
+      case ValueRepresentation_IntegerString:         // IS
+      case ValueRepresentation_LongString:            // LO
+      case ValueRepresentation_LongText:              // LT
+      case ValueRepresentation_PersonName:            // PN
+      case ValueRepresentation_ShortString:           // SH
+      case ValueRepresentation_ShortText:             // ST
+      case ValueRepresentation_Time:                  // TM
+      case ValueRepresentation_UnlimitedCharacters:   // UC
+      case ValueRepresentation_UniqueIdentifier:      // UI (UID)
+      case ValueRepresentation_UniversalResource:     // UR (URI or URL)
+      case ValueRepresentation_UnlimitedText:         // UT
+      {
+        return false;
+      }
+
+      /**
+       * Below are all the VR whose character repertoire is tagged as
+       * "not applicable"
+       **/
+      case ValueRepresentation_AttributeTag:          // AT (2 x uint16_t)
+      case ValueRepresentation_FloatingPointSingle:   // FL (float)
+      case ValueRepresentation_FloatingPointDouble:   // FD (double)
+      case ValueRepresentation_OtherByte:             // OB
+      case ValueRepresentation_OtherDouble:           // OD
+      case ValueRepresentation_OtherFloat:            // OF
+      case ValueRepresentation_OtherLong:             // OL
+      case ValueRepresentation_OtherWord:             // OW
+      case ValueRepresentation_SignedLong:            // SL (int32_t)
+      case ValueRepresentation_Sequence:              // SQ
+      case ValueRepresentation_SignedShort:           // SS (int16_t)
+      case ValueRepresentation_UnsignedLong:          // UL (uint32_t)
+      case ValueRepresentation_Unknown:               // UN
+      case ValueRepresentation_UnsignedShort:         // US (uint16_t)
+      {
+        return true;
+      }
+
+      case ValueRepresentation_NotSupported:
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }  
+
+
+  static Mutex     defaultEncodingMutex_;  // Should not be necessary
+  static Encoding  defaultEncoding_ = ORTHANC_DEFAULT_DICOM_ENCODING;
+  
+  Encoding GetDefaultDicomEncoding()
+  {
+    Mutex::ScopedLock lock(defaultEncodingMutex_);
+    return defaultEncoding_;
+  }
+
+  void SetDefaultDicomEncoding(Encoding encoding)
+  {
+    std::string name = EnumerationToString(encoding);
+    
+    {
+      Mutex::ScopedLock lock(defaultEncodingMutex_);
+      defaultEncoding_ = encoding;
+    }
+
+    LOG(INFO) << "Default encoding for DICOM was changed to: " << name;
+  }
+
+
+  const char* GetResourceTypeText(ResourceType type,
+                                  bool isPlural,
+                                  bool isUpperCase)
+  {
+    if (isPlural && !isUpperCase)
+    {
+      switch (type)
+      {
+        case ResourceType_Patient:
+          return "patients";
+
+        case ResourceType_Study:
+          return "studies";
+
+        case ResourceType_Series:
+          return "series";
+
+        case ResourceType_Instance:
+          return "instances";
+      
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+    else if (isPlural && isUpperCase)
+    {
+      switch (type)
+      {
+        case ResourceType_Patient:
+          return "Patients";
+
+        case ResourceType_Study:
+          return "Studies";
+
+        case ResourceType_Series:
+          return "Series";
+
+        case ResourceType_Instance:
+          return "Instances";
+      
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+    else if (!isPlural && !isUpperCase)
+    {
+      switch (type)
+      {
+        case ResourceType_Patient:
+          return "patient";
+
+        case ResourceType_Study:
+          return "study";
+
+        case ResourceType_Series:
+          return "series";
+
+        case ResourceType_Instance:
+          return "instance";
+      
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+    else if (!isPlural && isUpperCase)
+    {
+      switch (type)
+      {
+        case ResourceType_Patient:
+          return "Patient";
+
+        case ResourceType_Study:
+          return "Study";
+
+        case ResourceType_Series:
+          return "Series";
+
+        case ResourceType_Instance:
+          return "Instance";
+      
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  DicomTransferSyntax GetTransferSyntax(const std::string& value)
+  {
+    DicomTransferSyntax syntax;
+    if (LookupTransferSyntax(syntax, value))
+    {
+      return syntax;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unknown transfer syntax: " + value);
+    }
+  }
+
+  RetrieveMethod StringToRetrieveMethod(const std::string& str)
+  {
+    if (str == "C-MOVE")
+    {
+      return RetrieveMethod_Move;
+    }
+    else if (str == "C-GET")
+    {
+      return RetrieveMethod_Get;
+    }
+    else if (str == "SystemDefault")
+    {
+      return RetrieveMethod_SystemDefault;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "RetrieveMethod can be \"C-MOVE\", \"C-GET\" or \"SystemDefault\": " + str);
+    }    
+  }
+
+  const char* EnumerationToString(RetrieveMethod method)
+  {
+    switch (method)
+    {
+      case RetrieveMethod_Get:
+        return "C-GET";
+
+      case RetrieveMethod_Move:
+        return "C-MOVE";
+
+      case RetrieveMethod_SystemDefault:
+        return "SystemDefault";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
+
+
+#include "./Enumerations_TransferSyntaxes.impl.h"
diff --git a/OrthancFramework/Sources/Enumerations.h b/OrthancFramework/Sources/Enumerations.h
new file mode 100644
index 0000000..00801ef
--- /dev/null
+++ b/OrthancFramework/Sources/Enumerations.h
@@ -0,0 +1,965 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "OrthancFramework.h"
+
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  static const char* const URI_SCHEME_PREFIX_BINARY = "data:application/octet-stream;base64,";
+
+  static const char* const MIME_BINARY = "application/octet-stream";
+  static const char* const MIME_JPEG = "image/jpeg";
+  static const char* const MIME_JSON = "application/json";
+  static const char* const MIME_JSON_UTF8 = "application/json; charset=utf-8";
+  static const char* const MIME_PDF = "application/pdf";
+  static const char* const MIME_PNG = "image/png";
+  static const char* const MIME_XML = "application/xml";
+  static const char* const MIME_XML_UTF8 = "application/xml; charset=utf-8";
+
+  // Added in Orthanc 1.12.1
+  static const char* const MIME_OBJ = "model/obj";
+  static const char* const MIME_MTL = "model/mtl";
+  static const char* const MIME_STL = "model/stl";
+
+  static const char* const MIME_CSS = "text/css";
+  static const char* const MIME_DICOM = "application/dicom";
+  static const char* const MIME_GIF = "image/gif";
+  static const char* const MIME_GZIP = "application/gzip";
+  static const char* const MIME_HTML = "text/html";
+  static const char* const MIME_JAVASCRIPT = "application/javascript";
+  static const char* const MIME_JPEG2000 = "image/jp2";
+  static const char* const MIME_NACL = "application/x-nacl";
+  static const char* const MIME_PLAIN_TEXT = "text/plain";
+  static const char* const MIME_PNACL = "application/x-pnacl";
+  static const char* const MIME_SVG = "image/svg+xml";
+  static const char* const MIME_WEB_ASSEMBLY = "application/wasm";
+  static const char* const MIME_WOFF = "application/x-font-woff";
+  static const char* const MIME_WOFF2 = "font/woff2";
+  static const char* const MIME_XML_2 = "text/xml";
+  static const char* const MIME_ZIP = "application/zip";
+  static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json";
+  static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml";
+  static const char* const MIME_ICO = "image/x-icon";
+
+
+  /**
+   * "No Internet Media Type (aka MIME type, content type) for PBM has
+   * been registered with IANA, but the unofficial value
+   * image/x-portable-arbitrarymap is assigned by this specification,
+   * to be consistent with conventional values for the older Netpbm
+   * formats."  http://netpbm.sourceforge.net/doc/pam.html
+   **/
+  static const char* const MIME_PAM = "image/x-portable-arbitrarymap";
+
+
+  enum MimeType
+  {
+    MimeType_Binary,
+    MimeType_Css,
+    MimeType_Dicom,
+    MimeType_Gif,
+    MimeType_Gzip,
+    MimeType_Html,
+    MimeType_JavaScript,
+    MimeType_Jpeg,
+    MimeType_Jpeg2000,
+    MimeType_Json,
+    MimeType_NaCl,
+    MimeType_PNaCl,
+    MimeType_Pam,
+    MimeType_Pdf,
+    MimeType_PlainText,
+    MimeType_Png,
+    MimeType_Svg,
+    MimeType_WebAssembly,
+    MimeType_Xml,
+    MimeType_Woff,            // Web Open Font Format
+    MimeType_Woff2,
+    MimeType_Zip,
+    MimeType_PrometheusText,  // Prometheus text-based exposition format (for metrics)
+    MimeType_DicomWebJson,
+    MimeType_DicomWebXml,
+    MimeType_Ico,
+    MimeType_Mtl,             // MTL - New in Orthanc 1.12.1
+    MimeType_Obj,             // OBJ - New in Orthanc 1.12.1
+    MimeType_Stl              // STL - New in Orthanc 1.12.1
+  };
+
+  
+  enum Endianness
+  {
+    Endianness_Unknown,
+    Endianness_Big,
+    Endianness_Little
+  };
+
+  // This enumeration is autogenerated by the script
+  // "Resources/CodeGeneration/GenerateErrorCodes.py"
+  enum ErrorCode
+  {
+    ErrorCode_InternalError = -1    /*!< Internal error */,
+    ErrorCode_Success = 0    /*!< Success */,
+    ErrorCode_Plugin = 1    /*!< Error encountered within the plugin engine */,
+    ErrorCode_NotImplemented = 2    /*!< Not implemented yet */,
+    ErrorCode_ParameterOutOfRange = 3    /*!< Parameter out of range */,
+    ErrorCode_NotEnoughMemory = 4    /*!< The server hosting Orthanc is running out of memory */,
+    ErrorCode_BadParameterType = 5    /*!< Bad type for a parameter */,
+    ErrorCode_BadSequenceOfCalls = 6    /*!< Bad sequence of calls */,
+    ErrorCode_InexistentItem = 7    /*!< Accessing an inexistent item */,
+    ErrorCode_BadRequest = 8    /*!< Bad request */,
+    ErrorCode_NetworkProtocol = 9    /*!< Error in the network protocol */,
+    ErrorCode_SystemCommand = 10    /*!< Error while calling a system command */,
+    ErrorCode_Database = 11    /*!< Error with the database engine */,
+    ErrorCode_UriSyntax = 12    /*!< Badly formatted URI */,
+    ErrorCode_InexistentFile = 13    /*!< Inexistent file */,
+    ErrorCode_CannotWriteFile = 14    /*!< Cannot write to file */,
+    ErrorCode_BadFileFormat = 15    /*!< Bad file format */,
+    ErrorCode_Timeout = 16    /*!< Timeout */,
+    ErrorCode_UnknownResource = 17    /*!< Unknown resource */,
+    ErrorCode_IncompatibleDatabaseVersion = 18    /*!< Incompatible version of the database */,
+    ErrorCode_FullStorage = 19    /*!< The file storage is full */,
+    ErrorCode_CorruptedFile = 20    /*!< Corrupted file (e.g. inconsistent MD5 hash) */,
+    ErrorCode_InexistentTag = 21    /*!< Inexistent tag */,
+    ErrorCode_ReadOnly = 22    /*!< Cannot modify a read-only data structure */,
+    ErrorCode_IncompatibleImageFormat = 23    /*!< Incompatible format of the images */,
+    ErrorCode_IncompatibleImageSize = 24    /*!< Incompatible size of the images */,
+    ErrorCode_SharedLibrary = 25    /*!< Error while using a shared library (plugin) */,
+    ErrorCode_UnknownPluginService = 26    /*!< Plugin invoking an unknown service */,
+    ErrorCode_UnknownDicomTag = 27    /*!< Unknown DICOM tag */,
+    ErrorCode_BadJson = 28    /*!< Cannot parse a JSON document */,
+    ErrorCode_Unauthorized = 29    /*!< Bad credentials were provided to an HTTP request */,
+    ErrorCode_BadFont = 30    /*!< Badly formatted font file */,
+    ErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
+    ErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
+    ErrorCode_EmptyRequest = 33    /*!< The request is empty */,
+    ErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
+    ErrorCode_NullPointer = 35    /*!< Cannot handle a NULL pointer */,
+    ErrorCode_DatabaseUnavailable = 36    /*!< The database is currently not available (probably a transient situation) */,
+    ErrorCode_CanceledJob = 37    /*!< This job was canceled */,
+    ErrorCode_BadGeometry = 38    /*!< Geometry error encountered in Stone */,
+    ErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
+    ErrorCode_DiscontinuedAbi = 40    /*!< Calling a function that has been removed from the Orthanc Framework */,
+    ErrorCode_BadRange = 41    /*!< Incorrect range request */,
+    ErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
+    ErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
+    ErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
+    ErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
+    ErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    ErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
+    ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
+    ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
+    ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
+    ErrorCode_SQLiteStatementAlreadyUsed = 1003    /*!< SQLite: This cached statement is already being referred to */,
+    ErrorCode_SQLiteExecute = 1004    /*!< SQLite: Cannot execute a command */,
+    ErrorCode_SQLiteRollbackWithoutTransaction = 1005    /*!< SQLite: Rolling back a nonexistent transaction (have you called Begin()?) */,
+    ErrorCode_SQLiteCommitWithoutTransaction = 1006    /*!< SQLite: Committing a nonexistent transaction */,
+    ErrorCode_SQLiteRegisterFunction = 1007    /*!< SQLite: Unable to register a function */,
+    ErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
+    ErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
+    ErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
+    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
+    ErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
+    ErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
+    ErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
+    ErrorCode_SQLiteTransactionBegin = 1015    /*!< SQLite: Cannot start a transaction */,
+    ErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
+    ErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
+    ErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
+    ErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    ErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
+    ErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
+    ErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
+    ErrorCode_PathToExecutable = 2007    /*!< Unable to get the path to the executable */,
+    ErrorCode_MakeDirectory = 2008    /*!< Cannot create a directory */,
+    ErrorCode_BadApplicationEntityTitle = 2009    /*!< An application entity title (AET) cannot be empty or be longer than 16 characters */,
+    ErrorCode_NoCFindHandler = 2010    /*!< No request handler factory for DICOM C-FIND SCP */,
+    ErrorCode_NoCMoveHandler = 2011    /*!< No request handler factory for DICOM C-MOVE SCP */,
+    ErrorCode_NoCStoreHandler = 2012    /*!< No request handler factory for DICOM C-STORE SCP */,
+    ErrorCode_NoApplicationEntityFilter = 2013    /*!< No application entity filter */,
+    ErrorCode_NoSopClassOrInstance = 2014    /*!< DicomUserConnection: Unable to find the SOP class and instance */,
+    ErrorCode_NoPresentationContext = 2015    /*!< DicomUserConnection: No acceptable presentation context for modality */,
+    ErrorCode_DicomFindUnavailable = 2016    /*!< DicomUserConnection: The C-FIND command is not supported by the remote SCP */,
+    ErrorCode_DicomMoveUnavailable = 2017    /*!< DicomUserConnection: The C-MOVE command is not supported by the remote SCP */,
+    ErrorCode_CannotStoreInstance = 2018    /*!< Cannot store an instance */,
+    ErrorCode_CreateDicomNotString = 2019    /*!< Only string values are supported when creating DICOM instances */,
+    ErrorCode_CreateDicomOverrideTag = 2020    /*!< Trying to override a value inherited from a parent module */,
+    ErrorCode_CreateDicomUseContent = 2021    /*!< Use \"Content\" to inject an image into a new DICOM instance */,
+    ErrorCode_CreateDicomNoPayload = 2022    /*!< No payload is present for one instance in the series */,
+    ErrorCode_CreateDicomUseDataUriScheme = 2023    /*!< The payload of the DICOM instance must be specified according to Data URI scheme */,
+    ErrorCode_CreateDicomBadParent = 2024    /*!< Trying to attach a new DICOM instance to an inexistent resource */,
+    ErrorCode_CreateDicomParentIsInstance = 2025    /*!< Trying to attach a new DICOM instance to an instance (must be a series, study or patient) */,
+    ErrorCode_CreateDicomParentEncoding = 2026    /*!< Unable to get the encoding of the parent resource */,
+    ErrorCode_UnknownModality = 2027    /*!< Unknown modality */,
+    ErrorCode_BadJobOrdering = 2028    /*!< Bad ordering of filters in a job */,
+    ErrorCode_JsonToLuaTable = 2029    /*!< Cannot convert the given JSON object to a Lua table */,
+    ErrorCode_CannotCreateLua = 2030    /*!< Cannot create the Lua context */,
+    ErrorCode_CannotExecuteLua = 2031    /*!< Cannot execute a Lua command */,
+    ErrorCode_LuaAlreadyExecuted = 2032    /*!< Arguments cannot be pushed after the Lua function is executed */,
+    ErrorCode_LuaBadOutput = 2033    /*!< The Lua function does not give the expected number of outputs */,
+    ErrorCode_NotLuaPredicate = 2034    /*!< The Lua function is not a predicate (only true/false outputs allowed) */,
+    ErrorCode_LuaReturnsNoString = 2035    /*!< The Lua function does not return a string */,
+    ErrorCode_StorageAreaAlreadyRegistered = 2036    /*!< Another plugin has already registered a custom storage area */,
+    ErrorCode_DatabaseBackendAlreadyRegistered = 2037    /*!< Another plugin has already registered a custom database back-end */,
+    ErrorCode_DatabaseNotInitialized = 2038    /*!< Plugin trying to call the database during its initialization */,
+    ErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
+    ErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
+    ErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
+    ErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
+    ErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
+    ErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    ErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
+    ErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
+    ErrorCode_START_PLUGINS = 1000000
+  };
+
+  // This enumeration is autogenerated by the script
+  // "Resources/GenerateTransferSyntaxes.py"
+  enum DicomTransferSyntax
+  {
+    DicomTransferSyntax_LittleEndianImplicit    /*!< Implicit VR Little Endian */,
+    DicomTransferSyntax_LittleEndianExplicit    /*!< Explicit VR Little Endian */,
+    DicomTransferSyntax_DeflatedLittleEndianExplicit    /*!< Deflated Explicit VR Little Endian */,
+    DicomTransferSyntax_BigEndianExplicit    /*!< Explicit VR Big Endian */,
+    DicomTransferSyntax_JPEGProcess1    /*!< JPEG Baseline (process 1, lossy) */,
+    DicomTransferSyntax_JPEGProcess2_4    /*!< JPEG Extended Sequential (processes 2 & 4) */,
+    DicomTransferSyntax_JPEGProcess3_5    /*!< JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess6_8    /*!< JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess7_9    /*!< JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess10_12    /*!< JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess11_13    /*!< JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess14    /*!< JPEG Lossless, Nonhierarchical with any selection value (process 14) */,
+    DicomTransferSyntax_JPEGProcess15    /*!< JPEG Lossless with any selection value, arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess16_18    /*!< JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess17_19    /*!< JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess20_22    /*!< JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess21_23    /*!< JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess24_26    /*!< JPEG Full Progression, Hierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess25_27    /*!< JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess28    /*!< JPEG Lossless, Hierarchical */,
+    DicomTransferSyntax_JPEGProcess29    /*!< JPEG Lossless, Hierarchical, arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess14SV1    /*!< JPEG Lossless, Nonhierarchical, First-Order Prediction (Processes 14 [Selection Value 1]) */,
+    DicomTransferSyntax_JPEGLSLossless    /*!< JPEG-LS (lossless) */,
+    DicomTransferSyntax_JPEGLSLossy    /*!< JPEG-LS (lossy or near-lossless) */,
+    DicomTransferSyntax_JPEG2000LosslessOnly    /*!< JPEG 2000 (lossless) */,
+    DicomTransferSyntax_JPEG2000    /*!< JPEG 2000 (lossless or lossy) */,
+    DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly    /*!< JPEG 2000 part 2 multicomponent extensions (lossless) */,
+    DicomTransferSyntax_JPEG2000Multicomponent    /*!< JPEG 2000 part 2 multicomponent extensions (lossless or lossy) */,
+    DicomTransferSyntax_JPIPReferenced    /*!< JPIP Referenced */,
+    DicomTransferSyntax_JPIPReferencedDeflate    /*!< JPIP Referenced Deflate */,
+    DicomTransferSyntax_MPEG2MainProfileAtMainLevel    /*!< MPEG2 Main Profile / Main Level */,
+    DicomTransferSyntax_MPEG2MainProfileAtHighLevel    /*!< MPEG2 Main Profile / High Level */,
+    DicomTransferSyntax_MPEG4HighProfileLevel4_1    /*!< MPEG4 AVC/H.264 High Profile / Level 4.1 */,
+    DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1    /*!< MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1 */,
+    DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo    /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video */,
+    DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo    /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video */,
+    DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2    /*!< MPEG4 AVC/H.264 Stereo High Profile / Level 4.2 */,
+    DicomTransferSyntax_HEVCMainProfileLevel5_1    /*!< HEVC/H.265 Main Profile / Level 5.1 */,
+    DicomTransferSyntax_HEVCMain10ProfileLevel5_1    /*!< HEVC/H.265 Main 10 Profile / Level 5.1 */,
+    DicomTransferSyntax_RLELossless    /*!< RLE - Run Length Encoding (lossless) */,
+    DicomTransferSyntax_RFC2557MimeEncapsulation    /*!< RFC 2557 MIME Encapsulation */,
+    DicomTransferSyntax_XML    /*!< XML Encoding */
+  };
+
+
+  /**
+   * {summary}{The memory layout of the pixels (resp. voxels) of a 2D (resp. 3D) image.}
+   **/
+  enum PixelFormat
+  {
+    /**
+     * {summary}{Color image in RGB24 format.}
+     * {description}{This format describes a color image. The pixels are stored in 3
+     * consecutive bytes. The memory layout is RGB.}
+     **/
+    PixelFormat_RGB24 = 1,
+
+    /**
+     * {summary}{Color image in RGBA32 format.}
+     * {description}{This format describes a color image. The pixels are stored in 4
+     * consecutive bytes. The memory layout is RGBA.}
+     **/
+    PixelFormat_RGBA32 = 2,
+
+    /**
+     * {summary}{Graylevel 8bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in one byte.}
+     **/
+    PixelFormat_Grayscale8 = 3,
+      
+    /**
+     * {summary}{Graylevel, unsigned 16bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in two bytes.}
+     **/
+    PixelFormat_Grayscale16 = 4,
+      
+    /**
+     * {summary}{Graylevel, signed 16bpp image.}
+     * {description}{The image is graylevel. Each pixel is signed and stored in two bytes.}
+     **/
+    PixelFormat_SignedGrayscale16 = 5,
+      
+    /**
+     * {summary}{Graylevel, floating-point image.}
+     * {description}{The image is graylevel. Each pixel is floating-point and stored in 4 bytes.}
+     **/
+    PixelFormat_Float32 = 6,
+
+    // This is the memory layout for Cairo (for internal use in Stone of Orthanc)
+    PixelFormat_BGRA32 = 7,
+
+    /**
+     * {summary}{Graylevel, unsigned 32bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in 4 bytes.}
+     **/
+    PixelFormat_Grayscale32 = 8,
+    
+    /**
+     * {summary}{Color image in RGB48 format.}
+     * {description}{This format describes a color image. The pixels are stored in 6
+     * consecutive bytes. The memory layout is RGB.}
+     **/
+    PixelFormat_RGB48 = 9,
+
+    /**
+     * {summary}{Graylevel, unsigned 64bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in 8 bytes.}
+     **/
+    PixelFormat_Grayscale64 = 10,
+
+    /**
+     * {summary}{Color image in RGBA64 format.}
+     * {description}{This format describes a color image. The pixels are stored in 8
+     * consecutive bytes. The memory layout is RGBA.}
+     **/
+    PixelFormat_RGBA64 = 11
+  };
+
+
+  /**
+   * {summary}{The extraction mode specifies the way the values of the pixels are scaled when downloading a 2D image.}
+   **/
+  enum ImageExtractionMode
+  {
+    /**
+     * {summary}{Rescaled to 8bpp.}
+     * {description}{The minimum value of the image is set to 0, and its maximum value is set to 255.}
+     **/
+    ImageExtractionMode_Preview = 1,
+
+    /**
+     * {summary}{Truncation to the [0, 255] range.}
+     **/
+    ImageExtractionMode_UInt8 = 2,
+
+    /**
+     * {summary}{Truncation to the [0, 65535] range.}
+     **/
+    ImageExtractionMode_UInt16 = 3,
+
+    /**
+     * {summary}{Truncation to the [-32768, 32767] range.}
+     **/
+    ImageExtractionMode_Int16 = 4
+  };
+
+
+  /**
+   * Most common, non-joke and non-experimental HTTP status codes
+   * http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+   **/
+  enum HttpStatus
+  {
+    HttpStatus_None = -1,
+
+    // 1xx Informational
+    HttpStatus_100_Continue = 100,
+    HttpStatus_101_SwitchingProtocols = 101,
+    HttpStatus_102_Processing = 102,
+
+    // 2xx Success
+    HttpStatus_200_Ok = 200,
+    HttpStatus_201_Created = 201,
+    HttpStatus_202_Accepted = 202,
+    HttpStatus_203_NonAuthoritativeInformation = 203,
+    HttpStatus_204_NoContent = 204,
+    HttpStatus_205_ResetContent = 205,
+    HttpStatus_206_PartialContent = 206,
+    HttpStatus_207_MultiStatus = 207,
+    HttpStatus_208_AlreadyReported = 208,
+    HttpStatus_226_IMUsed = 226,
+
+    // 3xx Redirection
+    HttpStatus_300_MultipleChoices = 300,
+    HttpStatus_301_MovedPermanently = 301,
+    HttpStatus_302_Found = 302,
+    HttpStatus_303_SeeOther = 303,
+    HttpStatus_304_NotModified = 304,
+    HttpStatus_305_UseProxy = 305,
+    HttpStatus_307_TemporaryRedirect = 307,
+
+    // 4xx Client Error
+    HttpStatus_400_BadRequest = 400,
+    HttpStatus_401_Unauthorized = 401,
+    HttpStatus_402_PaymentRequired = 402,
+    HttpStatus_403_Forbidden = 403,
+    HttpStatus_404_NotFound = 404,
+    HttpStatus_405_MethodNotAllowed = 405,
+    HttpStatus_406_NotAcceptable = 406,
+    HttpStatus_407_ProxyAuthenticationRequired = 407,
+    HttpStatus_408_RequestTimeout = 408,
+    HttpStatus_409_Conflict = 409,
+    HttpStatus_410_Gone = 410,
+    HttpStatus_411_LengthRequired = 411,
+    HttpStatus_412_PreconditionFailed = 412,
+    HttpStatus_413_RequestEntityTooLarge = 413,
+    HttpStatus_414_RequestUriTooLong = 414,
+    HttpStatus_415_UnsupportedMediaType = 415,
+    HttpStatus_416_RequestedRangeNotSatisfiable = 416,
+    HttpStatus_417_ExpectationFailed = 417,
+    HttpStatus_422_UnprocessableEntity = 422,
+    HttpStatus_423_Locked = 423,
+    HttpStatus_424_FailedDependency = 424,
+    HttpStatus_426_UpgradeRequired = 426,
+
+    // 5xx Server Error
+    HttpStatus_500_InternalServerError = 500,
+    HttpStatus_501_NotImplemented = 501,
+    HttpStatus_502_BadGateway = 502,
+    HttpStatus_503_ServiceUnavailable = 503,
+    HttpStatus_504_GatewayTimeout = 504,
+    HttpStatus_505_HttpVersionNotSupported = 505,
+    HttpStatus_506_VariantAlsoNegotiates = 506,
+    HttpStatus_507_InsufficientStorage = 507,
+    HttpStatus_509_BandwidthLimitExceeded = 509,
+    HttpStatus_510_NotExtended = 510
+  };
+
+
+  enum HttpMethod
+  {
+    HttpMethod_Get = 0,
+    HttpMethod_Post = 1,
+    HttpMethod_Delete = 2,
+    HttpMethod_Put = 3
+  };
+
+
+  enum ImageFormat
+  {
+    ImageFormat_Png = 1
+  };
+
+
+  // https://en.wikipedia.org/wiki/HTTP_compression
+  enum HttpCompression
+  {
+    HttpCompression_None,
+    HttpCompression_Deflate,
+    HttpCompression_Gzip
+  };
+
+
+  // Specific Character Sets
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
+  enum Encoding
+  {
+    Encoding_Ascii,
+    Encoding_Utf8,
+    Encoding_Latin1,
+    Encoding_Latin2,
+    Encoding_Latin3,
+    Encoding_Latin4,
+    Encoding_Latin5,                        // Turkish
+    Encoding_Cyrillic,
+    Encoding_Windows1251,                   // Windows-1251 (commonly used for Cyrillic)
+    Encoding_Arabic,
+    Encoding_Greek,
+    Encoding_Hebrew,
+    Encoding_Thai,                          // TIS 620-2533
+    Encoding_Japanese,                      // JIS X 0201 (Shift JIS): Katakana
+    Encoding_Chinese,                       // GB18030 - Chinese simplified
+    Encoding_JapaneseKanji,                 // Multibyte - JIS X 0208: Kanji
+    //Encoding_JapaneseSupplementaryKanji,  // Multibyte - JIS X 0212: Supplementary Kanji set
+    Encoding_Korean,                        // Multibyte - KS X 1001: Hangul and Hanja
+    Encoding_SimplifiedChinese              // ISO 2022 IR 58
+  };
+
+
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.2
+  enum PhotometricInterpretation
+  {
+    PhotometricInterpretation_ARGB,  // Retired
+    PhotometricInterpretation_CMYK,  // Retired
+    PhotometricInterpretation_HSV,   // Retired
+    PhotometricInterpretation_Monochrome1,
+    PhotometricInterpretation_Monochrome2,
+    PhotometricInterpretation_Palette,
+    PhotometricInterpretation_RGB,
+    PhotometricInterpretation_YBRFull,
+    PhotometricInterpretation_YBRFull422,
+    PhotometricInterpretation_YBRPartial420,
+    PhotometricInterpretation_YBRPartial422,
+    PhotometricInterpretation_YBR_ICT,
+    PhotometricInterpretation_YBR_RCT,
+    PhotometricInterpretation_Unknown
+  };
+
+  enum DicomModule
+  {
+    DicomModule_Patient,
+    DicomModule_Study,
+    DicomModule_Series,
+    DicomModule_Instance,
+    DicomModule_Image
+  };
+
+  enum RequestOrigin
+  {
+    RequestOrigin_Unknown,
+    RequestOrigin_DicomProtocol,
+    RequestOrigin_RestApi,
+    RequestOrigin_Plugins,
+    RequestOrigin_Lua,
+    RequestOrigin_WebDav,   // New in Orthanc 1.8.0
+    RequestOrigin_Documentation  // New in Orthanc in Orthanc 1.8.3 for API documentation (OpenAPI)
+  };
+
+  enum ServerBarrierEvent
+  {
+    ServerBarrierEvent_Stop,
+    ServerBarrierEvent_Reload  // SIGHUP signal: reload configuration file
+  };
+
+  enum FileMode
+  {
+    FileMode_ReadBinary,
+    FileMode_WriteBinary
+  };
+
+  /**
+   * The value representations Orthanc knows about. They correspond to
+   * the DICOM 2016b version of the standard.
+   * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+   **/
+  enum ValueRepresentation
+  {
+    ValueRepresentation_ApplicationEntity = 1,     // AE
+    ValueRepresentation_AgeString = 2,             // AS
+    ValueRepresentation_AttributeTag = 3,          // AT (2 x uint16_t)
+    ValueRepresentation_CodeString = 4,            // CS
+    ValueRepresentation_Date = 5,                  // DA
+    ValueRepresentation_DecimalString = 6,         // DS
+    ValueRepresentation_DateTime = 7,              // DT
+    ValueRepresentation_FloatingPointSingle = 8,   // FL (float)
+    ValueRepresentation_FloatingPointDouble = 9,   // FD (double)
+    ValueRepresentation_IntegerString = 10,        // IS
+    ValueRepresentation_LongString = 11,           // LO
+    ValueRepresentation_LongText = 12,             // LT
+    ValueRepresentation_OtherByte = 13,            // OB
+    ValueRepresentation_OtherDouble = 14,          // OD
+    ValueRepresentation_OtherFloat = 15,           // OF
+    ValueRepresentation_OtherLong = 16,            // OL
+    ValueRepresentation_OtherWord = 17,            // OW
+    ValueRepresentation_PersonName = 18,           // PN
+    ValueRepresentation_ShortString = 19,          // SH
+    ValueRepresentation_SignedLong = 20,           // SL (int32_t)
+    ValueRepresentation_Sequence = 21,             // SQ
+    ValueRepresentation_SignedShort = 22,          // SS (int16_t)
+    ValueRepresentation_ShortText = 23,            // ST
+    ValueRepresentation_Time = 24,                 // TM
+    ValueRepresentation_UnlimitedCharacters = 25,  // UC
+    ValueRepresentation_UniqueIdentifier = 26,     // UI (UID)
+    ValueRepresentation_UnsignedLong = 27,         // UL (uint32_t)
+    ValueRepresentation_Unknown = 28,              // UN
+    ValueRepresentation_UniversalResource = 29,    // UR (URI or URL)
+    ValueRepresentation_UnsignedShort = 30,        // US (uint16_t)
+    ValueRepresentation_UnlimitedText = 31,        // UT
+    ValueRepresentation_NotSupported               // Not supported by Orthanc, or tag not in dictionary
+  };
+
+  enum DicomReplaceMode
+  {
+    DicomReplaceMode_InsertIfAbsent,
+    DicomReplaceMode_ThrowIfAbsent,
+    DicomReplaceMode_IgnoreIfAbsent
+  };
+
+  enum DicomToJsonFormat
+  {
+    DicomToJsonFormat_Full,
+    DicomToJsonFormat_Short,
+    DicomToJsonFormat_Human
+  };
+
+  enum DicomToJsonFlags
+  {
+    DicomToJsonFlags_IncludeBinary         = (1 << 0),
+    DicomToJsonFlags_IncludePrivateTags    = (1 << 1),
+    DicomToJsonFlags_IncludeUnknownTags    = (1 << 2),
+    DicomToJsonFlags_IncludePixelData      = (1 << 3),
+    DicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),
+    DicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),
+    DicomToJsonFlags_StopAfterPixelData    = (1 << 6),  // New in Orthanc 1.9.1
+    DicomToJsonFlags_SkipGroupLengths      = (1 << 7),  // New in Orthanc 1.9.1
+
+    // Some predefined combinations
+    DicomToJsonFlags_None     = 0,
+    DicomToJsonFlags_Default  = (DicomToJsonFlags_IncludeBinary |
+                                 DicomToJsonFlags_IncludePixelData | 
+                                 DicomToJsonFlags_IncludePrivateTags | 
+                                 DicomToJsonFlags_IncludeUnknownTags | 
+                                 DicomToJsonFlags_ConvertBinaryToNull |
+                                 DicomToJsonFlags_StopAfterPixelData /* added in 1.9.1 */)
+  };
+  
+  enum DicomFromJsonFlags
+  {
+    DicomFromJsonFlags_DecodeDataUriScheme = (1 << 0),
+    DicomFromJsonFlags_GenerateIdentifiers = (1 << 1),
+
+    // Some predefined combinations
+    DicomFromJsonFlags_None = 0
+  };
+
+  // If adding a new DICOM version below, update the
+  // "DeidentifyLogsDicomVersion" configuration option
+  enum DicomVersion
+  {
+    DicomVersion_2008,
+    DicomVersion_2017c,
+    DicomVersion_2021b,
+    DicomVersion_2023b
+  };
+
+  enum ModalityManufacturer
+  {
+    ModalityManufacturer_Generic,
+    ModalityManufacturer_GenericNoWildcardInDates,
+    ModalityManufacturer_GenericNoUniversalWildcard,
+    ModalityManufacturer_Vitrea,
+    ModalityManufacturer_GE
+  };
+
+  enum DicomRequestType
+  {
+    DicomRequestType_Echo,
+    DicomRequestType_Find,
+    DicomRequestType_FindWorklist,
+    DicomRequestType_Get,
+    DicomRequestType_Move,
+    DicomRequestType_Store,
+    DicomRequestType_NAction,
+    DicomRequestType_NEventReport
+  };
+
+  enum JobState
+  {
+    JobState_Pending,
+    JobState_Running,
+    JobState_Success,
+    JobState_Failure,
+    JobState_Paused,
+    JobState_Retry
+  };
+
+  enum JobStepCode
+  {
+    JobStepCode_Success,
+    JobStepCode_Failure,
+    JobStepCode_Continue,
+    JobStepCode_Retry
+  };
+
+  enum JobStopReason
+  {
+    JobStopReason_Paused,
+    JobStopReason_Canceled,
+    JobStopReason_Success,
+    JobStopReason_Failure,
+    JobStopReason_Retry
+  };
+
+  
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+  enum StorageCommitmentFailureReason
+  {
+    StorageCommitmentFailureReason_Success = 0,
+
+    // A general failure in processing the operation was encountered
+    StorageCommitmentFailureReason_ProcessingFailure = 0x0110,
+
+    // One or more of the elements in the Referenced SOP Instance
+    // Sequence was not available
+    StorageCommitmentFailureReason_NoSuchObjectInstance = 0x0112,
+
+    // The SCP does not currently have enough resources to store the
+    // requested SOP Instance(s)
+    StorageCommitmentFailureReason_ResourceLimitation = 0x0213,
+
+    // Storage Commitment has been requested for a SOP Instance with a
+    // SOP Class that is not supported by the SCP
+    StorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 0x0122,
+
+    // The SOP Class of an element in the Referenced SOP Instance
+    // Sequence did not correspond to the SOP class registered for
+    // this SOP Instance at the SCP
+    StorageCommitmentFailureReason_ClassInstanceConflict = 0x0119,
+
+    // The Transaction UID of the Storage Commitment Request is already in use
+    StorageCommitmentFailureReason_DuplicateTransactionUID = 0x0131
+  };
+
+
+  enum DicomAssociationRole
+  {
+    DicomAssociationRole_Default,
+    DicomAssociationRole_Scu,
+    DicomAssociationRole_Scp
+  };
+
+
+  /**
+   * WARNING: Do not change the explicit values in the enumerations
+   * below this point. This would result in incompatible databases
+   * between versions of Orthanc!
+   **/
+
+  enum CompressionType
+  {
+    /**
+     * Buffer/file that is stored as-is, in a raw fashion, without
+     * compression.
+     **/
+    CompressionType_None = 1,
+
+    /**
+     * Buffer that is compressed using the "deflate" algorithm (RFC
+     * 1951), wrapped inside the zlib data format (RFC 1950), prefixed
+     * with a "uint64_t" (8 bytes) that encodes the size of the
+     * uncompressed buffer. If the compressed buffer is empty, its
+     * represents an empty uncompressed buffer. This format is
+     * internal to Orthanc. If the 8 first bytes are skipped AND the
+     * buffer is non-empty, the buffer is compatible with the
+     * "deflate" HTTP compression.
+     **/
+    CompressionType_ZlibWithSize = 2
+  };
+
+  enum FileContentType
+  {
+    // If you add a value below, insert it in "PluginStorageArea" in
+    // the file "Plugins/Engine/OrthancPlugins.cpp"
+    FileContentType_Unknown = 0,
+    FileContentType_Dicom = 1,
+    FileContentType_DicomAsJson = 2,          // For Orthanc <= 1.9.0
+    FileContentType_DicomUntilPixelData = 3,  // New in Orthanc 1.9.1
+
+    // Make sure that the value "65535" can be stored into this enumeration
+    FileContentType_StartUser = 1024,
+    FileContentType_EndUser = 65535
+  };
+
+  enum ResourceType
+  {
+    ResourceType_Patient = 1,
+    ResourceType_Study = 2,
+    ResourceType_Series = 3,
+    ResourceType_Instance = 4
+  };
+
+  enum RetrieveMethod                         // new in Orthanc 1.12.6
+  {
+    RetrieveMethod_Move = 1,
+    RetrieveMethod_Get = 2,
+
+    RetrieveMethod_SystemDefault = 65535
+  };
+
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ErrorCode code);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(HttpMethod method);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(HttpStatus status);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ResourceType type);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ImageFormat format);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(Encoding encoding);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(PhotometricInterpretation photometric);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(RequestOrigin origin);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(PixelFormat format);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ModalityManufacturer manufacturer);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(DicomRequestType type);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(DicomVersion version);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ValueRepresentation vr);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(JobState state);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(MimeType mime);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(Endianness endianness);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(StorageCommitmentFailureReason reason);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(DicomToJsonFormat format);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(RetrieveMethod method);
+
+  ORTHANC_PUBLIC
+  Encoding StringToEncoding(const char* encoding);
+
+  ORTHANC_PUBLIC
+  ResourceType StringToResourceType(const char* type);
+
+  ORTHANC_PUBLIC
+  ImageFormat StringToImageFormat(const char* format);
+
+  ORTHANC_PUBLIC
+  ValueRepresentation StringToValueRepresentation(const std::string& vr,
+                                                  bool throwIfUnsupported);
+
+  ORTHANC_PUBLIC
+  PhotometricInterpretation StringToPhotometricInterpretation(const char* value);
+
+  ORTHANC_PUBLIC
+  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
+
+  ORTHANC_PUBLIC
+  DicomVersion StringToDicomVersion(const std::string& version);
+
+  ORTHANC_PUBLIC
+  JobState StringToJobState(const std::string& state);
+  
+  ORTHANC_PUBLIC
+  RequestOrigin StringToRequestOrigin(const std::string& origin);
+
+  ORTHANC_PUBLIC
+  MimeType StringToMimeType(const std::string& mime);
+  
+  ORTHANC_PUBLIC
+  DicomToJsonFormat StringToDicomToJsonFormat(const std::string& format);
+  
+  ORTHANC_PUBLIC
+  bool LookupMimeType(MimeType& target,
+                      const std::string& source);
+  
+  ORTHANC_PUBLIC
+  unsigned int GetBytesPerPixel(PixelFormat format);
+
+  ORTHANC_PUBLIC
+  bool GetDicomEncoding(Encoding& encoding,
+                        const char* specificCharacterSet);
+
+  ORTHANC_PUBLIC
+  ResourceType GetChildResourceType(ResourceType type);
+
+  ORTHANC_PUBLIC
+  ResourceType GetParentResourceType(ResourceType type);
+
+  ORTHANC_PUBLIC
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference);
+
+ORTHANC_PUBLIC
+  const char* ResourceTypeToDicomQueryRetrieveLevel(ResourceType type);
+
+  ORTHANC_PUBLIC
+  DicomModule GetModule(ResourceType type);
+
+  ORTHANC_PUBLIC
+  const char* GetDicomSpecificCharacterSet(Encoding encoding);
+
+  ORTHANC_PUBLIC
+  HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error);
+
+  ORTHANC_PUBLIC
+  bool IsUserContentType(FileContentType type);
+
+  ORTHANC_PUBLIC
+  bool IsBinaryValueRepresentation(ValueRepresentation vr);
+  
+  ORTHANC_PUBLIC
+  Encoding GetDefaultDicomEncoding();
+
+  ORTHANC_PUBLIC
+  void SetDefaultDicomEncoding(Encoding encoding);
+
+  ORTHANC_PUBLIC
+  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax);
+
+  ORTHANC_PUBLIC
+  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax);
+
+  ORTHANC_PUBLIC
+  bool LookupTransferSyntax(DicomTransferSyntax& target,
+                            const std::string& uid);
+
+  ORTHANC_PUBLIC
+  DicomTransferSyntax GetTransferSyntax(const std::string& uid);
+
+  ORTHANC_PUBLIC
+  const char* GetResourceTypeText(ResourceType type,
+                                  bool isPlural,
+                                  bool isUpperCase);
+
+  ORTHANC_PUBLIC
+  void GetAllDicomTransferSyntaxes(std::set& target);
+
+  ORTHANC_PUBLIC 
+  RetrieveMethod StringToRetrieveMethod(const std::string& str);
+}
diff --git a/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h b/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h
new file mode 100644
index 0000000..5e5825f
--- /dev/null
+++ b/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h
@@ -0,0 +1,605 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+// This file is autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        return "1.2.840.10008.1.2";
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        return "1.2.840.10008.1.2.1";
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        return "1.2.840.10008.1.2.1.99";
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        return "1.2.840.10008.1.2.2";
+
+      case DicomTransferSyntax_JPEGProcess1:
+        return "1.2.840.10008.1.2.4.50";
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+        return "1.2.840.10008.1.2.4.51";
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+        return "1.2.840.10008.1.2.4.52";
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+        return "1.2.840.10008.1.2.4.53";
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+        return "1.2.840.10008.1.2.4.54";
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+        return "1.2.840.10008.1.2.4.55";
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+        return "1.2.840.10008.1.2.4.56";
+
+      case DicomTransferSyntax_JPEGProcess14:
+        return "1.2.840.10008.1.2.4.57";
+
+      case DicomTransferSyntax_JPEGProcess15:
+        return "1.2.840.10008.1.2.4.58";
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+        return "1.2.840.10008.1.2.4.59";
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+        return "1.2.840.10008.1.2.4.60";
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+        return "1.2.840.10008.1.2.4.61";
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+        return "1.2.840.10008.1.2.4.62";
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+        return "1.2.840.10008.1.2.4.63";
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+        return "1.2.840.10008.1.2.4.64";
+
+      case DicomTransferSyntax_JPEGProcess28:
+        return "1.2.840.10008.1.2.4.65";
+
+      case DicomTransferSyntax_JPEGProcess29:
+        return "1.2.840.10008.1.2.4.66";
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+        return "1.2.840.10008.1.2.4.70";
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        return "1.2.840.10008.1.2.4.80";
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        return "1.2.840.10008.1.2.4.81";
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        return "1.2.840.10008.1.2.4.90";
+
+      case DicomTransferSyntax_JPEG2000:
+        return "1.2.840.10008.1.2.4.91";
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        return "1.2.840.10008.1.2.4.92";
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        return "1.2.840.10008.1.2.4.93";
+
+      case DicomTransferSyntax_JPIPReferenced:
+        return "1.2.840.10008.1.2.4.94";
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        return "1.2.840.10008.1.2.4.95";
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        return "1.2.840.10008.1.2.4.100";
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        return "1.2.840.10008.1.2.4.101";
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        return "1.2.840.10008.1.2.4.102";
+
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        return "1.2.840.10008.1.2.4.103";
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        return "1.2.840.10008.1.2.4.104";
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        return "1.2.840.10008.1.2.4.105";
+
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        return "1.2.840.10008.1.2.4.106";
+
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        return "1.2.840.10008.1.2.4.107";
+
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        return "1.2.840.10008.1.2.4.108";
+
+      case DicomTransferSyntax_RLELossless:
+        return "1.2.840.10008.1.2.5";
+
+      case DicomTransferSyntax_RFC2557MimeEncapsulation:
+        return "1.2.840.10008.1.2.6.1";
+
+      case DicomTransferSyntax_XML:
+        return "1.2.840.10008.1.2.6.2";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        return false;
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess1:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess15:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess28:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess29:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        return false;
+
+      case DicomTransferSyntax_JPIPReferenced:
+        return false;
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        return false;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        return false;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        return false;
+
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        return false;
+
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        return false;
+
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        return false;
+
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        return false;
+
+      case DicomTransferSyntax_RLELossless:
+        return false;
+
+      case DicomTransferSyntax_RFC2557MimeEncapsulation:
+        return true;
+
+      case DicomTransferSyntax_XML:
+        return true;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool LookupTransferSyntax(DicomTransferSyntax& target,
+                            const std::string& uid)
+  {
+    if (uid == "1.2.840.10008.1.2")
+    {
+      target = DicomTransferSyntax_LittleEndianImplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.1")
+    {
+      target = DicomTransferSyntax_LittleEndianExplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.1.99")
+    {
+      target = DicomTransferSyntax_DeflatedLittleEndianExplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.2")
+    {
+      target = DicomTransferSyntax_BigEndianExplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.50")
+    {
+      target = DicomTransferSyntax_JPEGProcess1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.51")
+    {
+      target = DicomTransferSyntax_JPEGProcess2_4;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.52")
+    {
+      target = DicomTransferSyntax_JPEGProcess3_5;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.53")
+    {
+      target = DicomTransferSyntax_JPEGProcess6_8;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.54")
+    {
+      target = DicomTransferSyntax_JPEGProcess7_9;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.55")
+    {
+      target = DicomTransferSyntax_JPEGProcess10_12;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.56")
+    {
+      target = DicomTransferSyntax_JPEGProcess11_13;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.57")
+    {
+      target = DicomTransferSyntax_JPEGProcess14;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.58")
+    {
+      target = DicomTransferSyntax_JPEGProcess15;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.59")
+    {
+      target = DicomTransferSyntax_JPEGProcess16_18;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.60")
+    {
+      target = DicomTransferSyntax_JPEGProcess17_19;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.61")
+    {
+      target = DicomTransferSyntax_JPEGProcess20_22;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.62")
+    {
+      target = DicomTransferSyntax_JPEGProcess21_23;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.63")
+    {
+      target = DicomTransferSyntax_JPEGProcess24_26;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.64")
+    {
+      target = DicomTransferSyntax_JPEGProcess25_27;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.65")
+    {
+      target = DicomTransferSyntax_JPEGProcess28;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.66")
+    {
+      target = DicomTransferSyntax_JPEGProcess29;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.70")
+    {
+      target = DicomTransferSyntax_JPEGProcess14SV1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.80")
+    {
+      target = DicomTransferSyntax_JPEGLSLossless;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.81")
+    {
+      target = DicomTransferSyntax_JPEGLSLossy;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.90")
+    {
+      target = DicomTransferSyntax_JPEG2000LosslessOnly;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.91")
+    {
+      target = DicomTransferSyntax_JPEG2000;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.92")
+    {
+      target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.93")
+    {
+      target = DicomTransferSyntax_JPEG2000Multicomponent;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.94")
+    {
+      target = DicomTransferSyntax_JPIPReferenced;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.95")
+    {
+      target = DicomTransferSyntax_JPIPReferencedDeflate;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.100")
+    {
+      target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.101")
+    {
+      target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.102")
+    {
+      target = DicomTransferSyntax_MPEG4HighProfileLevel4_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.103")
+    {
+      target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.104")
+    {
+      target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.105")
+    {
+      target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.106")
+    {
+      target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.107")
+    {
+      target = DicomTransferSyntax_HEVCMainProfileLevel5_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.108")
+    {
+      target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.5")
+    {
+      target = DicomTransferSyntax_RLELossless;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.6.1")
+    {
+      target = DicomTransferSyntax_RFC2557MimeEncapsulation;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.6.2")
+    {
+      target = DicomTransferSyntax_XML;
+      return true;
+    }
+    
+    return false;
+  }
+
+
+  void GetAllDicomTransferSyntaxes(std::set& target)
+  {
+    target.clear();
+    target.insert(DicomTransferSyntax_LittleEndianImplicit);
+    target.insert(DicomTransferSyntax_LittleEndianExplicit);
+    target.insert(DicomTransferSyntax_DeflatedLittleEndianExplicit);
+    target.insert(DicomTransferSyntax_BigEndianExplicit);
+    target.insert(DicomTransferSyntax_JPEGProcess1);
+    target.insert(DicomTransferSyntax_JPEGProcess2_4);
+    target.insert(DicomTransferSyntax_JPEGProcess3_5);
+    target.insert(DicomTransferSyntax_JPEGProcess6_8);
+    target.insert(DicomTransferSyntax_JPEGProcess7_9);
+    target.insert(DicomTransferSyntax_JPEGProcess10_12);
+    target.insert(DicomTransferSyntax_JPEGProcess11_13);
+    target.insert(DicomTransferSyntax_JPEGProcess14);
+    target.insert(DicomTransferSyntax_JPEGProcess15);
+    target.insert(DicomTransferSyntax_JPEGProcess16_18);
+    target.insert(DicomTransferSyntax_JPEGProcess17_19);
+    target.insert(DicomTransferSyntax_JPEGProcess20_22);
+    target.insert(DicomTransferSyntax_JPEGProcess21_23);
+    target.insert(DicomTransferSyntax_JPEGProcess24_26);
+    target.insert(DicomTransferSyntax_JPEGProcess25_27);
+    target.insert(DicomTransferSyntax_JPEGProcess28);
+    target.insert(DicomTransferSyntax_JPEGProcess29);
+    target.insert(DicomTransferSyntax_JPEGProcess14SV1);
+    target.insert(DicomTransferSyntax_JPEGLSLossless);
+    target.insert(DicomTransferSyntax_JPEGLSLossy);
+    target.insert(DicomTransferSyntax_JPEG2000LosslessOnly);
+    target.insert(DicomTransferSyntax_JPEG2000);
+    target.insert(DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly);
+    target.insert(DicomTransferSyntax_JPEG2000Multicomponent);
+    target.insert(DicomTransferSyntax_JPIPReferenced);
+    target.insert(DicomTransferSyntax_JPIPReferencedDeflate);
+    target.insert(DicomTransferSyntax_MPEG2MainProfileAtMainLevel);
+    target.insert(DicomTransferSyntax_MPEG2MainProfileAtHighLevel);
+    target.insert(DicomTransferSyntax_MPEG4HighProfileLevel4_1);
+    target.insert(DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1);
+    target.insert(DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo);
+    target.insert(DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo);
+    target.insert(DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2);
+    target.insert(DicomTransferSyntax_HEVCMainProfileLevel5_1);
+    target.insert(DicomTransferSyntax_HEVCMain10ProfileLevel5_1);
+    target.insert(DicomTransferSyntax_RLELossless);
+    target.insert(DicomTransferSyntax_RFC2557MimeEncapsulation);
+    target.insert(DicomTransferSyntax_XML);
+  }
+}
diff --git a/OrthancFramework/Sources/FileBuffer.cpp b/OrthancFramework/Sources/FileBuffer.cpp
new file mode 100644
index 0000000..64f1f20
--- /dev/null
+++ b/OrthancFramework/Sources/FileBuffer.cpp
@@ -0,0 +1,113 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "FileBuffer.h"
+
+#include "TemporaryFile.h"
+#include "OrthancException.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  class FileBuffer::PImpl
+  {
+  private:
+    TemporaryFile                file_;
+    boost::filesystem::ofstream  stream_;
+    bool                         isWriting_;
+
+  public:
+    PImpl() :
+      isWriting_(true)
+    {
+      stream_.open(file_.GetPath(), std::ofstream::out | std::ofstream::binary);
+      if (!stream_.good())
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile);
+      }
+    }
+
+    ~PImpl()
+    {
+      if (isWriting_)
+      {
+        stream_.close();
+      }
+    }
+
+    void Append(const char* buffer,
+                size_t size)
+    {
+      if (!isWriting_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (size > 0)
+      {
+        stream_.write(buffer, size);
+        if (!stream_.good())
+        {
+          stream_.close();
+          throw OrthancException(ErrorCode_FileStorageCannotWrite);
+        }
+      }
+    }
+
+    void Read(std::string& target)
+    {
+      if (isWriting_)
+      {
+        stream_.close();
+        isWriting_ = false;
+      }
+
+      file_.Read(target);
+    }
+  };
+
+    
+  FileBuffer::FileBuffer() :
+    pimpl_(new PImpl)
+  {
+  }
+
+
+  void FileBuffer::Append(const char* buffer,
+                          size_t size)
+  {
+    assert(pimpl_.get() != NULL);
+    pimpl_->Append(buffer, size);
+  }
+
+
+  void FileBuffer::Read(std::string& target)
+  {
+    assert(pimpl_.get() != NULL);
+    pimpl_->Read(target);
+  }
+}
diff --git a/OrthancFramework/Sources/FileBuffer.h b/OrthancFramework/Sources/FileBuffer.h
new file mode 100644
index 0000000..d8dae96
--- /dev/null
+++ b/OrthancFramework/Sources/FileBuffer.h
@@ -0,0 +1,57 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "OrthancFramework.h"
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class FileBuffer cannot be used in sandboxed environments
+#endif
+
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC FileBuffer : public boost::noncopyable
+  {
+  private:
+    class PImpl;
+    boost::shared_ptr pimpl_;
+
+  public:
+    FileBuffer();
+
+    void Append(const char* buffer,
+                size_t size);
+
+    void Read(std::string& target);
+  };
+}
diff --git a/OrthancFramework/Sources/FileStorage/FileInfo.cpp b/OrthancFramework/Sources/FileStorage/FileInfo.cpp
new file mode 100644
index 0000000..6c70aeb
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp
@@ -0,0 +1,224 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "FileInfo.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  FileInfo::FileInfo() :
+    valid_(false),
+    contentType_(FileContentType_Unknown),
+    uncompressedSize_(0),
+    compressionType_(CompressionType_None),
+    compressedSize_(0)
+  {
+  }
+
+  
+  FileInfo::FileInfo(const std::string& uuid,
+                     FileContentType contentType,
+                     uint64_t size,
+                     const std::string& md5) :
+    valid_(true),
+    uuid_(uuid),
+    contentType_(contentType),
+    uncompressedSize_(size),
+    uncompressedMD5_(md5),
+    compressionType_(CompressionType_None),
+    compressedSize_(size),
+    compressedMD5_(md5)
+  {
+  }
+
+
+  FileInfo::FileInfo(const std::string& uuid,
+                     FileContentType contentType,
+                     uint64_t uncompressedSize,
+                     const std::string& uncompressedMD5,
+                     CompressionType compressionType,
+                     uint64_t compressedSize,
+                     const std::string& compressedMD5) :
+    valid_(true),
+    uuid_(uuid),
+    contentType_(contentType),
+    uncompressedSize_(uncompressedSize),
+    uncompressedMD5_(uncompressedMD5),
+    compressionType_(compressionType),
+    compressedSize_(compressedSize),
+    compressedMD5_(compressedMD5)
+  {
+  }
+
+  
+  bool FileInfo::IsValid() const
+  {
+    return valid_;
+  }
+
+  
+  const std::string& FileInfo::GetUuid() const
+  {
+    if (valid_)
+    {
+      return uuid_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  
+  FileContentType FileInfo::GetContentType() const
+  {
+    if (valid_)
+    {
+      return contentType_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+
+  uint64_t FileInfo::GetUncompressedSize() const
+  {
+    if (valid_)
+    {
+      return uncompressedSize_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+
+  CompressionType FileInfo::GetCompressionType() const
+  {
+    if (valid_)
+    {
+      return compressionType_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+
+  uint64_t FileInfo::GetCompressedSize() const
+  {
+    if (valid_)
+    {
+      return compressedSize_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  
+  const std::string& FileInfo::GetCompressedMD5() const
+  {
+    if (valid_)
+    {
+      return compressedMD5_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  
+  const std::string& FileInfo::GetUncompressedMD5() const
+  {
+    if (valid_)
+    {
+      return uncompressedMD5_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  void FileInfo::SetCustomData(const void* data,
+                               size_t size)
+  {
+    if (valid_)
+    {
+      customData_.assign(reinterpret_cast(data), size);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FileInfo::SetCustomData(const std::string& data)
+  {
+    if (valid_)
+    {
+      customData_ = data;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FileInfo::SwapCustomData(std::string& data)
+  {
+    if (valid_)
+    {
+      customData_.swap(data);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const std::string& FileInfo::GetCustomData() const
+  {
+    if (valid_)
+    {
+      return customData_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/FileStorage/FileInfo.h b/OrthancFramework/Sources/FileStorage/FileInfo.h
new file mode 100644
index 0000000..238c95c
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h
@@ -0,0 +1,94 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+#include "../Enumerations.h"
+
+#include 
+
+namespace Orthanc
+{
+  struct ORTHANC_PUBLIC FileInfo
+  {
+  private:
+    bool             valid_;
+    std::string      uuid_;
+    FileContentType  contentType_;
+    uint64_t         uncompressedSize_;
+    std::string      uncompressedMD5_;
+    CompressionType  compressionType_;
+    uint64_t         compressedSize_;
+    std::string      compressedMD5_;
+    std::string      customData_;
+
+  public:
+    FileInfo();
+
+    /**
+     * Constructor for an uncompressed attachment.
+     **/
+    FileInfo(const std::string& uuid,
+             FileContentType contentType,
+             uint64_t size,
+             const std::string& md5);
+
+    /**
+     * Constructor for a compressed attachment.
+     **/
+    FileInfo(const std::string& uuid,
+             FileContentType contentType,
+             uint64_t uncompressedSize,
+             const std::string& uncompressedMD5,
+             CompressionType compressionType,
+             uint64_t compressedSize,
+             const std::string& compressedMD5);
+
+    bool IsValid() const;
+    
+    const std::string& GetUuid() const;
+
+    FileContentType GetContentType() const;
+
+    uint64_t GetUncompressedSize() const;
+
+    CompressionType GetCompressionType() const;
+
+    uint64_t GetCompressedSize() const;
+
+    const std::string& GetCompressedMD5() const;
+
+    const std::string& GetUncompressedMD5() const;
+
+    void SetCustomData(const void* data,
+                       size_t size);
+
+    void SetCustomData(const std::string& data);
+
+    void SwapCustomData(std::string& data);
+
+    const std::string& GetCustomData() const;
+  };
+}
diff --git a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp
new file mode 100644
index 0000000..2a4a8f7
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp
@@ -0,0 +1,355 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "FilesystemStorage.h"
+#include 
+
+// http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system
+// http://stackoverflow.com/questions/446358/storing-a-large-number-of-images
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../StringMemoryBuffer.h"
+#include "../SystemToolbox.h"
+#include "../Toolbox.h"
+
+#include 
+
+
+static std::string ToString(const boost::filesystem::path& p)
+{
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+  return p.filename().string();
+#else
+  return p.filename();
+#endif
+}
+
+
+namespace Orthanc
+{
+  boost::filesystem::path FilesystemStorage::GetPath(const std::string& uuid) const
+  {
+    namespace fs = boost::filesystem;
+
+    if (!Toolbox::IsUuid(uuid))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    fs::path path = root_;
+
+    path /= std::string(&uuid[0], &uuid[2]);
+    path /= std::string(&uuid[2], &uuid[4]);
+    path /= uuid;
+
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+    path.make_preferred();
+#endif
+
+    return path;
+  }
+
+  void FilesystemStorage::Setup(const std::string& root)
+  {
+    //root_ = boost::filesystem::absolute(root).string();
+    root_ = root;
+
+    SystemToolbox::MakeDirectory(root);
+  }
+
+  FilesystemStorage::FilesystemStorage(const std::string &root) :
+    fsyncOnWrite_(false)
+  {
+    Setup(root);
+  }
+
+  FilesystemStorage::FilesystemStorage(const std::string &root,
+                                       bool fsyncOnWrite) :
+    fsyncOnWrite_(fsyncOnWrite)
+  {
+    Setup(root);
+  }
+
+
+
+  static const char* GetDescriptionInternal(FileContentType content)
+  {
+    // This function is for logging only (internal use), a more
+    // fully-featured version is available in ServerEnumerations.cpp
+    switch (content)
+    {
+      case FileContentType_Unknown:
+        return "Unknown";
+
+      case FileContentType_Dicom:
+        return "DICOM";
+
+      case FileContentType_DicomAsJson:
+        return "JSON summary of DICOM";
+
+      case FileContentType_DicomUntilPixelData:
+        return "DICOM until pixel data";
+
+      default:
+        return "User-defined";
+    }
+  }
+
+
+  void FilesystemStorage::Create(const std::string& uuid,
+                                 const void* content, 
+                                 size_t size,
+                                 FileContentType type)
+  {
+    Toolbox::ElapsedTimer timer;
+    LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+              << "\" type";
+
+    boost::filesystem::path path;
+    
+    path = GetPath(uuid);
+
+    if (boost::filesystem::exists(path))
+    {
+      // Extremely unlikely case: This Uuid has already been created
+      // in the past.
+      throw OrthancException(ErrorCode_InternalError, "This file UUID already exists");
+    }
+
+    // In very unlikely cases, a thread could be deleting a
+    // directory while another thread needs it -> introduce 3 retries at 1 ms interval
+    int retryCount = 0;
+    const int maxRetryCount = 3;
+    
+    while (retryCount < maxRetryCount)
+    {
+      retryCount++;
+      if (retryCount > 1)
+      {
+        boost::this_thread::sleep(boost::posix_time::milliseconds(2 * retryCount + (rand() % 10)));
+        LOG(INFO) << "Retrying to create attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+                  << "\" type";
+      }
+
+      try 
+      {
+        boost::filesystem::create_directories(path.parent_path());  // the function ensures that the directory exists or throws
+      }
+      catch (boost::filesystem::filesystem_error& er)
+      {
+        if (er.code() == boost::system::errc::file_exists  // the last element of the parent_path is a file
+          || er.code() == boost::system::errc::not_a_directory) // one of the element of the parent_path is not a directory 
+        {
+          throw OrthancException(ErrorCode_DirectoryOverFile, "One of the element of the path is a file");  // no need to retry this error
+        }
+
+        // ignore other errors and retry
+      }
+
+      try 
+      {
+        SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
+        
+        LOG(INFO) << "Created attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, size) << ")";
+        return;
+      }
+      catch (OrthancException&)
+      {
+        if (retryCount >= maxRetryCount)
+        {
+          throw;
+        }
+      }
+    }
+  }
+
+
+  IMemoryBuffer* FilesystemStorage::ReadWhole(const std::string& uuid,
+                                              FileContentType type)
+  {
+    Toolbox::ElapsedTimer timer;
+    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+              << "\" content type";
+
+    std::string content;
+    SystemToolbox::ReadFile(content, GetPath(uuid).string());
+
+    LOG(INFO) << "Read attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, content.size()) << ")";
+
+    return StringMemoryBuffer::CreateFromSwap(content);
+  }
+
+
+  IMemoryBuffer* FilesystemStorage::ReadRange(const std::string& uuid,
+                                              FileContentType type,
+                                              uint64_t start /* inclusive */,
+                                              uint64_t end /* exclusive */)
+  {
+    Toolbox::ElapsedTimer timer;
+    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+              << "\" content type (range from " << start << " to " << end << ")";
+
+    std::string content;
+    SystemToolbox::ReadFileRange(
+      content, GetPath(uuid).string(), start, end, true /* throw if overflow */);
+
+    LOG(INFO) << "Read range of attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, content.size()) << ")";
+    return StringMemoryBuffer::CreateFromSwap(content);
+  }
+
+
+  uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const
+  {
+    boost::filesystem::path path = GetPath(uuid);
+    return boost::filesystem::file_size(path);
+  }
+
+
+
+  void FilesystemStorage::ListAllFiles(std::set& result) const
+  {
+    namespace fs = boost::filesystem;
+
+    result.clear();
+
+    if (fs::exists(root_) && fs::is_directory(root_))
+    {
+      for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current)
+      {
+        if (SystemToolbox::IsRegularFile(current->path().string()))
+        {
+          try
+          {
+            fs::path d = current->path();
+            std::string uuid = ToString(d);
+            if (Toolbox::IsUuid(uuid))
+            {
+              fs::path p0 = d.parent_path().parent_path().parent_path();
+              std::string p1 = ToString(d.parent_path().parent_path());
+              std::string p2 = ToString(d.parent_path());
+              if (p1.length() == 2 &&
+                  p2.length() == 2 &&
+                  p1 == uuid.substr(0, 2) &&
+                  p2 == uuid.substr(2, 2) &&
+                  p0 == root_)
+              {
+                result.insert(uuid);
+              }
+            }
+          }
+          catch (fs::filesystem_error&)
+          {
+          }
+        }
+      }
+    }
+  }
+
+
+  void FilesystemStorage::Clear()
+  {
+    namespace fs = boost::filesystem;
+    typedef std::set List;
+
+    List result;
+    ListAllFiles(result);
+
+    for (List::const_iterator it = result.begin(); it != result.end(); ++it)
+    {
+      Remove(*it, FileContentType_Unknown /*ignored in this class*/);
+    }
+  }
+
+
+  void FilesystemStorage::Remove(const std::string& uuid,
+                                 FileContentType type)
+  {
+    LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast(type);
+
+    namespace fs = boost::filesystem;
+
+    fs::path p = GetPath(uuid);
+
+    try
+    {
+      fs::remove(p);
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+
+    // Remove the two parent directories, ignoring the error code if
+    // these directories are not empty
+
+    try
+    {
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+      boost::system::error_code err;
+      fs::remove(p.parent_path(), err);
+      fs::remove(p.parent_path().parent_path(), err);
+#else
+      fs::remove(p.parent_path());
+      fs::remove(p.parent_path().parent_path());
+#endif
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+  }
+
+
+  uintmax_t FilesystemStorage::GetCapacity() const
+  {
+    return boost::filesystem::space(root_).capacity;
+  }
+
+  uintmax_t FilesystemStorage::GetAvailableSpace() const
+  {
+    return boost::filesystem::space(root_).available;
+  }
+
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+  FilesystemStorage::FilesystemStorage(std::string root) :
+    fsyncOnWrite_(false)
+  {
+    Setup(root);
+  }
+#endif
+
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+  void FilesystemStorage::Read(std::string& content,
+                               const std::string& uuid,
+                               FileContentType type)
+  {
+    std::unique_ptr buffer(ReadWhole(uuid, type));
+    buffer->MoveToString(content);
+  }
+#endif
+}
diff --git a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h
new file mode 100644
index 0000000..4d9caca
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h
@@ -0,0 +1,110 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class FilesystemStorage cannot be used in sandboxed environments
+#endif
+
+#include "IStorageArea.h"
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea
+  {
+    // TODO REMOVE THIS
+    friend class FilesystemHttpSender;
+    friend class FileStorageAccessor;
+
+  private:
+    boost::filesystem::path root_;
+    bool                    fsyncOnWrite_;
+
+    boost::filesystem::path GetPath(const std::string& uuid) const;
+
+    void Setup(const std::string& root);
+    
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+    // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
+    explicit FilesystemStorage(std::string root);
+#endif
+
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+    // Binary compatibility with Orthanc Framework <= 1.8.2
+    void Read(std::string& content,
+              const std::string& uuid,
+              FileContentType type);
+#endif
+
+  public:
+    explicit FilesystemStorage(const std::string& root);
+
+    FilesystemStorage(const std::string& root,
+                      bool fsyncOnWrite);
+
+    virtual void Create(const std::string& uuid,
+                        const void* content, 
+                        size_t size,
+                        FileContentType type) ORTHANC_OVERRIDE;
+
+    // This flavor is only used in the "DelayedDeletion" plugin
+    IMemoryBuffer* ReadWhole(const std::string& uuid,
+                             FileContentType type);
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */) ORTHANC_OVERRIDE;
+
+    virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type) ORTHANC_OVERRIDE;
+
+    void ListAllFiles(std::set& result) const;
+
+    uintmax_t GetSize(const std::string& uuid) const;
+
+    void Clear();
+
+    uintmax_t GetCapacity() const;
+
+    uintmax_t GetAvailableSpace() const;
+  };
+}
diff --git a/OrthancFramework/Sources/FileStorage/IStorageArea.h b/OrthancFramework/Sources/FileStorage/IStorageArea.h
new file mode 100644
index 0000000..dee16e2
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h
@@ -0,0 +1,91 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Compatibility.h"
+#include "../Enumerations.h"
+#include "../IMemoryBuffer.h"
+
+#include 
+#include 
+
+
+namespace Orthanc
+{
+  class DicomInstanceToStore;
+
+  class IStorageArea : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageArea()
+    {
+    }
+
+    virtual void Create(const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type) = 0;
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */) = 0;
+
+    virtual bool HasEfficientReadRange() const = 0;
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type) = 0;
+  };
+
+
+  // storage area with customData (customData are used only in plugins)
+  class IPluginStorageArea : public boost::noncopyable
+  {
+  public:
+    virtual ~IPluginStorageArea()
+    {
+    }
+
+    virtual void Create(std::string& customData /* out */,
+                        const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type,
+                        CompressionType compression,
+                        const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) = 0;
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */,
+                                     const std::string& customData) = 0;
+
+    virtual bool HasEfficientReadRange() const = 0;
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData) = 0;
+  };
+}
diff --git a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp
new file mode 100644
index 0000000..935c42c
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp
@@ -0,0 +1,150 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "MemoryStorageArea.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../StringMemoryBuffer.h"
+#include "../Toolbox.h"
+
+namespace Orthanc
+{
+  MemoryStorageArea::~MemoryStorageArea()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+  }
+    
+  void MemoryStorageArea::Create(const std::string& uuid,
+                                 const void* content,
+                                 size_t size,
+                                 FileContentType type)
+  {
+    LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << static_cast(type)
+              << "\" type (size: " << Toolbox::GetHumanFileSize(size) << ")";
+
+    Mutex::ScopedLock lock(mutex_);
+
+    if (size != 0 &&
+        content == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (content_.find(uuid) != content_.end())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      content_[uuid] = new std::string(reinterpret_cast(content), size);
+    }
+  }
+
+  
+  IMemoryBuffer* MemoryStorageArea::ReadRange(const std::string& uuid,
+                                              FileContentType type,
+                                              uint64_t start /* inclusive */,
+                                              uint64_t end /* exclusive */)
+  {
+    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \""
+              << static_cast(type) << "\" content type "
+              << "(range from " << start << " to " << end << ")";
+
+    if (start > end)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+    else if (start == end)
+    {
+      return new StringMemoryBuffer;
+    }
+    else
+    {
+      const uint64_t size = end - start;
+      if (static_cast(static_cast(size)) != size)
+      {
+        throw OrthancException(ErrorCode_InternalError, "Buffer larger than 4GB, which is too large for Orthanc running in 32bits");
+      }
+
+      Mutex::ScopedLock lock(mutex_);
+
+      Content::const_iterator found = content_.find(uuid);
+
+      if (found == content_.end())
+      {
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+      else if (found->second == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else if (end > found->second->size())
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
+      else
+      {
+        std::string range;
+        range.resize(static_cast(size));
+        assert(!range.empty());
+
+        memcpy(&range[0], &found->second[start], range.size());
+        
+        return StringMemoryBuffer::CreateFromSwap(range);
+      }
+    }
+  }
+
+
+  void MemoryStorageArea::Remove(const std::string& uuid,
+                                 FileContentType type)
+  {
+    LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast(type);
+
+    Mutex::ScopedLock lock(mutex_);
+
+    Content::iterator found = content_.find(uuid);
+    
+    if (found == content_.end())
+    {
+      // Ignore second removal
+    }
+    else if (found->second == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      delete found->second;
+      content_.erase(found);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h
new file mode 100644
index 0000000..318d0b9
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h
@@ -0,0 +1,65 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IStorageArea.h"
+
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+#include "../MultiThreading/Mutex.h"
+
+#include 
+
+namespace Orthanc
+{
+  class MemoryStorageArea : public IStorageArea
+  {
+  private:
+    typedef std::map  Content;
+    
+    Mutex    mutex_;
+    Content  content_;
+    
+  public:
+    virtual ~MemoryStorageArea();
+    
+    virtual void Create(const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type) ORTHANC_OVERRIDE;
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */) ORTHANC_OVERRIDE;
+    
+    virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type) ORTHANC_OVERRIDE;
+  };
+}
diff --git a/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.cpp b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.cpp
new file mode 100644
index 0000000..4e5c90e
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.cpp
@@ -0,0 +1,53 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "PluginStorageAreaAdapter.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  PluginStorageAreaAdapter::PluginStorageAreaAdapter(IStorageArea* storage) :
+    storage_(storage)
+  {
+    if (storage == NULL)
+    {
+      throw OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  void PluginStorageAreaAdapter::Create(std::string& customData,
+                                        const std::string& uuid,
+                                        const void* content,
+                                        size_t size,
+                                        FileContentType type,
+                                        CompressionType compression,
+                                        const DicomInstanceToStore* dicomInstance)
+  {
+    customData.clear();
+    storage_->Create(uuid, content, size, type);
+  }
+}
diff --git a/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h
new file mode 100644
index 0000000..778eb94
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h
@@ -0,0 +1,69 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "IStorageArea.h"
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC PluginStorageAreaAdapter : public IPluginStorageArea
+  {
+  private:
+    std::unique_ptr storage_;
+
+  public:
+    explicit PluginStorageAreaAdapter(IStorageArea* storage /* takes ownership */);
+
+    virtual void Create(std::string& customData,
+                        const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type,
+                        CompressionType compression,
+                        const DicomInstanceToStore* dicomInstance) ORTHANC_OVERRIDE;
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */,
+                                     const std::string& customData) ORTHANC_OVERRIDE
+    {
+      return storage_->ReadRange(uuid, type, start, end);
+    }
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData) ORTHANC_OVERRIDE
+    {
+      storage_->Remove(uuid, type);
+    }
+
+    virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
+    {
+      return storage_->HasEfficientReadRange();
+    }
+  };
+}
diff --git a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp
new file mode 100644
index 0000000..f3a5bc1
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp
@@ -0,0 +1,788 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "StorageAccessor.h"
+#include "StorageCache.h"
+
+#include "../Logging.h"
+#include "../StringMemoryBuffer.h"
+#include "../Compression/ZlibCompressor.h"
+#include "../MetricsRegistry.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../HttpServer/HttpStreamTranscoder.h"
+#endif
+
+#include 
+
+
+static const std::string METRICS_CREATE_DURATION = "orthanc_storage_create_duration_ms";
+static const std::string METRICS_READ_DURATION = "orthanc_storage_read_duration_ms";
+static const std::string METRICS_REMOVE_DURATION = "orthanc_storage_remove_duration_ms";
+static const std::string METRICS_READ_BYTES = "orthanc_storage_read_bytes";
+static const std::string METRICS_WRITTEN_BYTES = "orthanc_storage_written_bytes";
+static const std::string METRICS_CACHE_HIT_COUNT = "orthanc_storage_cache_hit_count";
+static const std::string METRICS_CACHE_MISS_COUNT = "orthanc_storage_cache_miss_count";
+
+
+namespace Orthanc
+{
+  void StorageAccessor::Range::SanityCheck() const
+  {
+    if (hasStart_ && hasEnd_ && start_ > end_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  StorageAccessor::Range::Range():
+    hasStart_(false),
+    start_(0),
+    hasEnd_(false),
+    end_(0)
+  {
+  }
+
+  void StorageAccessor::Range::SetStartInclusive(uint64_t start)
+  {
+    hasStart_ = true;
+    start_ = start;
+  }
+
+  void StorageAccessor::Range::SetEndInclusive(uint64_t end)
+  {
+    hasEnd_ = true;
+    end_ = end;
+  }
+
+  uint64_t StorageAccessor::Range::GetStartInclusive() const
+  {
+    if (!hasStart_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (hasEnd_ && start_ > end_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return start_;
+    }
+  }
+
+  uint64_t StorageAccessor::Range::GetEndInclusive() const
+  {
+    if (!hasEnd_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (hasStart_ && start_ > end_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return end_;
+    }
+  }
+
+  std::string StorageAccessor::Range::FormatHttpContentRange(uint64_t fullSize) const
+  {
+    SanityCheck();
+
+    if (fullSize == 0 ||
+        (hasStart_ && start_ >= fullSize) ||
+        (hasEnd_ && end_ >= fullSize))
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    std::string s = "bytes ";
+
+    if (hasStart_)
+    {
+      s += boost::lexical_cast(start_);
+    }
+    else
+    {
+      s += "0";
+    }
+
+    s += "-";
+
+    if (hasEnd_)
+    {
+      s += boost::lexical_cast(end_);
+    }
+    else
+    {
+      s += boost::lexical_cast(fullSize - 1);
+    }
+
+    return s + "/" + boost::lexical_cast(fullSize);
+  }
+
+  void StorageAccessor::Range::Extract(std::string &target,
+                                       const std::string &source) const
+  {
+    SanityCheck();
+
+    if (hasStart_ && start_ >= source.size())
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasEnd_ && end_ >= source.size())
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasStart_ && hasEnd_)
+    {
+      target = source.substr(start_, end_ - start_ + 1);
+    }
+    else if (hasStart_)
+    {
+      target = source.substr(start_, source.size() - start_);
+    }
+    else if (hasEnd_)
+    {
+      target = source.substr(0, end_ + 1);
+    }
+    else
+    {
+      target = source;
+    }
+  }
+
+  uint64_t StorageAccessor::Range::GetContentLength(uint64_t fullSize) const
+  {
+    SanityCheck();
+
+    if (fullSize == 0)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasStart_ && start_ >= fullSize)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasEnd_ && end_ >= fullSize)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    if (hasStart_ && hasEnd_)
+    {
+      return end_ - start_ + 1;
+    }
+    else if (hasStart_)
+    {
+      return fullSize - start_;
+    }
+    else if (hasEnd_)
+    {
+      return end_ + 1;
+    }
+    else
+    {
+      return fullSize;
+    }
+  }
+
+  StorageAccessor::Range StorageAccessor::Range::ParseHttpRange(const std::string& s)
+  {
+    static const std::string BYTES = "bytes=";
+
+    if (!boost::starts_with(s, BYTES))
+    {
+      throw OrthancException(ErrorCode_BadRange);  // Range not satisfiable
+    }
+
+    std::vector tokens;
+    Orthanc::Toolbox::TokenizeString(tokens, s.substr(BYTES.length()), '-');
+
+    if (tokens.size() != 2)
+    {
+      throw OrthancException(ErrorCode_BadRange);
+    }
+
+    Range range;
+
+    uint64_t tmp;
+    if (!tokens[0].empty())
+    {
+      if (SerializationToolbox::ParseUnsignedInteger64(tmp, tokens[0]))
+      {
+        range.SetStartInclusive(tmp);
+      }
+    }
+
+    if (!tokens[1].empty())
+    {
+      if (SerializationToolbox::ParseUnsignedInteger64(tmp, tokens[1]))
+      {
+        range.SetEndInclusive(tmp);
+      }
+    }
+
+    range.SanityCheck();
+    return range;
+  }
+
+  class StorageAccessor::MetricsTimer : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr  timer_;
+
+  public:
+    MetricsTimer(StorageAccessor& that,
+                 const std::string& name)
+    {
+      if (that.metrics_ != NULL)
+      {
+        timer_.reset(new MetricsRegistry::Timer(*that.metrics_, name));
+      }
+    }
+  };
+
+
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area) :
+    area_(area),
+    cache_(NULL),
+    metrics_(NULL)
+  {
+  }
+  
+
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
+                                   StorageCache& cache) :
+    area_(area),
+    cache_(&cache),
+    metrics_(NULL)
+  {
+  }
+
+
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
+                                   MetricsRegistry& metrics) :
+    area_(area),
+    cache_(NULL),
+    metrics_(&metrics)
+  {
+  }
+
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
+                                   StorageCache& cache,
+                                   MetricsRegistry& metrics) :
+    area_(area),
+    cache_(&cache),
+    metrics_(&metrics)
+  {
+  }
+
+
+  void StorageAccessor::Write(FileInfo& info,
+                              const void* data,
+                              size_t size,
+                              FileContentType type,
+                              CompressionType compression,
+                              bool storeMd5,
+                              const DicomInstanceToStore* instance)
+  {
+    const std::string uuid = Toolbox::GenerateUuid();
+
+    std::string md5;
+
+    if (storeMd5)
+    {
+      Toolbox::ComputeMD5(md5, data, size);
+    }
+
+    std::string customData;
+
+    switch (compression)
+    {
+      case CompressionType_None:
+      {
+        {
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
+          area_.Create(customData, uuid, data, size, type, compression, instance);
+        }
+
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, size);
+        }
+        
+        if (cache_ != NULL)
+        {
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);
+        }
+
+        info = FileInfo(uuid, type, size, md5);
+        info.SetCustomData(customData);
+        return;
+      }
+
+      case CompressionType_ZlibWithSize:
+      {
+        ZlibCompressor zlib;
+
+        std::string compressed;
+        zlib.Compress(compressed, data, size);
+
+        std::string compressedMD5;
+      
+        if (storeMd5)
+        {
+          Toolbox::ComputeMD5(compressedMD5, compressed);
+        }
+
+        {
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
+
+          if (compressed.size() > 0)
+          {
+            area_.Create(customData, uuid, &compressed[0], compressed.size(), type, compression, instance);
+          }
+          else
+          {
+            area_.Create(customData, uuid, NULL, 0, type, compression, instance);
+          }
+        }
+
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, compressed.size());
+        }
+
+        if (cache_ != NULL)
+        {
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);    // always add uncompressed data to cache
+        }
+
+        info = FileInfo(uuid, type, size, md5,
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+        info.SetCustomData(customData);
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  void StorageAccessor::Read(std::string& content,
+                             const FileInfo& info)
+  {
+    if (cache_ == NULL)
+    {
+      ReadWholeInternal(content, info);
+    }
+    else
+    {
+      StorageCache::Accessor cacheAccessor(*cache_);
+
+      if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
+      {
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+        }
+
+        ReadWholeInternal(content, info);
+
+        // always store the uncompressed data in cache
+        cacheAccessor.Add(info.GetUuid(), info.GetContentType(), content);
+      } 
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+      }
+    }
+  }
+
+  void StorageAccessor::ReadWholeInternal(std::string& content,
+                                          const FileInfo& info)
+  {
+    switch (info.GetCompressionType())
+    {
+      case CompressionType_None:
+      {
+        std::unique_ptr buffer;
+
+        {
+          MetricsTimer timer(*this, METRICS_READ_DURATION);
+          buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
+        }
+
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize());
+        }
+
+        buffer->MoveToString(content);
+
+        break;
+      }
+
+      case CompressionType_ZlibWithSize:
+      {
+        ZlibCompressor zlib;
+
+        std::unique_ptr compressed;
+        
+        {
+          MetricsTimer timer(*this, METRICS_READ_DURATION);
+          compressed.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
+        }
+        
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_READ_BYTES, compressed->GetSize());
+        }
+
+        zlib.Uncompress(content, compressed->GetData(), compressed->GetSize());
+
+        break;
+      }
+
+      default:
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+    // TODO Check the validity of the uncompressed MD5?
+  }
+
+
+  void StorageAccessor::ReadRaw(std::string& content,
+                                const FileInfo& info)
+  {
+    if (cache_ == NULL || info.GetCompressionType() != CompressionType_None)
+    {
+      ReadRawInternal(content, info);
+    }
+    else
+    {// use the cache only if the data is uncompressed.
+      StorageCache::Accessor cacheAccessor(*cache_);
+
+      if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
+      {
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+        }
+
+        ReadRawInternal(content, info);
+
+        cacheAccessor.Add(info.GetUuid(), info.GetContentType(), content);
+      }
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+      }
+    }
+  }
+
+  void StorageAccessor::ReadRawInternal(std::string& content,
+                                        const FileInfo& info)
+  {
+    std::unique_ptr buffer;
+
+    {
+      MetricsTimer timer(*this, METRICS_READ_DURATION);
+      buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
+    }
+
+    if (metrics_ != NULL)
+    {
+      metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize());
+    }
+
+    buffer->MoveToString(content);
+  }
+
+
+  void StorageAccessor::Remove(const std::string& fileUuid,
+                               FileContentType type,
+                               const std::string& customData)
+  {
+    if (cache_ != NULL)
+    {
+      cache_->Invalidate(fileUuid, type);
+    }
+
+    {
+      MetricsTimer timer(*this, METRICS_REMOVE_DURATION);
+      area_.Remove(fileUuid, type, customData);
+    }
+  }
+  
+
+  void StorageAccessor::Remove(const FileInfo &info)
+  {
+    Remove(info.GetUuid(), info.GetContentType(), info.GetCustomData());
+  }
+
+
+  void StorageAccessor::ReadStartRange(std::string& target,
+                                       const FileInfo& info,
+                                       uint64_t end /* exclusive */)
+  {
+    if (cache_ == NULL)
+    {
+      ReadStartRangeInternal(target, info, end);
+    }
+    else
+    {
+      StorageCache::Accessor accessorStartRange(*cache_);
+      if (!accessorStartRange.FetchStartRange(target, info.GetUuid(), info.GetContentType(), end))
+      {
+        // the start range is not in cache, let's check if the whole file is
+        StorageCache::Accessor accessorWhole(*cache_);
+        if (!accessorWhole.Fetch(target, info.GetUuid(), info.GetContentType()))
+        {
+          if (metrics_ != NULL)
+          {
+            metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+          }
+
+          // if nothing is in the cache, let's read and cache only the start
+          ReadStartRangeInternal(target, info, end);
+          accessorStartRange.AddStartRange(info.GetUuid(), info.GetContentType(), target);
+        }
+        else
+        {
+          if (metrics_ != NULL)
+          {
+            metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+          }
+
+          // we have read the whole file, check size and resize if needed
+          if (target.size() < end)
+          {
+            throw OrthancException(ErrorCode_CorruptedFile);
+          }
+
+          target.resize(end);
+        }
+      }
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+      }
+    }
+  }
+
+  void StorageAccessor::ReadStartRangeInternal(std::string& target,
+                                                const FileInfo& info,
+                                                uint64_t end /* exclusive */)
+  {
+    std::unique_ptr buffer;
+
+    {
+      MetricsTimer timer(*this, METRICS_READ_DURATION);
+      buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end, info.GetCustomData()));
+      assert(buffer->GetSize() == end);
+    }
+
+    if (metrics_ != NULL)
+    {
+      metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize());
+    }
+
+    buffer->MoveToString(target);
+  }
+
+
+  void StorageAccessor::ReadRange(std::string &target,
+                                  const FileInfo &info,
+                                  const Range &range,
+                                  bool uncompressIfNeeded)
+  {
+    if (uncompressIfNeeded &&
+        info.GetCompressionType() != CompressionType_None)
+    {
+      // An uncompression is needed in this case
+      if (cache_ != NULL)
+      {
+        StorageCache::Accessor cacheAccessor(*cache_);
+
+        std::string content;
+        if (cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
+        {
+          range.Extract(target, content);
+          return;
+        }
+      }
+
+      std::string content;
+      Read(content, info);
+      range.Extract(target, content);
+    }
+    else
+    {
+      // Access to the raw attachment is sufficient in this case
+      if (info.GetCompressionType() == CompressionType_None &&
+          cache_ != NULL)
+      {
+        // Check out whether the raw attachment is already present in the cache, by chance
+        StorageCache::Accessor cacheAccessor(*cache_);
+
+        std::string content;
+        if (cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
+        {
+          range.Extract(target, content);
+          return;
+        }
+      }
+
+      if (range.HasEnd() &&
+        range.GetEndInclusive() >= info.GetCompressedSize())
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
+
+      std::unique_ptr buffer;
+
+      if (range.HasStart() &&
+          range.HasEnd())
+      {
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1, info.GetCustomData()));
+      }
+      else if (range.HasStart())
+      {
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize(), info.GetCustomData()));
+      }
+      else if (range.HasEnd())
+      {
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1, info.GetCustomData()));
+      }
+      else
+      {
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
+      }
+
+      buffer->MoveToString(target);
+    }
+  }
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void StorageAccessor::SetupSender(BufferHttpSender& sender,
+                                    const FileInfo& info,
+                                    const std::string& mime)
+  {
+    Read(sender.GetBuffer(), info);
+
+    sender.SetContentType(mime);
+
+    const char* extension;
+    switch (info.GetContentType())
+    {
+      case FileContentType_Dicom:
+      case FileContentType_DicomUntilPixelData:
+        extension = ".dcm";
+        break;
+
+      case FileContentType_DicomAsJson:
+        extension = ".json";
+        break;
+
+      default:
+        // Non-standard content type
+        extension = "";
+    }
+
+    sender.SetContentFilename(info.GetUuid() + std::string(extension));
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void StorageAccessor::AnswerFile(HttpOutput& output,
+                                   const FileInfo& info,
+                                   MimeType mime,
+                                   const std::string& contentFilename)
+  {
+    AnswerFile(output, info, EnumerationToString(mime), contentFilename);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void StorageAccessor::AnswerFile(HttpOutput& output,
+                                   const FileInfo& info,
+                                   const std::string& mime,
+                                   const std::string& contentFilename)
+  {
+    BufferHttpSender sender;
+    SetupSender(sender, info, mime);
+    sender.SetContentFilename(contentFilename);
+  
+    HttpStreamTranscoder transcoder(sender, CompressionType_None); // since 1.11.2, the storage accessor only returns uncompressed buffers
+    output.Answer(transcoder);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void StorageAccessor::AnswerFile(RestApiOutput& output,
+                                   const FileInfo& info,
+                                   MimeType mime,
+                                   const std::string& contentFilename)
+  {
+    AnswerFile(output, info, EnumerationToString(mime), contentFilename);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void StorageAccessor::AnswerFile(RestApiOutput& output,
+                                   const FileInfo& info,
+                                   const std::string& mime,
+                                   const std::string& contentFilename)
+  {
+    BufferHttpSender sender;
+    SetupSender(sender, info, mime);
+    sender.SetContentFilename(contentFilename);
+
+    HttpStreamTranscoder transcoder(sender, CompressionType_None); // since 1.11.2, the storage accessor only returns uncompressed buffers
+    output.AnswerStream(transcoder);
+  }
+#endif
+
+}
diff --git a/OrthancFramework/Sources/FileStorage/StorageAccessor.h b/OrthancFramework/Sources/FileStorage/StorageAccessor.h
new file mode 100644
index 0000000..1bb350c
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h
@@ -0,0 +1,199 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class StorageAccessor cannot be used in sandboxed environments
+#endif
+
+#if !defined(ORTHANC_ENABLE_CIVETWEB)
+#  error Macro ORTHANC_ENABLE_CIVETWEB must be defined to use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_MONGOOSE)
+#  error Macro ORTHANC_ENABLE_MONGOOSE must be defined to use this file
+#endif
+
+#include "IStorageArea.h"
+#include "FileInfo.h"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../HttpServer/BufferHttpSender.h"
+#  include "../RestApi/RestApiOutput.h"
+#endif
+
+#include 
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  class MetricsRegistry;
+  class StorageCache;
+
+  /**
+   * This class handles the compression/decompression of the raw files
+   * contained in the storage area, and monitors timing metrics (if
+   * enabled).
+   **/
+  class ORTHANC_PUBLIC StorageAccessor : boost::noncopyable
+  {
+  public:
+    class ORTHANC_PUBLIC Range
+    {
+    private:
+      bool      hasStart_;
+      uint64_t  start_;
+      bool      hasEnd_;
+      uint64_t  end_;
+
+      void SanityCheck() const;
+
+    public:
+      Range();
+
+      void SetStartInclusive(uint64_t start);
+
+      void SetEndInclusive(uint64_t end);
+
+      bool HasStart() const
+      {
+        return hasStart_;
+      }
+
+      bool HasEnd() const
+      {
+        return hasEnd_;
+      }
+
+      uint64_t GetStartInclusive() const;
+
+      uint64_t GetEndInclusive() const;
+
+      std::string FormatHttpContentRange(uint64_t fullSize) const;
+
+      void Extract(std::string& target,
+                   const std::string& source) const;
+
+      uint64_t GetContentLength(uint64_t fullSize) const;
+
+      static Range ParseHttpRange(const std::string& s);
+    };
+
+  private:
+    class MetricsTimer;
+
+    IPluginStorageArea&     area_;
+    StorageCache*     cache_;
+    MetricsRegistry*  metrics_;
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+    void SetupSender(BufferHttpSender& sender,
+                     const FileInfo& info,
+                     const std::string& mime);
+#endif
+
+  public:
+    explicit StorageAccessor(IPluginStorageArea& area);
+
+    StorageAccessor(IPluginStorageArea& area,
+                    StorageCache& cache);
+
+    StorageAccessor(IPluginStorageArea& area,
+                    MetricsRegistry& metrics);
+
+    StorageAccessor(IPluginStorageArea& area,
+                    StorageCache& cache,
+                    MetricsRegistry& metrics);
+
+    void Write(FileInfo& info /* out */,
+               const void* data,
+               size_t size,
+               FileContentType type,
+               CompressionType compression,
+               bool storeMd5,
+               const DicomInstanceToStore* instance);
+
+    void Read(std::string& content,
+              const FileInfo& info);
+
+    void ReadRaw(std::string& content,
+                 const FileInfo& info);
+
+    void ReadStartRange(std::string& target,
+                        const FileInfo& info,
+                        uint64_t end /* exclusive */);
+
+    void Remove(const std::string& fileUuid,
+                FileContentType type,
+                const std::string& customData);
+
+    void Remove(const FileInfo& info);
+
+    void ReadRange(std::string& target,
+                   const FileInfo& info,
+                   const Range& range,
+                   bool uncompressIfNeeded);
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+    void AnswerFile(HttpOutput& output,
+                    const FileInfo& info,
+                    MimeType mime,
+                    const std::string& contentFilename);
+
+    void AnswerFile(HttpOutput& output,
+                    const FileInfo& info,
+                    const std::string& mime,
+                    const std::string& contentFilename);
+
+    void AnswerFile(RestApiOutput& output,
+                    const FileInfo& info,
+                    MimeType mime,
+                    const std::string& contentFilename);
+
+    void AnswerFile(RestApiOutput& output,
+                    const FileInfo& info,
+                    const std::string& mime,
+                    const std::string& contentFilename);
+#endif
+
+  private:
+    void ReadStartRangeInternal(std::string& target,
+                                const FileInfo& info,
+                                uint64_t end /* exclusive */);
+
+    void ReadWholeInternal(std::string& content,
+                           const FileInfo& info);
+
+    void ReadRawInternal(std::string& content,
+                         const FileInfo& info);
+
+  };
+}
diff --git a/OrthancFramework/Sources/FileStorage/StorageCache.cpp b/OrthancFramework/Sources/FileStorage/StorageCache.cpp
new file mode 100644
index 0000000..8f5a6a7
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.cpp
@@ -0,0 +1,201 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "StorageCache.h"
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  static std::string GetCacheKeyFullFile(const std::string& uuid,
+                                         FileContentType contentType)
+  {
+    return uuid + ":" + boost::lexical_cast(contentType) + ":1";
+  }
+
+
+  static std::string GetCacheKeyStartRange(const std::string& uuid,
+                                           FileContentType contentType)
+  {
+    return uuid + ":" + boost::lexical_cast(contentType) + ":0";
+  }
+
+
+  static std::string GetCacheKeyTranscodedInstance(const std::string& uuid,
+                                                   DicomTransferSyntax transferSyntax)
+  {
+    return uuid + ":ts:" + GetTransferSyntaxUid(transferSyntax);
+  }
+
+
+  void StorageCache::SetMaximumSize(size_t size)
+  {
+    cache_.SetMaximumSize(size);
+  }
+  
+
+  void StorageCache::Invalidate(const std::string& uuid,
+                                FileContentType contentType)
+  {
+    std::set transferSyntaxes;
+
+    {
+      boost::mutex::scoped_lock lock(subKeysMutex_);
+      transferSyntaxes = subKeysTransferSyntax_;
+    }
+
+    // invalidate full file, start range file and possible transcoded instances
+    const std::string keyFullFile = GetCacheKeyFullFile(uuid, contentType);
+    cache_.Invalidate(keyFullFile);
+
+    const std::string keyPartialFile = GetCacheKeyStartRange(uuid, contentType);
+    cache_.Invalidate(keyPartialFile);
+    
+    for (std::set::const_iterator it = transferSyntaxes.begin(); it != transferSyntaxes.end(); ++it)
+    {
+      const std::string keyTransferSyntax = GetCacheKeyTranscodedInstance(uuid, *it);
+      cache_.Invalidate(keyTransferSyntax);
+    }
+  }
+
+
+  StorageCache::Accessor::Accessor(StorageCache& cache)
+  : MemoryStringCache::Accessor(cache.cache_),
+    storageCache_(cache)
+  {
+  }
+
+  void StorageCache::Accessor::Add(const std::string& uuid, 
+                                   FileContentType contentType,
+                                   const std::string& value)
+  {
+
+    std::string key = GetCacheKeyFullFile(uuid, contentType);
+    MemoryStringCache::Accessor::Add(key, value);
+  }
+
+  void StorageCache::Accessor::AddStartRange(const std::string& uuid, 
+                                             FileContentType contentType,
+                                             const std::string& value)
+  {
+    const std::string key = GetCacheKeyStartRange(uuid, contentType);
+    MemoryStringCache::Accessor::Add(key, value);
+  }
+
+  void StorageCache::Accessor::Add(const std::string& uuid, 
+                                   FileContentType contentType,
+                                   const void* buffer,
+                                   size_t size)
+  {
+    const std::string key = GetCacheKeyFullFile(uuid, contentType);
+    MemoryStringCache::Accessor::Add(key, reinterpret_cast(buffer), size);
+  }                                   
+
+  bool StorageCache::Accessor::Fetch(std::string& value, 
+                                     const std::string& uuid,
+                                     FileContentType contentType)
+  {
+    const std::string key = GetCacheKeyFullFile(uuid, contentType);
+    if (MemoryStringCache::Accessor::Fetch(value, key))
+    {
+      LOG(INFO) << "Read attachment \"" << uuid << "\" with content type "
+                << boost::lexical_cast(contentType) << " from cache";
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  bool StorageCache::Accessor::FetchTranscodedInstance(std::string& value, 
+                                                       const std::string& uuid,
+                                                       DicomTransferSyntax targetSyntax)
+  {
+    const std::string key = GetCacheKeyTranscodedInstance(uuid, targetSyntax);
+    if (MemoryStringCache::Accessor::Fetch(value, key))
+    {
+      LOG(INFO) << "Read instance \"" << uuid << "\" transcoded to "
+                << GetTransferSyntaxUid(targetSyntax) << " from cache";
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  void StorageCache::Accessor::AddTranscodedInstance(const std::string& uuid,
+                                                     DicomTransferSyntax targetSyntax,
+                                                     const void* buffer,
+                                                     size_t size)
+  {
+    {
+      boost::mutex::scoped_lock lock(storageCache_.subKeysMutex_);
+      storageCache_.subKeysTransferSyntax_.insert(targetSyntax);
+    }
+
+    const std::string key = GetCacheKeyTranscodedInstance(uuid, targetSyntax);
+    MemoryStringCache::Accessor::Add(key, reinterpret_cast(buffer), size);
+  }
+
+  bool StorageCache::Accessor::FetchStartRange(std::string& value, 
+                                               const std::string& uuid,
+                                               FileContentType contentType,
+                                               uint64_t end /* exclusive */)
+  {
+    const std::string keyPartialFile = GetCacheKeyStartRange(uuid, contentType);
+    if (MemoryStringCache::Accessor::Fetch(value, keyPartialFile) && value.size() >= end)
+    {
+      if (value.size() > end)  // the start range that has been cached is larger than the requested value
+      {
+        value.resize(end);
+      }
+
+      LOG(INFO) << "Read start of attachment \"" << uuid << "\" with content type "
+                << boost::lexical_cast(contentType) << " from cache";
+      return true;
+    }
+
+    return false;
+  }
+
+
+  size_t StorageCache::GetCurrentSize() const
+  {
+    return cache_.GetCurrentSize();
+  }
+  
+  size_t StorageCache::GetNumberOfItems() const
+  {
+    return cache_.GetNumberOfItems();
+  }
+
+}
diff --git a/OrthancFramework/Sources/FileStorage/StorageCache.h b/OrthancFramework/Sources/FileStorage/StorageCache.h
new file mode 100644
index 0000000..fbd7449
--- /dev/null
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.h
@@ -0,0 +1,125 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../Cache/MemoryStringCache.h"
+
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+
+#include 
+#include 
+
+namespace Orthanc
+{
+   /**
+   *  Note: this class is thread safe
+   **/
+   class ORTHANC_PUBLIC StorageCache : public boost::noncopyable
+    {
+    public:
+
+      // The StorageCache is only accessible through this accessor.
+      // It will make sure that only one user will fill load data and fill
+      // the cache if multiple users try to access the same item at the same time.
+      // This scenario happens a lot when multiple workers from a viewer access 
+      // the same file.
+      class Accessor : public MemoryStringCache::Accessor
+      {
+        StorageCache& storageCache_;
+      public:
+        explicit Accessor(StorageCache& cache);
+
+        void Add(const std::string& uuid, 
+                 FileContentType contentType,
+                 const std::string& value);
+
+        void AddStartRange(const std::string& uuid, 
+                           FileContentType contentType,
+                           const std::string& value);
+
+        void Add(const std::string& uuid, 
+                 FileContentType contentType,
+                 const void* buffer,
+                 size_t size);
+
+        bool Fetch(std::string& value, 
+                   const std::string& uuid,
+                   FileContentType contentType);
+
+        bool FetchStartRange(std::string& value, 
+                             const std::string& uuid,
+                             FileContentType contentType,
+                             uint64_t end /* exclusive */);
+
+        bool FetchTranscodedInstance(std::string& value, 
+                                     const std::string& uuid,
+                                     DicomTransferSyntax targetSyntax);
+
+        void AddTranscodedInstance(const std::string& uuid,
+                                   DicomTransferSyntax targetSyntax,
+                                   const void* buffer,
+                                   size_t size);
+      };
+
+    private:
+      MemoryStringCache             cache_;
+      std::set subKeysTransferSyntax_;
+      boost::mutex                  subKeysMutex_;
+
+    public:
+      void SetMaximumSize(size_t size);
+
+      void Invalidate(const std::string& uuid,
+                      FileContentType contentType);
+
+      size_t GetCurrentSize() const;
+      
+      size_t GetNumberOfItems() const;
+
+    private:
+      void Add(const std::string& uuid, 
+               FileContentType contentType,
+               const std::string& value);
+
+      void AddStartRange(const std::string& uuid, 
+                         FileContentType contentType,
+                         const std::string& value);
+
+      void Add(const std::string& uuid, 
+               FileContentType contentType,
+               const void* buffer,
+               size_t size);
+
+      bool Fetch(std::string& value, 
+                 const std::string& uuid,
+                 FileContentType contentType);
+
+      bool FetchStartRange(std::string& value, 
+                           const std::string& uuid,
+                           FileContentType contentType,
+                           uint64_t end /* exclusive */);
+
+    };
+}
diff --git a/OrthancFramework/Sources/HttpClient.cpp b/OrthancFramework/Sources/HttpClient.cpp
new file mode 100644
index 0000000..cda8fe3
--- /dev/null
+++ b/OrthancFramework/Sources/HttpClient.cpp
@@ -0,0 +1,1426 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "HttpClient.h"
+
+#include "Toolbox.h"
+#include "OrthancException.h"
+#include "Logging.h"
+#include "ChunkedBuffer.h"
+#include "SystemToolbox.h"
+
+#include 
+#include 
+#include 
+#include 
+
+// Default timeout = 60 seconds (in Orthanc <= 1.5.6, it was 10 seconds)
+static const unsigned int DEFAULT_HTTP_TIMEOUT = 60;
+
+
+#if ORTHANC_ENABLE_PKCS11 == 1
+#  include "Pkcs11.h"
+#endif
+
+
+extern "C"
+{
+  static CURLcode GetHttpStatus(CURLcode code, CURL* curl, long* status, const std::string& url)
+  {
+    if (code == CURLE_OK)
+    {
+      code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
+      return code;
+    }
+    else
+    {
+      *status = 0;
+      return code;
+    }
+  }
+}
+
+// This is a dummy wrapper function to suppress any OpenSSL-related
+// problem in valgrind. Inlining is prevented.
+#if defined(__GNUC__) || defined(__clang__)
+__attribute__((noinline)) 
+#endif
+static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status, const std::string& url)
+{
+#if ORTHANC_ENABLE_SSL == 1
+  return GetHttpStatus(curl_easy_perform(curl), curl, status, url);
+#else
+  throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                  "Orthanc was compiled without SSL support, "
+                                  "cannot make HTTPS request");
+#endif
+}
+
+
+
+namespace Orthanc
+{
+  static CURLcode CheckCode(CURLcode code)
+  {
+    if (code == CURLE_NOT_BUILT_IN)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Your libcurl does not contain a required feature, "
+                             "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF");
+    }
+
+    if (code != CURLE_OK)
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "libCURL error: " + std::string(curl_easy_strerror(code)));
+    }
+
+    return code;
+  }
+
+  static CURLcode CheckCode(CURLcode code, const std::string& url)
+  {
+    if (code == CURLE_NOT_BUILT_IN)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Your libcurl does not contain a required feature, "
+                             "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF");
+    }
+
+    if (code != CURLE_OK)
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "libCURL error: " + std::string(curl_easy_strerror(code)) + " while accessing " + url);
+    }
+
+    return code;
+  }
+
+
+  // RAII pattern around a "curl_slist"
+  class HttpClient::CurlHeaders : public boost::noncopyable
+  {
+  private:
+    struct curl_slist *content_;
+    bool               isChunkedTransfer_;
+    bool               hasExpect_;
+
+  public:
+    CurlHeaders() :
+      content_(NULL),
+      isChunkedTransfer_(false),
+      hasExpect_(false)
+    {
+    }
+
+    explicit CurlHeaders(const HttpClient::HttpHeaders& headers)
+    {
+      for (HttpClient::HttpHeaders::const_iterator
+             it = headers.begin(); it != headers.end(); ++it)
+      {
+        AddHeader(it->first, it->second);
+      }
+    }
+
+    ~CurlHeaders()
+    {
+      Clear();
+    }
+
+    bool IsEmpty() const
+    {
+      return content_ == NULL;
+    }
+
+    void Clear()
+    {
+      if (content_ != NULL)
+      {
+        curl_slist_free_all(content_);
+        content_ = NULL;
+      }
+
+      isChunkedTransfer_ = false;
+      hasExpect_ = false;
+    }
+
+    void AddHeader(const std::string& key,
+                   const std::string& value)
+    {
+      if (boost::iequals(key, "Expect"))
+      {
+        hasExpect_ = true;
+      }
+
+      if (boost::iequals(key, "Transfer-Encoding") &&
+          value == "chunked")
+      {
+        isChunkedTransfer_ = true;
+      }
+        
+      std::string item = key + ": " + value;
+
+      struct curl_slist *tmp = curl_slist_append(content_, item.c_str());
+        
+      if (tmp == NULL)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+      else
+      {
+        content_ = tmp;
+      }
+    }
+
+    void Assign(CURL* curl) const
+    {
+      CheckCode(curl_easy_setopt(curl, CURLOPT_HTTPHEADER, content_));
+    }
+
+    bool HasExpect() const
+    {
+      return hasExpect_;
+    }
+
+    bool IsChunkedTransfer() const
+    {
+      return isChunkedTransfer_;
+    }
+  };
+
+
+  class HttpClient::CurlRequestBody : public boost::noncopyable
+  {
+  private:
+    HttpClient::IRequestBody*  body_;
+    std::string                pending_;
+    size_t                     pendingPos_;
+
+    size_t CallbackInternal(char* curlBuffer,
+                            size_t curlBufferSize)
+    {
+      if (body_ == NULL)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (curlBufferSize == 0)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (pendingPos_ + curlBufferSize <= pending_.size())
+      {
+        assert(sizeof(char) == 1);
+        memcpy(curlBuffer, &pending_[pendingPos_], curlBufferSize);
+        pendingPos_ += curlBufferSize;
+        return curlBufferSize;
+      }
+      else
+      {
+        ChunkedBuffer buffer;
+        buffer.SetPendingBufferSize(curlBufferSize);
+
+        if (pendingPos_ < pending_.size())
+        {
+          buffer.AddChunk(&pending_[pendingPos_], pending_.size() - pendingPos_);
+        }
+        
+        // Read chunks from the body stream so as to fill the target buffer
+        std::string chunk;
+        
+        while (buffer.GetNumBytes() < curlBufferSize &&
+               body_->ReadNextChunk(chunk))
+        {
+          buffer.AddChunk(chunk);
+        }
+
+        buffer.Flatten(pending_);
+        pendingPos_ = std::min(pending_.size(), curlBufferSize);
+
+        if (pendingPos_ != 0)
+        {
+          memcpy(curlBuffer, pending_.c_str(), pendingPos_);
+        }
+
+        return pendingPos_;
+      }
+    }
+    
+  public:
+    CurlRequestBody() :
+      body_(NULL),
+      pendingPos_(0)
+    {
+    }
+
+    void SetBody(HttpClient::IRequestBody& body)
+    {
+      body_ = &body;
+      pending_.clear();
+      pendingPos_ = 0;
+    }
+
+    void Clear()
+    {
+      body_ = NULL;
+      pending_.clear();
+      pendingPos_ = 0;
+    }
+
+    bool IsValid() const
+    {
+      return body_ != NULL;
+    }
+
+    static size_t Callback(char *buffer, size_t size, size_t nitems, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+        return reinterpret_cast(userdata)->
+          CallbackInternal(buffer, size * nitems);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+  };
+
+
+  class HttpClient::CurlAnswer : public boost::noncopyable
+  {
+  private:
+    HttpClient::IAnswer&  answer_;
+    bool                  headersLowerCase_;
+
+  public:
+    CurlAnswer(HttpClient::IAnswer& answer,
+               bool headersLowerCase) :
+      answer_(answer),
+      headersLowerCase_(headersLowerCase)
+    {
+    }
+
+    static size_t HeaderCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+
+        size_t length = size * nmemb;
+        if (length == 0)
+        {
+          return 0;
+        }
+        else
+        {
+          std::string s(reinterpret_cast(buffer), length);
+          std::size_t colon = s.find(':');
+          std::size_t eol = s.find("\r\n");
+          if (colon != std::string::npos &&
+              eol != std::string::npos)
+          {
+            CurlAnswer& that = *(static_cast(userdata));
+            std::string tmp(s.substr(0, colon));
+
+            if (that.headersLowerCase_)
+            {
+              Toolbox::ToLowerCase(tmp);
+            }
+
+            std::string key = Toolbox::StripSpaces(tmp);
+
+            if (!key.empty())
+            {
+              std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol));
+
+              that.answer_.AddHeader(key, value);
+            }
+          }
+
+          return length;
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+
+    static size_t BodyCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+
+        size_t length = size * nmemb;
+        if (length == 0)
+        {
+          return 0;
+        }
+        else
+        {
+          CurlAnswer& that = *(static_cast(userdata));
+          that.answer_.AddChunk(buffer, length);
+          return length;
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+  };
+
+
+  class HttpClient::DefaultAnswer : public HttpClient::IAnswer
+  {
+  private:
+    ChunkedBuffer   answer_;
+    HttpHeaders*    headers_;
+
+  public:
+    DefaultAnswer() : headers_(NULL)
+    {
+    }
+
+    void SetHeaders(HttpHeaders& headers)
+    {
+      headers_ = &headers;
+      headers_->clear();
+    }
+
+    void FlattenBody(std::string& target)
+    {
+      answer_.Flatten(target);
+    }
+
+    virtual void AddHeader(const std::string& key,
+                           const std::string& value) ORTHANC_OVERRIDE
+    {
+      if (headers_ != NULL)
+      {
+        (*headers_) [key] = value;
+      }
+    }
+      
+    virtual void AddChunk(const void* data,
+                          size_t size) ORTHANC_OVERRIDE
+    {
+      answer_.AddChunk(data, size);
+    }
+  };
+
+
+  class HttpClient::GlobalParameters
+  {
+  private:
+    boost::mutex    mutex_;
+    bool            httpsVerifyPeers_;
+    std::string     httpsCACertificates_;
+    std::string     proxy_;
+    long            timeout_;
+    bool            verbose_;
+
+    GlobalParameters() : 
+      httpsVerifyPeers_(true),
+      timeout_(0),
+      verbose_(false)
+    {
+    }
+
+  public:
+    // Singleton pattern
+    static GlobalParameters& GetInstance()
+    {
+      static GlobalParameters parameters;
+      return parameters;
+    }
+
+    void ConfigureSsl(bool httpsVerifyPeers,
+                      const std::string& httpsCACertificates)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      httpsVerifyPeers_ = httpsVerifyPeers;
+      httpsCACertificates_ = httpsCACertificates;
+    }
+
+    void GetSslConfiguration(bool& httpsVerifyPeers,
+                             std::string& httpsCACertificates)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      httpsVerifyPeers = httpsVerifyPeers_;
+      httpsCACertificates = httpsCACertificates_;
+    }
+
+    void SetDefaultProxy(const std::string& proxy)
+    {
+      CLOG(INFO, HTTP) << "Setting the default proxy for HTTP client connections: " << proxy;
+
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        proxy_ = proxy;
+      }
+    }
+
+    void GetDefaultProxy(std::string& target)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      target = proxy_;
+    }
+
+    void SetDefaultTimeout(long seconds)
+    {
+      CLOG(INFO, HTTP) << "Setting the default timeout for HTTP client connections: " << seconds << " seconds";
+
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        timeout_ = seconds;
+      }
+    }
+
+    long GetDefaultTimeout()
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      return timeout_;
+    }
+
+#if ORTHANC_ENABLE_PKCS11 == 1
+    bool IsPkcs11Initialized()
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      return Pkcs11::IsInitialized();
+    }
+
+    void InitializePkcs11(const std::string& module,
+                          const std::string& pin,
+                          bool verbose)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      Pkcs11::Initialize(module, pin, verbose);
+    }
+#endif
+
+    bool IsDefaultVerbose() const
+    {
+      return verbose_;
+    }
+
+    void SetDefaultVerbose(bool verbose) 
+    {
+      verbose_ = verbose;
+    }
+  };
+
+
+  struct HttpClient::PImpl
+  {
+    CURL* curl_;
+    CurlHeaders defaultPostHeaders_;
+    CurlHeaders defaultChunkedHeaders_;
+    CurlHeaders userHeaders_;
+    CurlRequestBody requestBody_;
+  };
+
+
+  void HttpClient::ThrowException(HttpStatus status)
+  {
+    switch (status)
+    {
+      case HttpStatus_400_BadRequest:
+        throw OrthancException(ErrorCode_BadRequest);
+
+      case HttpStatus_401_Unauthorized:
+      case HttpStatus_403_Forbidden:
+        throw OrthancException(ErrorCode_Unauthorized);
+
+      case HttpStatus_404_NotFound:
+        throw OrthancException(ErrorCode_UnknownResource);
+
+      default:
+        throw OrthancException(ErrorCode_NetworkProtocol);
+    }
+  }
+
+
+  /*static int CurlDebugCallback(CURL *handle,
+    curl_infotype type,
+    char *data,
+    size_t size,
+    void *userptr)
+    {
+    switch (type)
+    {
+    case CURLINFO_TEXT:
+    case CURLINFO_HEADER_IN:
+    case CURLINFO_HEADER_OUT:
+    case CURLINFO_SSL_DATA_IN:
+    case CURLINFO_SSL_DATA_OUT:
+    case CURLINFO_END:
+    case CURLINFO_DATA_IN:
+    case CURLINFO_DATA_OUT:
+    {
+    std::string s(data, size);
+    CLOG(INFO, INFO) << "libcurl: " << s;
+    break;
+    }
+
+    default:
+    break;
+    }
+
+    return 0;
+    }*/
+
+
+  void HttpClient::Setup()
+  {
+    pimpl_->defaultPostHeaders_.AddHeader("Expect", "");
+    pimpl_->defaultChunkedHeaders_.AddHeader("Expect", "");
+    pimpl_->defaultChunkedHeaders_.AddHeader("Transfer-Encoding", "chunked");
+
+    pimpl_->curl_ = curl_easy_init();
+
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlAnswer::HeaderCallback));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlAnswer::BodyCallback));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1));
+
+    // This fixes the "longjmp causes uninitialized stack frame" crash
+    // that happens on modern Linux versions.
+    // http://stackoverflow.com/questions/9191668/error-longjmp-causes-uninitialized-stack-frame
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOSIGNAL, 1));
+
+    url_ = "";
+    method_ = HttpMethod_Get;
+    lastStatus_ = HttpStatus_None;
+    SetVerbose(GlobalParameters::GetInstance().IsDefaultVerbose());
+    timeout_ = GlobalParameters::GetInstance().GetDefaultTimeout();
+    GlobalParameters::GetInstance().GetDefaultProxy(proxy_);
+    GlobalParameters::GetInstance().GetSslConfiguration(verifyPeers_, caCertificates_);    
+
+    hasExternalBody_ = false;
+    externalBodyData_ = NULL;
+    externalBodySize_ = 0;
+  }
+
+
+  HttpClient::HttpClient() : 
+    pimpl_(new PImpl),
+    verifyPeers_(true),
+    pkcs11Enabled_(false),
+    headersToLowerCase_(true),
+    redirectionFollowed_(true)
+  {
+    Setup();
+  }
+
+
+  HttpClient::HttpClient(const WebServiceParameters& service,
+                         const std::string& uri) : 
+    pimpl_(new PImpl),
+    verifyPeers_(true),
+    headersToLowerCase_(true),
+    redirectionFollowed_(true)
+  {
+    Setup();
+
+    if (service.GetUsername().size() != 0 && 
+        service.GetPassword().size() != 0)
+    {
+      SetCredentials(service.GetUsername().c_str(), 
+                     service.GetPassword().c_str());
+    }
+
+    if (!service.GetCertificateFile().empty())
+    {
+      SetClientCertificate(service.GetCertificateFile(),
+                           service.GetCertificateKeyFile(),
+                           service.GetCertificateKeyPassword());
+    }
+
+    SetPkcs11Enabled(service.IsPkcs11Enabled());
+
+    SetUrl(Toolbox::JoinUri(service.GetUrl(), uri));
+
+    for (WebServiceParameters::Dictionary::const_iterator 
+           it = service.GetHttpHeaders().begin();
+         it != service.GetHttpHeaders().end(); ++it)
+    {
+      AddHeader(it->first, it->second);
+    }
+
+    if (service.HasTimeout())
+    {
+      SetTimeout(service.GetTimeout());
+    }
+  }
+
+
+  HttpClient::~HttpClient()
+  {
+    curl_easy_cleanup(pimpl_->curl_);
+  }
+
+  void HttpClient::SetUrl(const char *url)
+  {
+    url_ = std::string(url);
+  }
+
+  void HttpClient::SetUrl(const std::string &url)
+  {
+    url_ = url;
+  }
+
+  const std::string &HttpClient::GetUrl() const
+  {
+    return url_;
+  }
+
+  void HttpClient::SetMethod(HttpMethod method)
+  {
+    method_ = method;
+  }
+
+  HttpMethod HttpClient::GetMethod() const
+  {
+    return method_;
+  }
+
+  void HttpClient::SetTimeout(long seconds)
+  {
+    timeout_ = seconds;
+  }
+
+  long HttpClient::GetTimeout() const
+  {
+    return timeout_;
+  }
+
+
+  void HttpClient::AssignBody(const std::string& data)
+  {
+    body_ = data;
+    pimpl_->requestBody_.Clear();
+    hasExternalBody_ = false;
+  }
+
+
+  void HttpClient::AssignBody(const void* data,
+                              size_t size)
+  {
+    if (size != 0 &&
+        data == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      body_.assign(reinterpret_cast(data), size);
+      pimpl_->requestBody_.Clear();
+      hasExternalBody_ = false;
+    }
+  }
+
+
+  void HttpClient::SetBody(IRequestBody& body)
+  {
+    body_.clear();
+    pimpl_->requestBody_.SetBody(body);
+    hasExternalBody_ = false;
+  }
+
+  
+  void HttpClient::SetExternalBody(const void* data,
+                                   size_t size)
+  {
+    if (size != 0 &&
+        data == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      body_.clear();
+      pimpl_->requestBody_.Clear();
+      hasExternalBody_ = true;
+      externalBodyData_ = data;
+      externalBodySize_ = size;
+    }
+  }
+  
+
+  void HttpClient::SetExternalBody(const std::string& data)
+  {
+    SetExternalBody(data.empty() ? NULL : data.c_str(), data.size());
+  }
+
+
+  void HttpClient::ClearBody()
+  {
+    body_.clear();
+    pimpl_->requestBody_.Clear();
+    hasExternalBody_ = false;
+  }
+
+
+  void HttpClient::SetVerbose(bool isVerbose)
+  {
+    isVerbose_ = isVerbose;
+
+    if (isVerbose_)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 1));
+      //CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_DEBUGFUNCTION, &CurlDebugCallback));
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 0));
+    }
+  }
+
+  bool HttpClient::IsVerbose() const
+  {
+    return isVerbose_;
+  }
+
+
+  void HttpClient::AddHeader(const std::string& key,
+                             const std::string& value)
+  {
+    if (key.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      pimpl_->userHeaders_.AddHeader(key, value);
+    }
+  }
+
+
+  void HttpClient::ClearHeaders()
+  {
+    pimpl_->userHeaders_.Clear();
+  }
+
+
+  bool HttpClient::ApplyInternal(CurlAnswer& answer)
+  {
+    CLOG(INFO, HTTP) << "New HTTP request to: " << url_ << " (timeout: "
+                     << boost::lexical_cast(timeout_ <= 0 ? DEFAULT_HTTP_TIMEOUT : timeout_) << "s)";
+    
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &answer));
+
+#if ORTHANC_ENABLE_SSL == 1
+    // Setup HTTPS-related options
+
+    if (verifyPeers_)
+    {
+#if defined(CURLSSLOPT_NATIVE_CA)   // from curl v 8.2.0     
+      if (caCertificates_.empty())  // use native CA store (equivalent to --ca-native)
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA));
+      }
+      else 
+#endif
+      {
+        // use provided CA file (equivalent to --cacert)
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, caCertificates_.c_str()));
+      }
+      
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 2));  // libcurl default is strict verifyhost
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1)); 
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 0)); 
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 0)); 
+    }
+#endif
+
+    // Setup the HTTPS client certificate
+    if (!clientCertificateFile_.empty() &&
+        pkcs11Enabled_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Cannot enable both client certificates and PKCS#11 authentication");
+    }
+
+    if (pkcs11Enabled_)
+    {
+#if ORTHANC_ENABLE_PKCS11 == 1
+      if (GlobalParameters::GetInstance().IsPkcs11Initialized())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLENGINE, Pkcs11::GetEngineIdentifier()));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "ENG"));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "ENG"));
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Cannot use PKCS#11 for a HTTPS request, "
+                               "because it has not been initialized");
+      }
+#else
+      throw OrthancException(ErrorCode_InternalError,
+                             "This version of Orthanc is compiled without support for PKCS#11");
+#endif
+    }
+    else if (!clientCertificateFile_.empty())
+    {
+#if ORTHANC_ENABLE_SSL == 1
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "PEM"));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERT, clientCertificateFile_.c_str()));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_KEYPASSWD, clientCertificateKeyPassword_.c_str()));
+
+      // NB: If no "clientKeyFile_" is provided, the key must be
+      // prepended to the certificate file
+      if (!clientCertificateKeyFile_.empty())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "PEM"));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str()));
+      }
+#else
+      throw OrthancException(ErrorCode_InternalError,
+                             "This version of Orthanc is compiled without OpenSSL support, "
+                             "cannot use HTTPS client authentication");
+#endif
+    }
+
+    // Reset the parameters from previous calls to Apply()
+    pimpl_->userHeaders_.Assign(pimpl_->curl_);
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 0L));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 0L));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, NULL));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0L));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, NULL));
+
+    if (redirectionFollowed_)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1L));
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 0L));
+    }
+
+    // Set timeouts
+    if (timeout_ <= 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, DEFAULT_HTTP_TIMEOUT));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, DEFAULT_HTTP_TIMEOUT));
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_));
+    }
+
+    if (credentials_.size() != 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str()));
+    }
+
+    if (proxy_.size() != 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str()));
+    }
+
+    switch (method_)
+    {
+      case HttpMethod_Get:
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 1L));
+        break;
+
+      case HttpMethod_Post:
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
+
+        break;
+
+      case HttpMethod_Delete:
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE"));
+        break;
+
+      case HttpMethod_Put:
+        // http://stackoverflow.com/a/7570281/881731: Don't use
+        // CURLOPT_PUT if there is a body
+
+        // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L));
+
+        curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (method_ == HttpMethod_Post ||
+        method_ == HttpMethod_Put)
+    {
+      if (!pimpl_->userHeaders_.IsEmpty() &&
+          !pimpl_->userHeaders_.HasExpect())
+      {
+        CLOG(INFO, HTTP) << "For performance, the HTTP header \"Expect\" should be set to empty string in POST/PUT requests";
+      }
+
+      if (pimpl_->requestBody_.IsValid())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, CurlRequestBody::Callback));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READDATA, &pimpl_->requestBody_));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, -1L));
+    
+        if (pimpl_->userHeaders_.IsEmpty())
+        {
+          pimpl_->defaultChunkedHeaders_.Assign(pimpl_->curl_);
+        }
+        else if (!pimpl_->userHeaders_.IsChunkedTransfer())
+        {
+          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must be set to \"chunked\" "
+                       << "if streaming a chunked body in POST/PUT requests";
+        }
+      }
+      else
+      {
+        // Disable possible previous stream transfers
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, NULL));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_UPLOAD, 0));
+
+        if (pimpl_->userHeaders_.IsChunkedTransfer())
+        {
+          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must only be set "
+                       << "if streaming a chunked body in POST/PUT requests";
+        }
+
+        if (pimpl_->userHeaders_.IsEmpty())
+        {
+          pimpl_->defaultPostHeaders_.Assign(pimpl_->curl_);
+        }
+
+        if (hasExternalBody_)
+        {
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, externalBodyData_));
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, externalBodySize_));
+        }
+        else if (body_.size() > 0)
+        {
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, body_.c_str()));
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, body_.size()));
+        }
+        else
+        {
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
+        }
+      }
+    }
+
+
+    // Do the actual request
+    CURLcode code;
+    long status = 0;
+
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer));
+
+    const boost::posix_time::ptime start = boost::posix_time::microsec_clock::universal_time();
+    
+    if (boost::starts_with(url_, "https://"))
+    {
+      code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status, url_);
+    }
+    else
+    {
+      code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status, url_);
+    }
+
+    const boost::posix_time::ptime end = boost::posix_time::microsec_clock::universal_time();
+    
+    CLOG(INFO, HTTP) << "HTTP status code " << status << " in "
+                     << ((end - start).total_milliseconds()) << " ms after "
+                     << EnumerationToString(method_) << " request on: " << url_;
+
+    if (isVerbose_)
+    {
+      CLOG(INFO, HTTP) << "cURL status code: " << code;
+    }
+
+    CheckCode(code, url_);  // throws on HTTP error
+
+    if (status == 0)
+    {
+      // This corresponds to a call to an inexistent host
+      lastStatus_ = HttpStatus_500_InternalServerError;
+    }
+    else
+    {
+      lastStatus_ = static_cast(status);
+    }
+
+    if (status >= 200 && status < 300)
+    {
+      return true;   // Success
+    }
+    else
+    {
+      LOG(ERROR) << "Error in HTTP request, received HTTP status " << status 
+                 << " (" << EnumerationToString(lastStatus_) << ") after "
+                 << EnumerationToString(method_) << " request on: " << url_;
+      return false;
+    }
+  }
+
+
+  bool HttpClient::ApplyInternal(std::string& answerBody,
+                                 HttpHeaders* answerHeaders)
+  {
+    answerBody.clear();
+
+    DefaultAnswer answer;
+
+    if (answerHeaders != NULL)
+    {
+      answer.SetHeaders(*answerHeaders);
+    }
+
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+
+    if (ApplyInternal(wrapper))
+    {
+      answer.FlattenBody(answerBody);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool HttpClient::ApplyInternal(Json::Value& answerBody,
+                                 HttpClient::HttpHeaders* answerHeaders)
+  {
+    std::string s;
+    if (ApplyInternal(s, answerHeaders))
+    {
+      return Toolbox::ReadJson(answerBody, s);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void HttpClient::SetCredentials(const char* username,
+                                  const char* password)
+  {
+    credentials_ = std::string(username) + ":" + std::string(password);
+  }
+
+  void HttpClient::SetProxy(const std::string &proxy)
+  {
+    proxy_ = proxy;
+  }
+
+  void HttpClient::SetHttpsVerifyPeers(bool verify)
+  {
+    verifyPeers_ = verify;
+  }
+
+  bool HttpClient::IsHttpsVerifyPeers() const
+  {
+    return verifyPeers_;
+  }
+
+  void HttpClient::SetHttpsCACertificates(const std::string &certificates)
+  {
+    caCertificates_ = certificates;
+  }
+
+  const std::string &HttpClient::GetHttpsCACertificates() const
+  {
+    return caCertificates_;
+  }
+
+
+  void HttpClient::ConfigureSsl(bool httpsVerifyPeers,
+                                const std::string& httpsVerifyCertificates)
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    if (httpsVerifyPeers)
+    {
+      if (httpsVerifyCertificates.empty())
+      {
+        LOG(WARNING) << "No certificates are provided to validate peers.  Orthanc will use the native CA store. "
+                     << "Set \"HttpsCACertificates\" if you need to do HTTPS requests and use custom CAs.";
+      }
+      else
+      {
+        LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << httpsVerifyCertificates;
+      }
+    }
+    else
+    {
+      LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled";
+    }
+#endif
+
+    GlobalParameters::GetInstance().ConfigureSsl(httpsVerifyPeers, httpsVerifyCertificates);
+  }
+
+  
+  void HttpClient::GlobalInitialize()
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    CheckCode(curl_global_init(CURL_GLOBAL_ALL));
+#else
+    CheckCode(curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_SSL));
+#endif
+  }
+
+
+  void HttpClient::GlobalFinalize()
+  {
+    curl_global_cleanup();
+
+#if ORTHANC_ENABLE_PKCS11 == 1
+    Pkcs11::Finalize();
+#endif
+  }
+  
+
+  void HttpClient::SetDefaultVerbose(bool verbose)
+  {
+    GlobalParameters::GetInstance().SetDefaultVerbose(verbose);
+  }
+
+
+  void HttpClient::SetDefaultProxy(const std::string& proxy)
+  {
+    GlobalParameters::GetInstance().SetDefaultProxy(proxy);
+  }
+
+
+  void HttpClient::SetDefaultTimeout(long timeout)
+  {
+    GlobalParameters::GetInstance().SetDefaultTimeout(timeout);
+  }
+
+
+  bool HttpClient::Apply(IAnswer& answer)
+  {
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+    return ApplyInternal(wrapper);
+  }
+
+  bool HttpClient::Apply(std::string &answerBody)
+  {
+    return ApplyInternal(answerBody, NULL);
+  }
+
+  bool HttpClient::Apply(Json::Value &answerBody)
+  {
+    return ApplyInternal(answerBody, NULL);
+  }
+
+  bool HttpClient::Apply(std::string &answerBody,
+                         HttpClient::HttpHeaders &answerHeaders)
+  {
+    return ApplyInternal(answerBody, &answerHeaders);
+  }
+
+  bool HttpClient::Apply(Json::Value &answerBody,
+                         HttpClient::HttpHeaders &answerHeaders)
+  {
+    return ApplyInternal(answerBody, &answerHeaders);
+  }
+
+  HttpStatus HttpClient::GetLastStatus() const
+  {
+    return lastStatus_;
+  }
+
+
+  void HttpClient::ApplyAndThrowException(IAnswer& answer)
+  {
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+
+    if (!ApplyInternal(wrapper))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+
+  void HttpClient::ApplyAndThrowException(std::string& answerBody)
+  {
+    if (!Apply(answerBody))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+  
+  void HttpClient::ApplyAndThrowException(Json::Value& answerBody)
+  {
+    if (!Apply(answerBody))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+
+  void HttpClient::ApplyAndThrowException(std::string& answerBody,
+                                          HttpHeaders& answerHeaders)
+  {
+    if (!Apply(answerBody, answerHeaders))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+  
+
+  void HttpClient::ApplyAndThrowException(Json::Value& answerBody,
+                                          HttpHeaders& answerHeaders)
+  {
+    if (!Apply(answerBody, answerHeaders))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+
+  void HttpClient::SetClientCertificate(const std::string& certificateFile,
+                                        const std::string& certificateKeyFile,
+                                        const std::string& certificateKeyPassword)
+  {
+    if (certificateFile.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!SystemToolbox::IsRegularFile(certificateFile))
+    {
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "Cannot open certificate file: " + certificateFile);
+    }
+
+    if (!certificateKeyFile.empty() && 
+        !SystemToolbox::IsRegularFile(certificateKeyFile))
+    {
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "Cannot open key file: " + certificateKeyFile);
+    }
+
+    clientCertificateFile_ = certificateFile;
+    clientCertificateKeyFile_ = certificateKeyFile;
+    clientCertificateKeyPassword_ = certificateKeyPassword;
+  }
+
+  void HttpClient::SetPkcs11Enabled(bool enabled)
+  {
+    pkcs11Enabled_ = enabled;
+  }
+
+  bool HttpClient::IsPkcs11Enabled() const
+  {
+    return pkcs11Enabled_;
+  }
+
+  const std::string &HttpClient::GetClientCertificateFile() const
+  {
+    return clientCertificateFile_;
+  }
+
+  const std::string &HttpClient::GetClientCertificateKeyFile() const
+  {
+    return clientCertificateKeyFile_;
+  }
+
+  const std::string &HttpClient::GetClientCertificateKeyPassword() const
+  {
+    return clientCertificateKeyPassword_;
+  }
+
+  void HttpClient::SetConvertHeadersToLowerCase(bool lowerCase)
+  {
+    headersToLowerCase_ = lowerCase;
+  }
+
+  bool HttpClient::IsConvertHeadersToLowerCase() const
+  {
+    return headersToLowerCase_;
+  }
+
+  void HttpClient::SetRedirectionFollowed(bool follow)
+  {
+    redirectionFollowed_ = follow;
+  }
+
+  bool HttpClient::IsRedirectionFollowed() const
+  {
+    return redirectionFollowed_;
+  }
+
+
+  void HttpClient::InitializePkcs11(const std::string& module,
+                                    const std::string& pin,
+                                    bool verbose)
+  {
+#if ORTHANC_ENABLE_PKCS11 == 1
+    CLOG(INFO, HTTP) << "Initializing PKCS#11 using " << module 
+                     << (pin.empty() ? " (no PIN provided)" : " (PIN is provided)");
+    GlobalParameters::GetInstance().InitializePkcs11(module, pin, verbose);    
+#else
+    throw OrthancException(ErrorCode_InternalError,
+                           "This version of Orthanc is compiled without support for PKCS#11");
+#endif
+  }
+}
diff --git a/OrthancFramework/Sources/HttpClient.h b/OrthancFramework/Sources/HttpClient.h
new file mode 100644
index 0000000..ff3b9b6
--- /dev/null
+++ b/OrthancFramework/Sources/HttpClient.h
@@ -0,0 +1,256 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "Enumerations.h"
+#include "OrthancFramework.h"
+#include "WebServiceParameters.h"
+
+#include 
+#include 
+#include 
+#include 
+
+#if !defined(ORTHANC_ENABLE_CURL)
+#  error The macro ORTHANC_ENABLE_CURL must be defined
+#endif
+
+#if ORTHANC_ENABLE_CURL != 1
+#  error Support for curl is disabled, cannot use this file
+#endif
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_PKCS11)
+#  error The macro ORTHANC_ENABLE_PKCS11 must be defined
+#endif
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC HttpClient : public boost::noncopyable
+  {
+  public:
+    typedef std::map  HttpHeaders;
+
+    class IRequestBody : public boost::noncopyable
+    {
+    public:
+      virtual ~IRequestBody()
+      {
+      }
+      
+      virtual bool ReadNextChunk(std::string& chunk) = 0;
+    };
+
+    class IAnswer : public boost::noncopyable
+    {
+    public:
+      virtual ~IAnswer()
+      {
+      }
+
+      virtual void AddHeader(const std::string& key,
+                             const std::string& value) = 0;
+      
+      virtual void AddChunk(const void* data,
+                            size_t size) = 0;
+    };
+
+  private:
+    class CurlHeaders;
+    class CurlRequestBody;
+    class CurlAnswer;
+    class DefaultAnswer;
+    class GlobalParameters;
+
+    struct PImpl;
+    boost::shared_ptr pimpl_;
+
+    std::string url_;
+    std::string credentials_;
+    HttpMethod method_;
+    HttpStatus lastStatus_;
+    std::string body_;  // This only makes sense for POST and PUT requests
+    bool isVerbose_;
+    long timeout_;
+    std::string proxy_;
+    bool verifyPeers_;
+    std::string caCertificates_;
+    std::string clientCertificateFile_;
+    std::string clientCertificateKeyFile_;
+    std::string clientCertificateKeyPassword_;
+    bool pkcs11Enabled_;
+    bool headersToLowerCase_;
+    bool redirectionFollowed_;
+
+    // New in Orthanc 1.9.3 to avoid memcpy()
+    bool        hasExternalBody_;
+    const void* externalBodyData_;
+    size_t      externalBodySize_;
+
+    void Setup();
+
+    void operator= (const HttpClient&);  // Assignment forbidden
+    HttpClient(const HttpClient& base);  // Copy forbidden
+
+    bool ApplyInternal(CurlAnswer& answer);
+
+    bool ApplyInternal(std::string& answerBody,
+                       HttpHeaders* answerHeaders);
+
+    bool ApplyInternal(Json::Value& answerBody,
+                       HttpHeaders* answerHeaders);
+
+  public:
+    HttpClient();
+
+    HttpClient(const WebServiceParameters& service,
+               const std::string& uri);
+
+    ~HttpClient();
+
+    void SetUrl(const char* url);
+
+    void SetUrl(const std::string& url);
+
+    const std::string& GetUrl() const;
+
+    void SetMethod(HttpMethod method);
+
+    HttpMethod GetMethod() const;
+
+    void SetTimeout(long seconds);
+
+    long GetTimeout() const;
+
+    void AssignBody(const std::string& data);
+
+    void AssignBody(const void* data,
+                    size_t size);
+
+    void SetBody(IRequestBody& body);
+
+    // New in Orthanc 1.9.3: The "data" buffer must have a lifetime
+    // that is longer than the HttpClient object
+    void SetExternalBody(const void* data,
+                         size_t size);
+
+    void SetExternalBody(const std::string& data);
+
+    void ClearBody();
+
+    void SetVerbose(bool isVerbose);
+
+    bool IsVerbose() const;
+
+    void AddHeader(const std::string& key,
+                   const std::string& value);
+
+    void ClearHeaders();
+
+    bool Apply(IAnswer& answer);
+
+    bool Apply(std::string& answerBody);
+
+    bool Apply(Json::Value& answerBody);
+
+    bool Apply(std::string& answerBody,
+               HttpHeaders& answerHeaders);
+
+    bool Apply(Json::Value& answerBody,
+               HttpHeaders& answerHeaders);
+
+    HttpStatus GetLastStatus() const;
+
+    void SetCredentials(const char* username,
+                        const char* password);
+
+    void SetProxy(const std::string& proxy);
+
+    void SetHttpsVerifyPeers(bool verify);
+
+    bool IsHttpsVerifyPeers() const;
+
+    void SetHttpsCACertificates(const std::string& certificates);
+
+    const std::string& GetHttpsCACertificates() const;
+
+    void SetClientCertificate(const std::string& certificateFile,
+                              const std::string& certificateKeyFile,
+                              const std::string& certificateKeyPassword);
+
+    void SetPkcs11Enabled(bool enabled);
+
+    bool IsPkcs11Enabled() const;
+
+    const std::string& GetClientCertificateFile() const;
+
+    const std::string& GetClientCertificateKeyFile() const;
+
+    const std::string& GetClientCertificateKeyPassword() const;
+
+    void SetConvertHeadersToLowerCase(bool lowerCase);
+
+    bool IsConvertHeadersToLowerCase() const;
+
+    void SetRedirectionFollowed(bool follow);
+
+    bool IsRedirectionFollowed() const;
+
+    static void GlobalInitialize();
+  
+    static void GlobalFinalize();
+
+    static void InitializePkcs11(const std::string& module,
+                                 const std::string& pin,
+                                 bool verbose);
+
+    static void ConfigureSsl(bool httpsVerifyPeers,
+                             const std::string& httpsCACertificates);
+
+    static void SetDefaultVerbose(bool verbose);
+
+    static void SetDefaultProxy(const std::string& proxy);
+
+    static void SetDefaultTimeout(long timeout);
+
+    void ApplyAndThrowException(IAnswer& answer);
+
+    void ApplyAndThrowException(std::string& answerBody);
+
+    void ApplyAndThrowException(Json::Value& answerBody);
+
+    void ApplyAndThrowException(std::string& answerBody,
+                                HttpHeaders& answerHeaders);
+
+    void ApplyAndThrowException(Json::Value& answerBody,
+                                HttpHeaders& answerHeaders);
+
+    static void ThrowException(HttpStatus status);
+  };
+}
diff --git a/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp b/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp
new file mode 100644
index 0000000..493acb2
--- /dev/null
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp
@@ -0,0 +1,96 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+#include "../PrecompiledHeaders.h"
+#include "BufferHttpSender.h"
+
+#include "../OrthancException.h"
+
+#include 
+
+namespace Orthanc
+{
+  BufferHttpSender::BufferHttpSender() :
+    position_(0), 
+    chunkSize_(0),
+    currentChunkSize_(0)
+  {
+  }
+
+  std::string &BufferHttpSender::GetBuffer()
+  {
+    return buffer_;
+  }
+
+  const std::string &BufferHttpSender::GetBuffer() const
+  {
+    return buffer_;
+  }
+
+  void BufferHttpSender::SetChunkSize(size_t chunkSize)
+  {
+    chunkSize_ = chunkSize;
+  }
+
+  uint64_t BufferHttpSender::GetContentLength()
+  {
+    return buffer_.size();
+  }
+
+
+  bool BufferHttpSender::ReadNextChunk()
+  {
+    assert(position_ + currentChunkSize_ <= buffer_.size());
+
+    position_ += currentChunkSize_;
+
+    if (position_ == buffer_.size())
+    {
+      return false;
+    }
+    else
+    {
+      currentChunkSize_ = buffer_.size() - position_;
+
+      if (chunkSize_ != 0 &&
+          currentChunkSize_ > chunkSize_)
+      {
+        currentChunkSize_ = chunkSize_;
+      }
+
+      return true;
+    }
+  }
+
+
+  const char* BufferHttpSender::GetChunkContent()
+  {
+    return buffer_.c_str() + position_;
+  }
+
+
+  size_t BufferHttpSender::GetChunkSize()
+  {
+    return currentChunkSize_;
+  }
+}
diff --git a/OrthancFramework/Sources/HttpServer/BufferHttpSender.h b/OrthancFramework/Sources/HttpServer/BufferHttpSender.h
new file mode 100644
index 0000000..6f9858a
--- /dev/null
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.h
@@ -0,0 +1,62 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+#pragma once
+
+#include "HttpFileSender.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC BufferHttpSender : public HttpFileSender
+  {
+  private:
+    std::string  buffer_;
+    size_t       position_;
+    size_t       chunkSize_;
+    size_t       currentChunkSize_;
+
+  public:
+    BufferHttpSender();
+
+    std::string& GetBuffer();
+
+    const std::string& GetBuffer() const;
+
+    // This is for test purpose. If "chunkSize" is set to "0" (the
+    // default), the entire buffer is consumed at once.
+    void SetChunkSize(size_t chunkSize);
+
+
+    /**
+     * Implementation of the IHttpStreamAnswer interface.
+     **/
+
+    virtual uint64_t GetContentLength() ORTHANC_OVERRIDE;
+
+    virtual bool ReadNextChunk() ORTHANC_OVERRIDE;
+
+    virtual const char* GetChunkContent() ORTHANC_OVERRIDE;
+
+    virtual size_t GetChunkSize() ORTHANC_OVERRIDE;
+  };
+}
diff --git a/OrthancFramework/Sources/HttpServer/CStringMatcher.cpp b/OrthancFramework/Sources/HttpServer/CStringMatcher.cpp
new file mode 100644
index 0000000..d65e76b
--- /dev/null
+++ b/OrthancFramework/Sources/HttpServer/CStringMatcher.cpp
@@ -0,0 +1,150 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "CStringMatcher.h"
+
+#include "../OrthancException.h"
+
+#include 
+
+namespace Orthanc
+{
+  class CStringMatcher::Search : public boost::noncopyable
+  {
+  private:
+    typedef boost::algorithm::boyer_moore  Algorithm;
+
+    Algorithm algorithm_;
+
+  public:
+    // WARNING - The lifetime of "pattern_" must be larger than
+    // "search_", as the latter internally keeps a pointer to "pattern" (*)
+    explicit Search(const std::string& pattern) :
+      algorithm_(pattern.c_str(), pattern.c_str() + pattern.size())
+    {
+    }
+
+    const char* Apply(const char* start,
+                      const char* end) const
+    {
+#if BOOST_VERSION >= 106200
+      return algorithm_(start, end).first;
+#else
+      return algorithm_(start, end);
+#endif
+    }
+  };
+
+
+
+  CStringMatcher::CStringMatcher(const std::string& pattern) :
+    pattern_(pattern),
+    valid_(false),
+    matchBegin_(NULL),
+    matchEnd_(NULL)
+  {
+    // WARNING - Don't use "pattern" (local variable, will be
+    // destroyed once exiting the constructor) but "pattern_"
+    // (variable member, will last as long as the algorithm),
+    // otherwise lifetime is bad! (*)
+    search_.reset(new Search(pattern_));
+  }
+
+  const std::string& CStringMatcher::GetPattern() const
+  {
+    return pattern_;
+  }
+
+  bool CStringMatcher::IsValid() const
+  {
+    return valid_;
+  }
+  
+
+  bool CStringMatcher::Apply(const char* start,
+                             const char* end)
+  {
+    assert(search_.get() != NULL);
+
+    if (start > end)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    matchBegin_ = search_->Apply(start, end);
+    
+    if (matchBegin_ == end)
+    {
+      valid_ = false;
+    }
+    else
+    {
+      matchEnd_ = matchBegin_ + pattern_.size();
+      assert(matchEnd_ <= end);
+      valid_ = true;
+    }
+
+    return valid_;
+  }
+
+  
+  bool CStringMatcher::Apply(const std::string& corpus)
+  {
+    if (corpus.empty())
+    {
+      return false;
+    }
+    else
+    {
+      return Apply(corpus.c_str(), corpus.c_str() + corpus.size());
+    }
+  }
+
+
+  const char* CStringMatcher::GetMatchBegin() const
+  {
+    if (valid_)
+    {
+      return matchBegin_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const char* CStringMatcher::GetMatchEnd() const
+  {
+    if (valid_)
+    {
+      return matchEnd_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
diff --git a/OrthancFramework/Sources/HttpServer/CStringMatcher.h b/OrthancFramework/Sources/HttpServer/CStringMatcher.h
new file mode 100644
index 0000000..39229e5
--- /dev/null
+++ b/OrthancFramework/Sources/HttpServer/CStringMatcher.h
@@ -0,0 +1,64 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#pragma once
+
+#include "../OrthancFramework.h"
+
+#include 
+#include 
+#include 
+
+namespace Orthanc
+{
+  // Convenience class that wraps a Boost algorithm for string matching
+  class ORTHANC_PUBLIC CStringMatcher : public boost::noncopyable
+  {
+  private:
+    class Search;
+      
+    boost::shared_ptr  search_;  // PImpl pattern
+    std::string                pattern_;
+    bool                       valid_;
+    const char*                matchBegin_;
+    const char*                matchEnd_;
+    
+  public:
+    explicit CStringMatcher(const std::string& pattern);
+
+    const std::string& GetPattern() const;
+
+    bool IsValid() const;
+
+    // "end" is non-inclusive
+    bool Apply(const char* start,
+               const char* end);
+
+    bool Apply(const std::string& corpus);
+
+    const char* GetMatchBegin() const;
+
+    const char* GetMatchEnd() const;
+  };
+}
diff --git a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp
new file mode 100644
index 0000000..bfdea76
--- /dev/null
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp
@@ -0,0 +1,172 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * .
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "FilesystemHttpHandler.h"
+
+#include "../OrthancException.h"
+#include "../SystemToolbox.h"
+#include "FilesystemHttpSender.h"
+
+#include 
+
+
+namespace Orthanc
+{
+  struct FilesystemHttpHandler::PImpl
+  {
+    UriComponents baseUri_;
+    boost::filesystem::path root_;
+  };
+
+
+
+  static void OutputDirectoryContent(HttpOutput& output,
+                                     const HttpToolbox::Arguments& headers,
+                                     const UriComponents& uri,
+                                     const boost::filesystem::path& p)
+  {
+    namespace fs = boost::filesystem;
+
+    std::string s;
+    s += "";
+    s += "  ";
+    s += "    

Subdirectories

"; + s += "
    "; + + if (uri.size() > 0) + { + std::string h = Toolbox::FlattenUri(uri) + "/.."; + s += "
  • ..
  • "; + } + + fs::directory_iterator end; + for (fs::directory_iterator it(p) ; it != end; ++it) + { +#if BOOST_HAS_FILESYSTEM_V3 == 1 + std::string f = it->path().filename().string(); +#else + std::string f = it->path().filename(); +#endif + + std::string h = Toolbox::FlattenUri(uri) + "/" + f; + if (fs::is_directory(it->status())) + s += "
  • " + f + "
  • "; + } + + s += "
"; + s += "

Files

"; + s += "
    "; + + for (fs::directory_iterator it(p) ; it != end; ++it) + { +#if BOOST_HAS_FILESYSTEM_V3 == 1 + std::string f = it->path().filename().string(); +#else + std::string f = it->path().filename(); +#endif + + std::string h = Toolbox::FlattenUri(uri) + "/" + f; + if (SystemToolbox::IsRegularFile(it->path().string())) + { + s += "
  • " + f + "
  • "; + } + } + + s += "
"; + s += " "; + s += ""; + + output.SetContentType(MimeType_Html); + output.Answer(s); + } + + + FilesystemHttpHandler::FilesystemHttpHandler(const std::string& baseUri, + const std::string& root) : pimpl_(new PImpl) + { + Toolbox::SplitUriComponents(pimpl_->baseUri_, baseUri); + pimpl_->root_ = root; + listDirectoryContent_ = false; + + namespace fs = boost::filesystem; + if (!fs::exists(pimpl_->root_) || + !fs::is_directory(pimpl_->root_)) + { + throw OrthancException(ErrorCode_DirectoryExpected); + } + } + + + bool FilesystemHttpHandler::Handle(HttpOutput& output, + RequestOrigin /*origin*/, + const char* /*remoteIp*/, + const char* /*username*/, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers, + const HttpToolbox::GetArguments& arguments, + const void* /*bodyData*/, + size_t /*bodySize*/) + { + if (!Toolbox::IsChildUri(pimpl_->baseUri_, uri)) + { + // This URI is not served by this handler + return false; + } + + if (method != HttpMethod_Get) + { + output.SendMethodNotAllowed("GET"); + return true; + } + + namespace fs = boost::filesystem; + + fs::path p = pimpl_->root_; + for (size_t i = pimpl_->baseUri_.size(); i < uri.size(); i++) + { + p /= uri[i]; + } + + if (SystemToolbox::IsRegularFile(p.string())) + { + FilesystemHttpSender sender(p); + sender.SetContentType(SystemToolbox::AutodetectMimeType(p.string())); + output.Answer(sender); + } + else if (listDirectoryContent_ && + fs::exists(p) && + fs::is_directory(p)) + { + OutputDirectoryContent(output, headers, uri, p); + } + else + { + output.SendStatus(HttpStatus_404_NotFound); + } + + return true; + } +} diff --git a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h new file mode 100644 index 0000000..eef8584 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h @@ -0,0 +1,79 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IHttpHandler.h" + +#include + +namespace Orthanc +{ + class FilesystemHttpHandler : public IHttpHandler + { + private: + // PImpl idiom to avoid the inclusion of boost::filesystem + // throughout the software + struct PImpl; + boost::shared_ptr pimpl_; + + bool listDirectoryContent_; + + public: + FilesystemHttpHandler(const std::string& baseUri, + const std::string& root); + + virtual bool CreateChunkedRequestReader(std::unique_ptr& target, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool Handle(HttpOutput& output, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers, + const HttpToolbox::GetArguments& arguments, + const void* /*bodyData*/, + size_t /*bodySize*/) ORTHANC_OVERRIDE; + + bool IsListDirectoryContent() const + { + return listDirectoryContent_; + } + + void SetListDirectoryContent(bool enabled) + { + listDirectoryContent_ = enabled; + } + }; +} diff --git a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp new file mode 100644 index 0000000..40aebe3 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp @@ -0,0 +1,106 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + +#include "../PrecompiledHeaders.h" +#include "FilesystemHttpSender.h" + +#include "../OrthancException.h" + +static const size_t CHUNK_SIZE = 64 * 1024; // Use 64KB chunks + +namespace Orthanc +{ + void FilesystemHttpSender::Initialize(const boost::filesystem::path& path) + { + SetContentFilename(path.filename().string()); + file_.open(path.string().c_str(), std::ifstream::binary); + + if (!file_.is_open()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + + file_.seekg(0, file_.end); + size_ = file_.tellg(); + file_.seekg(0, file_.beg); + } + + FilesystemHttpSender::FilesystemHttpSender(const std::string& path) + { + Initialize(path); + } + + FilesystemHttpSender::FilesystemHttpSender(const boost::filesystem::path& path) + { + Initialize(path); + } + + FilesystemHttpSender::FilesystemHttpSender(const std::string& path, + MimeType contentType) + { + SetContentType(contentType); + Initialize(path); + } + + FilesystemHttpSender::FilesystemHttpSender(const FilesystemStorage& storage, + const std::string& uuid) + { + Initialize(storage.GetPath(uuid)); + } + + uint64_t FilesystemHttpSender::GetContentLength() + { + return size_; + } + + + bool FilesystemHttpSender::ReadNextChunk() + { + if (chunk_.size() == 0) + { + chunk_.resize(CHUNK_SIZE); + } + + file_.read(&chunk_[0], chunk_.size()); + + if ((file_.flags() & std::istream::failbit) || + file_.gcount() < 0) + { + throw OrthancException(ErrorCode_CorruptedFile); + } + + chunkSize_ = static_cast(file_.gcount()); + + return chunkSize_ > 0; + } + + const char *FilesystemHttpSender::GetChunkContent() + { + return chunk_.c_str(); + } + + size_t FilesystemHttpSender::GetChunkSize() + { + return chunkSize_; + } +} diff --git a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h new file mode 100644 index 0000000..efe2587 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h @@ -0,0 +1,67 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + +#pragma once + +#include "HttpFileSender.h" +#include "BufferHttpSender.h" +#include "../FileStorage/FilesystemStorage.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC FilesystemHttpSender : public HttpFileSender + { + private: + std::ifstream file_; + uint64_t size_; + std::string chunk_; + size_t chunkSize_; + + void Initialize(const boost::filesystem::path& path); + + public: + explicit FilesystemHttpSender(const std::string& path); + + explicit FilesystemHttpSender(const boost::filesystem::path& path) ORTHANC_LOCAL; + + FilesystemHttpSender(const std::string& path, + MimeType contentType); + + FilesystemHttpSender(const FilesystemStorage& storage, + const std::string& uuid); + + /** + * Implementation of the IHttpStreamAnswer interface. + **/ + + virtual uint64_t GetContentLength() ORTHANC_OVERRIDE; + + virtual bool ReadNextChunk() ORTHANC_OVERRIDE; + + virtual const char* GetChunkContent() ORTHANC_OVERRIDE; + + virtual size_t GetChunkSize() ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp new file mode 100644 index 0000000..9b1a3d6 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp @@ -0,0 +1,293 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "HttpContentNegociation.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + +namespace Orthanc +{ + HttpContentNegociation::Handler::Handler(const std::string& type, + const std::string& subtype, + IHandler& handler) : + type_(type), + subtype_(subtype), + handler_(handler) + { + } + + + bool HttpContentNegociation::Handler::IsMatch(const std::string& type, + const std::string& subtype) const + { + if (type == "*" && subtype == "*") + { + return true; + } + else if (subtype == "*" && type == type_) + { + return true; + } + else + { + return type == type_ && subtype == subtype_; + } + } + + + class HttpContentNegociation::Reference : public boost::noncopyable + { + private: + const Handler& handler_; + uint8_t level_; + float quality_; + Dictionary parameters_; + + static float GetQuality(const Dictionary& parameters) + { + Dictionary::const_iterator found = parameters.find("q"); + + if (found != parameters.end()) + { + float quality; + bool ok = false; + + try + { + quality = boost::lexical_cast(found->second); + ok = (quality >= 0.0f && quality <= 1.0f); + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + return quality; + } + else + { + throw OrthancException( + ErrorCode_BadRequest, + "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + found->second); + } + } + else + { + return 1.0f; // Default quality + } + } + + public: + Reference(const Handler& handler, + const std::string& type, + const std::string& subtype, + const Dictionary& parameters) : + handler_(handler), + quality_(GetQuality(parameters)), + parameters_(parameters) + { + if (type == "*" && subtype == "*") + { + level_ = 0; + } + else if (subtype == "*") + { + level_ = 1; + } + else + { + level_ = 2; + } + } + + void Call() const + { + handler_.Call(parameters_); + } + + bool operator< (const Reference& other) const + { + if (level_ < other.level_) + { + return true; + } + else if (level_ > other.level_) + { + return false; + } + else + { + return quality_ < other.quality_; + } + } + }; + + + bool HttpContentNegociation::SplitPair(std::string& first /* out */, + std::string& second /* out */, + const std::string& source, + char separator) + { + size_t pos = source.find(separator); + + if (pos == std::string::npos) + { + return false; + } + else + { + first = Toolbox::StripSpaces(source.substr(0, pos)); + second = Toolbox::StripSpaces(source.substr(pos + 1)); + return true; + } + } + + + void HttpContentNegociation::SelectBestMatch(std::unique_ptr& target, + const Handler& handler, + const std::string& type, + const std::string& subtype, + const Dictionary& parameters) + { + std::unique_ptr match(new Reference(handler, type, subtype, parameters)); + + if (target.get() == NULL || + *target < *match) + { +#if __cplusplus < 201103L + target.reset(match.release()); +#else + target = std::move(match); +#endif + } + } + + + void HttpContentNegociation::Register(const std::string& mime, + IHandler& handler) + { + std::string type, subtype; + + if (SplitPair(type, subtype, mime, '/') && + type != "*" && + subtype != "*") + { + handlers_.push_back(Handler(type, subtype, handler)); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool HttpContentNegociation::Apply(const Dictionary& headers) + { + Dictionary::const_iterator accept = headers.find("accept"); + if (accept != headers.end()) + { + return Apply(accept->second); + } + else + { + return Apply("*/*"); + } + } + + + bool HttpContentNegociation::Apply(const std::string& accept) + { + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + // https://en.wikipedia.org/wiki/Content_negotiation + // http://www.newmediacampaigns.com/blog/browser-rest-http-accept-headers + + Tokens mediaRanges; + Toolbox::TokenizeString(mediaRanges, accept, ','); + + std::unique_ptr bestMatch; + + for (Tokens::const_iterator it = mediaRanges.begin(); + it != mediaRanges.end(); ++it) + { + Tokens tokens; + Toolbox::TokenizeString(tokens, *it, ';'); + + if (tokens.size() > 0) + { + Dictionary parameters; + for (size_t i = 1; i < tokens.size(); i++) + { + std::string key, value; + + if (SplitPair(key, value, tokens[i], '=')) + { + // Remove the enclosing quotes, if present + if (!value.empty() && + value[0] == '"' && + value[value.size() - 1] == '"') + { + value = value.substr(1, value.size() - 2); + } + } + else + { + key = Toolbox::StripSpaces(tokens[i]); + value = ""; + } + + parameters[key] = value; + } + + std::string type, subtype; + if (SplitPair(type, subtype, tokens[0], '/')) + { + for (Handlers::const_iterator it2 = handlers_.begin(); + it2 != handlers_.end(); ++it2) + { + if (it2->IsMatch(type, subtype)) + { + SelectBestMatch(bestMatch, *it2, type, subtype, parameters); + } + } + } + } + } + + if (bestMatch.get() == NULL) // No match was found + { + return false; + } + else + { + bestMatch->Call(); + return true; + } + } +} diff --git a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h new file mode 100644 index 0000000..d04c871 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h @@ -0,0 +1,105 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + +#pragma once + +#include "../OrthancFramework.h" +#include "../Compatibility.h" + +#include +#include +#include +#include +#include +#include +#include + + +namespace Orthanc +{ + class ORTHANC_PUBLIC HttpContentNegociation : public boost::noncopyable + { + public: + typedef std::map Dictionary; + + class IHandler : public boost::noncopyable + { + public: + virtual ~IHandler() + { + } + + virtual void Handle(const std::string& type, + const std::string& subtype, + const Dictionary& parameters) = 0; + }; + + private: + struct Handler + { + std::string type_; + std::string subtype_; + IHandler& handler_; + + Handler(const std::string& type, + const std::string& subtype, + IHandler& handler); + + bool IsMatch(const std::string& type, + const std::string& subtype) const; + + void Call(const Dictionary& parameters) const + { + handler_.Handle(type_, subtype_, parameters); + } + }; + + + class Reference; + + typedef std::vector Tokens; + typedef std::list Handlers; + + Handlers handlers_; + + + static bool SplitPair(std::string& first /* out */, + std::string& second /* out */, + const std::string& source, + char separator); + + static void SelectBestMatch(std::unique_ptr& target, + const Handler& handler, + const std::string& type, + const std::string& subtype, + const Dictionary& parameters); + + public: + void Register(const std::string& mime, + IHandler& handler); + + bool Apply(const Dictionary& headers); + + bool Apply(const std::string& accept); + }; +} diff --git a/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp b/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp new file mode 100644 index 0000000..b5bb203 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp @@ -0,0 +1,97 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "HttpFileSender.h" + +#include "../OrthancException.h" +#include "../Toolbox.h" +#include "../SystemToolbox.h" + +#include + +namespace Orthanc +{ + void HttpFileSender::SetContentType(MimeType contentType) + { + contentType_ = EnumerationToString(contentType); + } + + void HttpFileSender::SetContentType(const std::string &contentType) + { + contentType_ = contentType; + } + + const std::string &HttpFileSender::GetContentType() const + { + return contentType_; + } + + void HttpFileSender::SetContentFilename(const std::string& filename) + { + filename_ = filename; + + if (contentType_.empty()) + { + MimeType mimeType = SystemToolbox::AutodetectMimeType(filename); + contentType_ = EnumerationToString(mimeType); + } + } + + const std::string &HttpFileSender::GetContentFilename() const + { + return filename_; + } + + HttpCompression HttpFileSender::SetupHttpCompression(bool, bool) + { + return HttpCompression_None; + } + + + bool HttpFileSender::HasContentFilename(std::string& filename) + { + if (filename_.empty()) + { + return false; + } + else + { + filename = filename_; + return true; + } + } + + std::string HttpFileSender::GetContentType() + { + if (contentType_.empty()) + { + return MIME_BINARY; + } + else + { + return contentType_; + } + } +} diff --git a/OrthancFramework/Sources/HttpServer/HttpFileSender.h b/OrthancFramework/Sources/HttpServer/HttpFileSender.h new file mode 100644 index 0000000..74117e2 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.h @@ -0,0 +1,61 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "HttpOutput.h" +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + class ORTHANC_PUBLIC HttpFileSender : public IHttpStreamAnswer + { + private: + std::string contentType_; + std::string filename_; + + public: + void SetContentType(MimeType contentType); + + void SetContentType(const std::string& contentType); + + const std::string& GetContentType() const; + + void SetContentFilename(const std::string& filename); + + const std::string& GetContentFilename() const; + + + /** + * Implementation of the IHttpStreamAnswer interface. + **/ + + virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/, + bool /*deflateAllowed*/) ORTHANC_OVERRIDE; + + virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE; + + virtual std::string GetContentType() ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp new file mode 100644 index 0000000..86f7ba0 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp @@ -0,0 +1,1001 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "HttpOutput.h" + +#include "../ChunkedBuffer.h" +#include "../Compression/GzipCompressor.h" +#include "../Compression/ZlibCompressor.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" +#include "../SystemToolbox.h" + +#include +#include +#include +#include + + +#if ORTHANC_ENABLE_CIVETWEB == 1 +# if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE) +# error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined +# endif +#endif + +static const std::string X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options"; + + +namespace Orthanc +{ + HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream, + bool isKeepAlive, + unsigned int keepAliveTimeout) : + stream_(stream), + state_(State_WritingHeader), + isContentCompressible_(false), + status_(HttpStatus_200_Ok), + hasContentLength_(false), + contentLength_(0), + contentPosition_(0), + keepAlive_(isKeepAlive), + keepAliveTimeout_(keepAliveTimeout), + hasXContentTypeOptions_(false), + hasContentType_(false) + { + } + + HttpOutput::StateMachine::~StateMachine() + { + if (state_ != State_Done) + { + //asm volatile ("int3;"); + //LOG(ERROR) << "This HTTP answer does not contain any body"; + } + + if (hasContentLength_ && contentPosition_ != contentLength_) + { + LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body. The remote client has likely closed the connection."; + } + } + + + void HttpOutput::StateMachine::SetHttpStatus(HttpStatus status) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + status_ = status; + } + + + void HttpOutput::StateMachine::SetContentLength(uint64_t length) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + hasContentLength_ = true; + contentLength_ = length; + } + + void HttpOutput::StateMachine::SetContentType(const char* contentType) + { + hasContentType_ = true; + AddHeader("Content-Type", contentType); + } + + void HttpOutput::StateMachine::SetContentCompressible(bool isContentCompressible) + { + isContentCompressible_ = isContentCompressible; + } + + bool HttpOutput::StateMachine::IsContentCompressible() const + { + // We assume that all files that compress correctly (mainly JSON, XML) are clearly identified. + return isContentCompressible_; + } + + void HttpOutput::StateMachine::SetContentFilename(const char* filename) + { + // TODO Escape double quotes + AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\""); + } + + void HttpOutput::StateMachine::SetCookie(const std::string& cookie, + const std::string& value) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + // TODO Escape "=" characters + AddHeader("Set-Cookie", cookie + "=" + value); + } + + + void HttpOutput::StateMachine::AddHeader(const std::string& header, + const std::string& value) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (header == X_CONTENT_TYPE_OPTIONS) + { + hasXContentTypeOptions_ = true; + } + + headers_.push_back(header + ": " + value + "\r\n"); + } + + void HttpOutput::StateMachine::ClearHeaders() + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + headers_.clear(); + } + + void HttpOutput::StateMachine::SendBody(const void* buffer, size_t length) + { + if (state_ == State_Done) + { + if (length == 0) + { + return; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Because of keep-alive connections, the entire body must " + "be sent at once or Content-Length must be given"); + } + } + + if (state_ == State_WritingMultipart) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (state_ == State_WritingHeader) + { + // Send the HTTP header before writing the body + + stream_.OnHttpStatusReceived(status_); + + std::string s = "HTTP/1.1 " + + boost::lexical_cast(status_) + + " " + std::string(EnumerationToString(status_)) + + "\r\n"; + + if (keepAlive_) + { + s += "Connection: keep-alive\r\n"; + + /** + * [LIFY-2311] The "Keep-Alive" HTTP header was missing in + * Orthanc <= 1.8.0, which notably caused failures if + * uploading DICOM instances by applying Java's + * "org.apache.http.client.methods.HttpPost()" on "/instances" + * URI, if "PoolingHttpClientConnectionManager" was in used. A + * workaround was to manually set a timeout for the keep-alive + * client to, say, 200 milliseconds, by using + * "HttpClients.custom().setKeepAliveStrategy((httpResponse,httpContext)->200)". + * Note that the "timeout" value can only be integer in the + * HTTP header, so we can't use the milliseconds granularity. + **/ + s += ("Keep-Alive: timeout=" + + boost::lexical_cast(keepAliveTimeout_) + "\r\n"); + } + else + { + s += "Connection: close\r\n"; + } + + for (std::list::const_iterator + it = headers_.begin(); it != headers_.end(); ++it) + { + s += *it; + } + + if (!hasXContentTypeOptions_) + { + // Always include this header to prevent MIME Confusion attacks: + // https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options + s += X_CONTENT_TYPE_OPTIONS + ": nosniff\r\n"; + } + + if (status_ != HttpStatus_200_Ok) + { + hasContentLength_ = false; + } + + uint64_t contentLength = (hasContentLength_ ? contentLength_ : length); + s += "Content-Length: " + boost::lexical_cast(contentLength) + "\r\n\r\n"; + + stream_.Send(true, s.c_str(), s.size()); + state_ = State_WritingBody; + } + + if (hasContentLength_ && + contentPosition_ + length > contentLength_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The body size exceeds what was declared with SetContentSize()"); + } + + if (length > 0) + { + stream_.Send(false, buffer, length); + contentPosition_ += length; + } + + if (!hasContentLength_ || + contentPosition_ == contentLength_) + { + state_ = State_Done; + } + } + + + void HttpOutput::StateMachine::CloseBody() + { + switch (state_) + { + case State_WritingHeader: + SetContentLength(0); + SendBody(NULL, 0); + break; + + case State_WritingBody: + if (!hasContentLength_ || + contentPosition_ == contentLength_) + { + state_ = State_Done; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The body size has not reached what was declared with SetContentSize()"); + } + + break; + + case State_WritingMultipart: + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot invoke CloseBody() with multipart outputs"); + + case State_Done: + return; // Ignore + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const + { + // Do not compress small files since there is no real size benefit. + if (bodySize < 2048) + { + return HttpCompression_None; + } + + // Prefer "gzip" over "deflate" if the choice is offered + + if (isGzipAllowed_) + { + return HttpCompression_Gzip; + } + else if (isDeflateAllowed_) + { + return HttpCompression_Deflate; + } + else + { + return HttpCompression_None; + } + } + + + HttpOutput::HttpOutput(IHttpOutputStream &stream, + bool isKeepAlive, + unsigned int keepAliveTimeout) : + stateMachine_(stream, isKeepAlive, keepAliveTimeout), + isDeflateAllowed_(false), + isGzipAllowed_(false) + { + } + + void HttpOutput::SetDeflateAllowed(bool allowed) + { + isDeflateAllowed_ = allowed; + } + + bool HttpOutput::IsDeflateAllowed() const + { + return isDeflateAllowed_; + } + + void HttpOutput::SetGzipAllowed(bool allowed) + { + isGzipAllowed_ = allowed; + } + + bool HttpOutput::IsGzipAllowed() const + { + return isGzipAllowed_; + } + + + void HttpOutput::SendMethodNotAllowed(const std::string& allowed) + { + stateMachine_.ClearHeaders(); + stateMachine_.SetHttpStatus(HttpStatus_405_MethodNotAllowed); + stateMachine_.AddHeader("Allow", allowed); + stateMachine_.SendBody(NULL, 0); + } + + + void HttpOutput::SendStatus(HttpStatus status, + const char* message, + size_t messageSize) + { + if (status == HttpStatus_301_MovedPermanently || + //status == HttpStatus_401_Unauthorized || + status == HttpStatus_405_MethodNotAllowed) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Please use the dedicated methods to this HTTP status code in HttpOutput"); + } + + stateMachine_.SetHttpStatus(status); + + if (messageSize > 0 && + !stateMachine_.HasContentType()) + { + // Assume that the body always contains a textual description of the error + stateMachine_.SetContentType("text/plain"); + } + + stateMachine_.SendBody(message, messageSize); + } + + void HttpOutput::SendStatus(HttpStatus status) + { + SendStatus(status, NULL, 0); + } + + void HttpOutput::SendStatus(HttpStatus status, const std::string &message) + { + SendStatus(status, message.c_str(), message.size()); + } + + void HttpOutput::SetContentType(MimeType contentType) + { + stateMachine_.SetContentType(EnumerationToString(contentType)); + stateMachine_.SetContentCompressible(SystemToolbox::IsContentCompressible(contentType)); + } + + void HttpOutput::SetContentType(const std::string &contentType) + { + stateMachine_.SetContentType(contentType.c_str()); + stateMachine_.SetContentCompressible(SystemToolbox::IsContentCompressible(contentType)); + } + + void HttpOutput::SetContentFilename(const char *filename) + { + stateMachine_.SetContentFilename(filename); + } + + void HttpOutput::SetCookie(const std::string &cookie, const std::string &value) + { + stateMachine_.SetCookie(cookie, value); + } + + void HttpOutput::AddHeader(const std::string &key, const std::string &value) + { + stateMachine_.AddHeader(key, value); + } + + + void HttpOutput::Redirect(const std::string& path) + { + /** + * "HttpStatus_301_MovedPermanently" was used in Orthanc <= + * 1.12.3. This caused issues on changes in the configuration of + * Orthanc. + **/ + stateMachine_.ClearHeaders(); + stateMachine_.SetHttpStatus(HttpStatus_307_TemporaryRedirect); + stateMachine_.AddHeader("Location", path); + stateMachine_.SendBody(NULL, 0); + } + + + void HttpOutput::SendUnauthorized(const std::string& realm) + { + stateMachine_.ClearHeaders(); + stateMachine_.SetHttpStatus(HttpStatus_401_Unauthorized); + stateMachine_.AddHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\""); + stateMachine_.SendBody(NULL, 0); + } + + void HttpOutput::StartMultipart(const std::string &subType, const std::string &contentType) + { + stateMachine_.StartMultipart(subType, contentType); + } + + void HttpOutput::SendMultipartItem(const void *item, + size_t size, + const std::map &headers) + { + stateMachine_.SendMultipartItem(item, size, headers); + } + + void HttpOutput::CloseMultipart() + { + stateMachine_.CloseMultipart(); + } + + bool HttpOutput::IsWritingMultipart() const + { + return stateMachine_.GetState() == StateMachine::State_WritingMultipart; + } + + bool HttpOutput::IsWritingStream() const + { + return stateMachine_.GetState() == StateMachine::State_WritingStream; + } + + void HttpOutput::Answer(const void* buffer, + size_t length) + { + if (length == 0) + { + AnswerEmpty(); + return; + } + + HttpCompression compression = GetPreferredCompression(length); + + if (compression == HttpCompression_None || !IsContentCompressible()) + { + stateMachine_.SetContentLength(length); + stateMachine_.SendBody(buffer, length); + return; + } + + std::string compressed, encoding; + + switch (compression) + { + case HttpCompression_Deflate: + { + encoding = "deflate"; + ZlibCompressor compressor; + // Do not prefix the buffer with its uncompressed size, to be compatible with "deflate" + compressor.SetPrefixWithUncompressedSize(false); + compressor.Compress(compressed, buffer, length); + break; + } + + case HttpCompression_Gzip: + { + encoding = "gzip"; + GzipCompressor compressor; + compressor.Compress(compressed, buffer, length); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + LOG(TRACE) << "Compressing a HTTP answer using " << encoding; + + // The body is empty, do not use HTTP compression + if (compressed.size() == 0) + { + AnswerEmpty(); + } + else + { + stateMachine_.AddHeader("Content-Encoding", encoding); + stateMachine_.SetContentLength(compressed.size()); + stateMachine_.SendBody(compressed.c_str(), compressed.size()); + } + + stateMachine_.CloseBody(); + } + + + void HttpOutput::Answer(const std::string& str) + { + Answer(str.size() == 0 ? NULL : str.c_str(), str.size()); + } + + + void HttpOutput::AnswerEmpty() + { + stateMachine_.CloseBody(); + } + + + void HttpOutput::StateMachine::CheckHeadersCompatibilityWithMultipart() const + { + for (std::list::const_iterator + it = headers_.begin(); it != headers_.end(); ++it) + { + if (!Toolbox::StartsWith(*it, "Set-Cookie: ")) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The only headers that can be set in multipart answers " + "are Set-Cookie (here: " + *it + " is set)"); + } + } + } + + + static void PrepareMultipartMainHeader(std::string& boundary, + std::string& contentTypeHeader, + const std::string& subType, + const std::string& contentType) + { + if (subType != "mixed" && + subType != "related") + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + /** + * Fix for issue 54 ("Decide what to do wrt. quoting of multipart + * answers"). The "type" parameter in the "Content-Type" HTTP + * header must be quoted if it contains a forward slash "/". This + * is necessary for DICOMweb compatibility with OsiriX, but breaks + * compatibility with old releases of the client in the Orthanc + * DICOMweb plugin <= 0.3 (releases >= 0.4 work fine). + * + * Full history is available at the following locations: + * - In changeset 2248:69b0f4e8a49b: + * # hg history -v -r 2248 + * - https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=54 + * - https://groups.google.com/d/msg/orthanc-users/65zhIM5xbKI/TU5Q1_LhAwAJ + **/ + std::string tmp; + if (contentType.find('/') == std::string::npos) + { + // No forward slash in the content type + tmp = contentType; + } + else + { + // Quote the content type because of the forward slash + tmp = "\"" + contentType + "\""; + } + + boundary = Toolbox::GenerateUuid() + "-" + Toolbox::GenerateUuid(); + + /** + * Fix for issue #165: "Encapsulation boundaries must not appear + * within the encapsulations, and must be no longer than 70 + * characters, not counting the two leading hyphens." + * https://tools.ietf.org/html/rfc1521 + * https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=165 + **/ + if (boundary.size() != 36 + 1 + 36) // one UUID contains 36 characters + { + throw OrthancException(ErrorCode_InternalError); + } + + boundary = boundary.substr(0, 70); + + contentTypeHeader = ("multipart/" + subType + "; type=" + tmp + "; boundary=" + boundary); + } + + + void HttpOutput::StateMachine::StartStreamInternal(const std::string& contentType) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (status_ != HttpStatus_200_Ok) + { + SendBody(NULL, 0); + return; + } + + stream_.OnHttpStatusReceived(status_); + + std::string header = "HTTP/1.1 200 OK\r\n"; + + if (keepAlive_) + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + throw OrthancException(ErrorCode_NotImplemented, + "Multipart answers are not implemented together " + "with keep-alive connections if using Mongoose"); + +#elif ORTHANC_ENABLE_CIVETWEB == 1 +# if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1 + // Turn off Keep-Alive for multipart answers + // https://github.com/civetweb/civetweb/issues/727 + stream_.DisableKeepAlive(); + header += "Connection: close\r\n"; +# else + // The function "mg_disable_keep_alive()" is not available, + // let's continue with Keep-Alive. Performance of WADO-RS will + // decrease. + header += "Connection: keep-alive\r\n"; +# endif + +#else +# error Please support your embedded Web server here +#endif + } + else + { + header += "Connection: close\r\n"; + } + + for (std::list::const_iterator + it = headers_.begin(); it != headers_.end(); ++it) + { + header += *it; + } + + header += ("Content-Type: " + contentType + "\r\n\r\n"); + + stream_.Send(true, header.c_str(), header.size()); + } + + + void HttpOutput::StateMachine::StartMultipart(const std::string& subType, + const std::string& contentType) + { + CheckHeadersCompatibilityWithMultipart(); + + std::string contentTypeHeader; + PrepareMultipartMainHeader(multipartBoundary_, contentTypeHeader, subType, contentType); + multipartContentType_ = contentType; + + StartStreamInternal(contentTypeHeader); + + state_ = State_WritingMultipart; + } + + + void HttpOutput::StateMachine::StartStream(const std::string& contentType) + { + StartStreamInternal(contentType); + state_ = State_WritingStream; + } + + + static void PrepareMultipartItemHeader(std::string& target, + size_t length, + const std::map& headers, + const std::string& boundary, + const std::string& contentType) + { + target = "--" + boundary + "\r\n"; + + bool hasContentType = false; + bool hasContentLength = false; + bool hasMimeVersion = false; + + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + target += it->first + ": " + it->second + "\r\n"; + + std::string tmp; + Toolbox::ToLowerCase(tmp, it->first); + + if (tmp == "content-type") + { + hasContentType = true; + } + + if (tmp == "content-length") + { + hasContentLength = true; + } + + if (tmp == "mime-version") + { + hasMimeVersion = true; + } + } + + if (!hasContentType) + { + target += "Content-Type: " + contentType + "\r\n"; + } + + if (!hasContentLength) + { + target += "Content-Length: " + boost::lexical_cast(length) + "\r\n"; + } + + if (!hasMimeVersion) + { + target += "MIME-Version: 1.0\r\n\r\n"; + } + } + + + void HttpOutput::StateMachine::SendMultipartItem(const void* item, + size_t length, + const std::map& headers) + { + if (state_ != State_WritingMultipart) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + std::string header; + PrepareMultipartItemHeader(header, length, headers, multipartBoundary_, multipartContentType_); + stream_.Send(false, header.c_str(), header.size()); + + if (length > 0) + { + stream_.Send(false, item, length); + } + + stream_.Send(false, "\r\n", 2); + } + + + void HttpOutput::StateMachine::CloseMultipart() + { + if (state_ != State_WritingMultipart) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + // The two lines below might throw an exception, if the client has + // closed the connection. Such an error is ignored. + try + { + std::string header = "--" + multipartBoundary_ + "--\r\n"; + stream_.Send(false, header.c_str(), header.size()); + } + catch (OrthancException&) + { + } + + state_ = State_Done; + } + + + void HttpOutput::StateMachine::SendStreamItem(const void* data, + size_t size) + { + if (state_ != State_WritingStream) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + if (size > 0) + { + stream_.Send(false, data, size); + } + } + } + + + void HttpOutput::StateMachine::CloseStream() + { + if (state_ != State_WritingStream) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + state_ = State_Done; + } + } + + + static void AnswerStreamAsBuffer(HttpOutput& output, + IHttpStreamAnswer& stream) + { + ChunkedBuffer buffer; + + while (stream.ReadNextChunk()) + { + if (stream.GetChunkSize() > 0) + { + buffer.AddChunk(stream.GetChunkContent(), stream.GetChunkSize()); + } + } + + std::string s; + buffer.Flatten(s); + + output.SetContentType(stream.GetContentType()); + + std::string filename; + if (stream.HasContentFilename(filename)) + { + output.SetContentFilename(filename.c_str()); + } + + output.Answer(s); + } + + + void HttpOutput::Answer(IHttpStreamAnswer& stream) + { + HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_); + + switch (compression) + { + case HttpCompression_None: + { + if (isGzipAllowed_ || isDeflateAllowed_) + { + // New in Orthanc 1.5.7: Compress streams without built-in + // compression, if requested by the "Accept-Encoding" HTTP + // header + AnswerStreamAsBuffer(*this, stream); + return; + } + + break; + } + + case HttpCompression_Gzip: + stateMachine_.AddHeader("Content-Encoding", "gzip"); + break; + + case HttpCompression_Deflate: + stateMachine_.AddHeader("Content-Encoding", "deflate"); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + stateMachine_.SetContentLength(stream.GetContentLength()); + + std::string contentType = stream.GetContentType(); + if (contentType.empty()) + { + contentType = MIME_BINARY; + } + + stateMachine_.SetContentType(contentType.c_str()); + + std::string filename; + if (stream.HasContentFilename(filename)) + { + SetContentFilename(filename.c_str()); + } + + while (stream.ReadNextChunk()) + { + stateMachine_.SendBody(stream.GetChunkContent(), + stream.GetChunkSize()); + } + + stateMachine_.CloseBody(); + } + + + void HttpOutput::AnswerMultipartWithoutChunkedTransfer( + const std::string& subType, + const std::string& contentType, + const std::vector& parts, + const std::vector& sizes, + const std::vector*>& headers) + { + if (parts.size() != sizes.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + stateMachine_.CheckHeadersCompatibilityWithMultipart(); + + std::string boundary, contentTypeHeader; + PrepareMultipartMainHeader(boundary, contentTypeHeader, subType, contentType); + SetContentType(contentTypeHeader); + + std::map empty; + + ChunkedBuffer chunked; + for (size_t i = 0; i < parts.size(); i++) + { + std::string partHeader; + PrepareMultipartItemHeader(partHeader, sizes[i], headers[i] == NULL ? empty : *headers[i], + boundary, contentType); + + chunked.AddChunk(partHeader); + chunked.AddChunk(parts[i], sizes[i]); + chunked.AddChunk("\r\n"); + } + + chunked.AddChunk("--" + boundary + "--\r\n"); + + std::string body; + chunked.Flatten(body); + Answer(body); + } + + + void HttpOutput::AnswerWithoutBuffering(IHttpStreamAnswer& stream) + { + std::string contentType = stream.GetContentType(); + if (contentType.empty()) + { + contentType = MIME_BINARY; + } + + std::string filename; + if (stream.HasContentFilename(filename)) + { + stateMachine_.AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\""); + } + + stateMachine_.StartStream(contentType.c_str()); + + while (stream.ReadNextChunk()) + { + stateMachine_.SendStreamItem(stream.GetChunkContent(), stream.GetChunkSize()); + } + + stateMachine_.CloseStream(); + } + + void HttpOutput::StartStream(const std::string& contentType) + { + stateMachine_.StartStream(contentType.c_str()); + } + + void HttpOutput::SendStreamItem(const void* data, + size_t size) + { + stateMachine_.SendStreamItem(data, size); + } + + void HttpOutput::CloseStream() + { + stateMachine_.CloseStream(); + } + +} diff --git a/OrthancFramework/Sources/HttpServer/HttpOutput.h b/OrthancFramework/Sources/HttpServer/HttpOutput.h new file mode 100644 index 0000000..ebce779 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h @@ -0,0 +1,237 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Enumerations.h" +#include "IHttpOutputStream.h" +#include "IHttpStreamAnswer.h" + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC HttpOutput : public boost::noncopyable + { + private: + typedef std::list< std::pair > Header; + + class StateMachine : public boost::noncopyable + { + public: + enum State + { + State_WritingHeader, + State_WritingBody, + State_WritingMultipart, + State_Done, + State_WritingStream + }; + + private: + IHttpOutputStream& stream_; + State state_; + + bool isContentCompressible_; + HttpStatus status_; + bool hasContentLength_; + uint64_t contentLength_; + uint64_t contentPosition_; + bool keepAlive_; + unsigned int keepAliveTimeout_; + std::list headers_; + bool hasXContentTypeOptions_; + bool hasContentType_; + + std::string multipartBoundary_; + std::string multipartContentType_; + + void StartStreamInternal(const std::string& contentType); + + public: + StateMachine(IHttpOutputStream& stream, + bool isKeepAlive, + unsigned int keepAliveTimeout); + + ~StateMachine(); + + void SetHttpStatus(HttpStatus status); + + void SetContentLength(uint64_t length); + + void SetContentType(const char* contentType); + + void SetContentCompressible(bool isCompressible); + + void SetContentFilename(const char* filename); + + void SetCookie(const std::string& cookie, + const std::string& value); + + void AddHeader(const std::string& header, + const std::string& value); + + void ClearHeaders(); + + void SendBody(const void* buffer, size_t length); + + void StartMultipart(const std::string& subType, + const std::string& contentType); + + void SendMultipartItem(const void* item, + size_t length, + const std::map& headers); + + void CloseMultipart(); + + void CloseBody(); + + State GetState() const + { + return state_; + } + + bool IsContentCompressible() const; + + void CheckHeadersCompatibilityWithMultipart() const; + + void StartStream(const std::string& contentType); + + void SendStreamItem(const void* data, + size_t size); + + void CloseStream(); + + bool HasContentType() const + { + return hasContentType_; + } + }; + + StateMachine stateMachine_; + bool isDeflateAllowed_; + bool isGzipAllowed_; + + HttpCompression GetPreferredCompression(size_t bodySize) const; + + public: + HttpOutput(IHttpOutputStream& stream, + bool isKeepAlive, + unsigned int keepAliveTimeout); + + void SetDeflateAllowed(bool allowed); + + bool IsDeflateAllowed() const; + + void SetGzipAllowed(bool allowed); + + bool IsGzipAllowed() const; + + bool IsContentCompressible() const + { + return stateMachine_.IsContentCompressible(); + } + + void SendStatus(HttpStatus status, + const char* message, + size_t messageSize); + + void SendStatus(HttpStatus status); + + void SendStatus(HttpStatus status, + const std::string& message); + + void SetContentType(MimeType contentType); + + void SetContentType(const std::string& contentType); + + void SetContentFilename(const char* filename); + + void SetCookie(const std::string& cookie, + const std::string& value); + + void AddHeader(const std::string& key, + const std::string& value); + + void Answer(const void* buffer, + size_t length); + + void Answer(const std::string& str); + + void AnswerEmpty(); + + void SendMethodNotAllowed(const std::string& allowed); + + void Redirect(const std::string& path); + + void SendUnauthorized(const std::string& realm); + + void StartMultipart(const std::string& subType, + const std::string& contentType); + + void SendMultipartItem(const void* item, + size_t size, + const std::map& headers); + + void CloseMultipart(); + + bool IsWritingMultipart() const; + + void Answer(IHttpStreamAnswer& stream); + + /** + * This method is a replacement to the combination + * "StartMultipart()" + "SendMultipartItem()". It generates the + * same answer, but it gives a chance to compress the body if + * "Accept-Encoding: gzip" is provided by the client, which is not + * possible in chunked transfers. + **/ + void AnswerMultipartWithoutChunkedTransfer( + const std::string& subType, + const std::string& contentType, + const std::vector& parts, + const std::vector& sizes, + const std::vector*>& headers); + + /** + * Contrarily to "Answer()", this method doesn't bufferizes the + * stream before sending it, which reduces memory but cannot be + * used to handle compression using "Content-Encoding". + **/ + void AnswerWithoutBuffering(IHttpStreamAnswer& stream); + + void StartStream(const std::string& contentType); + + void SendStreamItem(const void* data, + size_t size); + + void CloseStream(); + + bool IsWritingStream() const; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/HttpServer.cpp b/OrthancFramework/Sources/HttpServer/HttpServer.cpp new file mode 100644 index 0000000..d210f6b --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp @@ -0,0 +1,2207 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +// http://en.highscore.de/cpp/boost/stringhandling.html + +#include "../PrecompiledHeaders.h" +#include "HttpServer.h" + +#include "../ChunkedBuffer.h" +#include "../FileBuffer.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "../TemporaryFile.h" +#include "HttpToolbox.h" +#include "IHttpHandler.h" +#include "MultipartStreamReader.h" +#include "StringHttpOutput.h" +#include + +#if ORTHANC_ENABLE_PUGIXML == 1 +# include "IWebDavBucket.h" +#endif + +#if ORTHANC_ENABLE_MONGOOSE == 1 +# include + +#elif ORTHANC_ENABLE_CIVETWEB == 1 +# include +# define MONGOOSE_USE_CALLBACKS 1 +# if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE) +# error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined +# endif +# if !defined(CIVETWEB_HAS_WEBDAV_WRITING) +# error Macro CIVETWEB_HAS_WEBDAV_WRITING must be defined +# endif +#else +# error "Either Mongoose or Civetweb must be enabled to compile this file" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(ORTHANC_ENABLE_SSL) +# error The macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if ORTHANC_ENABLE_SSL == 1 +# include +# include + +# if OPENSSL_VERSION_NUMBER < 0x30000000L +# if defined(_MSC_VER) +# pragma message("You are linking Orthanc against OpenSSL 1.x, whose license is incompatible with the GPLv3+ used by Orthanc >= 1.10.0. Please update to OpenSSL 3.x, that uses the Apache 2 license.") +# else +# warning You are linking Orthanc against OpenSSL 1.x, whose license is incompatible with the GPLv3+ used by Orthanc >= 1.10.0. Please update to OpenSSL 3.x, that uses the Apache 2 license. +# endif +# endif + +#endif + +#define ORTHANC_REALM "Orthanc Secure Area" + + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + class MongooseOutputStream : public IHttpOutputStream + { + private: + struct mg_connection* connection_; + + public: + explicit MongooseOutputStream(struct mg_connection* connection) : + connection_(connection) + { + } + + virtual void Send(bool isHeader, + const void* buffer, + size_t length) ORTHANC_OVERRIDE + { + if (length > 0) + { + // mg_write does not support buffers > 2GB (INT_MAX) -> need to split it + size_t offset = 0; + size_t remainingSize = length; + + while (remainingSize > 0) + { + size_t packetSize = std::min(remainingSize, static_cast(INT_MAX)); + + int status = mg_write(connection_, &(reinterpret_cast(buffer)[offset]), packetSize); + + if (status != static_cast(packetSize)) + { + // status == 0 when the connection has been closed, -1 on error + throw OrthancException(ErrorCode_NetworkProtocol); + } + + offset += packetSize; + remainingSize -= packetSize; + } + } + } + + virtual void OnHttpStatusReceived(HttpStatus status) ORTHANC_OVERRIDE + { + // Ignore this + } + + virtual void DisableKeepAlive() ORTHANC_OVERRIDE + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + throw OrthancException(ErrorCode_NotImplemented, + "Only available if using CivetWeb"); + +#elif ORTHANC_ENABLE_CIVETWEB == 1 +# if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1 +# if CIVETWEB_VERSION_MAJOR == 1 && CIVETWEB_VERSION_MINOR <= 13 // From "civetweb-1.13.patch" + mg_disable_keep_alive(connection_); +# else + /** + * Function "mg_disable_keep_alive()" contributed by Sebastien + * Jodogne was renamed as "mg_disable_connection_keep_alive()" + * in the official CivetWeb repository: + * https://github.com/civetweb/civetweb/commit/78d45f4c4b0ab821f4f259b21ad3783f6d6c556a + **/ + mg_disable_connection_keep_alive(connection_); +# endif +# else +# if defined(__GNUC__) || defined(__clang__) +# warning The function "mg_disable_keep_alive()" is not available, DICOMweb might run slowly +# endif + throw OrthancException(ErrorCode_NotImplemented, + "Only available if using a patched version of CivetWeb"); +# endif + +#else +# error Please support your embedded Web server here +#endif + } + }; + + + enum PostDataStatus + { + PostDataStatus_Success, + PostDataStatus_NoLength, + PostDataStatus_Pending, + PostDataStatus_Failure + }; + } + + + namespace + { + class ChunkedFile : public ChunkedBuffer + { + private: + std::string filename_; + + public: + explicit ChunkedFile(const std::string& filename) : + filename_(filename) + { + } + + const std::string& GetFilename() const + { + return filename_; + } + }; + } + + + + class HttpServer::ChunkStore : public boost::noncopyable + { + private: + typedef std::list Content; + Content content_; + unsigned int numPlaces_; + + boost::mutex mutex_; + std::set discardedFiles_; + + void Clear() + { + for (Content::iterator it = content_.begin(); + it != content_.end(); ++it) + { + delete *it; + } + } + + Content::iterator Find(const std::string& filename) + { + for (Content::iterator it = content_.begin(); + it != content_.end(); ++it) + { + if ((*it)->GetFilename() == filename) + { + return it; + } + } + + return content_.end(); + } + + void Remove(const std::string& filename) + { + Content::iterator it = Find(filename); + if (it != content_.end()) + { + delete *it; + content_.erase(it); + } + } + + public: + ChunkStore() + { + numPlaces_ = 10; + } + + ~ChunkStore() + { + Clear(); + } + + PostDataStatus Store(std::string& completed, + const void* chunkData, + size_t chunkSize, + const std::string& filename, + size_t filesize) + { + boost::mutex::scoped_lock lock(mutex_); + + std::set::iterator wasDiscarded = discardedFiles_.find(filename); + if (wasDiscarded != discardedFiles_.end()) + { + discardedFiles_.erase(wasDiscarded); + return PostDataStatus_Failure; + } + + ChunkedFile* f; + Content::iterator it = Find(filename); + if (it == content_.end()) + { + f = new ChunkedFile(filename); + + // Make some room + if (content_.size() >= numPlaces_) + { + discardedFiles_.insert(content_.front()->GetFilename()); + delete content_.front(); + content_.pop_front(); + } + + content_.push_back(f); + } + else + { + f = *it; + } + + f->AddChunk(chunkData, chunkSize); + + if (f->GetNumBytes() > filesize) + { + Remove(filename); + } + else if (f->GetNumBytes() == filesize) + { + f->Flatten(completed); + Remove(filename); + return PostDataStatus_Success; + } + + return PostDataStatus_Pending; + } + + /*void Print() + { + boost::mutex::scoped_lock lock(mutex_); + + printf("ChunkStore status:\n"); + for (Content::const_iterator i = content_.begin(); + i != content_.end(); i++) + { + printf(" [%s]: %d\n", (*i)->GetFilename().c_str(), (*i)->GetNumBytes()); + } + printf("-----\n"); + }*/ + }; + + + struct HttpServer::PImpl + { + struct mg_context *context_; + ChunkStore chunkStore_; + + PImpl() : + context_(NULL) + { + } + }; + + + class HttpServer::MultipartFormDataHandler : public MultipartStreamReader::IHandler + { + private: + IHttpHandler& handler_; + ChunkStore& chunkStore_; + const std::string& remoteIp_; + const std::string& username_; + const UriComponents& uri_; + bool isJQueryUploadChunk_; + std::string jqueryUploadFileName_; + size_t jqueryUploadFileSize_; + + void HandleInternal(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) + { + StringHttpOutput stringOutput; + HttpOutput fakeOutput(stringOutput, false /* assume no keep-alive */, 0); + HttpToolbox::GetArguments getArguments; + + if (!handler_.Handle(fakeOutput, RequestOrigin_RestApi, remoteIp_.c_str(), username_.c_str(), + HttpMethod_Post, uri_, headers, getArguments, part, size)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + public: + MultipartFormDataHandler(IHttpHandler& handler, + ChunkStore& chunkStore, + const std::string& remoteIp, + const std::string& username, + const UriComponents& uri, + const MultipartStreamReader::HttpHeaders& headers) : + handler_(handler), + chunkStore_(chunkStore), + remoteIp_(remoteIp), + username_(username), + uri_(uri), + isJQueryUploadChunk_(false), + jqueryUploadFileSize_(0) // Dummy initialization + { + typedef HttpToolbox::Arguments::const_iterator Iterator; + + Iterator requestedWith = headers.find("x-requested-with"); + if (requestedWith != headers.end() && + requestedWith->second != "XMLHttpRequest") + { + throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-Requested-With\" should be " + "\"XMLHttpRequest\" in multipart uploads"); + } + + Iterator fileName = headers.find("x-file-name"); + Iterator fileSize = headers.find("x-file-size"); + if (fileName != headers.end() || + fileSize != headers.end()) + { + if (fileName == headers.end()) + { + throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Name\" is missing"); + } + + if (fileSize == headers.end()) + { + throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Size\" is missing"); + } + + isJQueryUploadChunk_ = true; + jqueryUploadFileName_ = fileName->second; + + try + { + int64_t s = boost::lexical_cast(fileSize->second); + if (s < 0) + { + throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Size\" has negative value"); + } + else + { + jqueryUploadFileSize_ = static_cast(s); + if (static_cast(jqueryUploadFileSize_) != s) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + } + catch (boost::bad_lexical_cast& e) + { + throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Size\" is not an integer"); + } + } + } + + virtual void HandlePart(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) ORTHANC_OVERRIDE + { + if (isJQueryUploadChunk_) + { + std::string completedFile; + + PostDataStatus status = chunkStore_.Store(completedFile, part, size, jqueryUploadFileName_, jqueryUploadFileSize_); + + switch (status) + { + case PostDataStatus_Failure: + throw OrthancException(ErrorCode_NetworkProtocol, "Error in the multipart form upload"); + + case PostDataStatus_Success: + assert(completedFile.size() == jqueryUploadFileSize_); + HandleInternal(headers, completedFile.empty() ? NULL : completedFile.c_str(), completedFile.size()); + break; + + case PostDataStatus_Pending: + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + HandleInternal(headers, part, size); + } + } + }; + + + void HttpServer::ProcessMultipartFormData(const std::string& remoteIp, + const std::string& username, + const UriComponents& uri, + const std::map& headers, + const std::string& body, + const std::string& boundary) + { + MultipartFormDataHandler handler(GetHandler(), pimpl_->chunkStore_, remoteIp, username, uri, headers); + + MultipartStreamReader reader(boundary); + reader.SetHandler(handler); + reader.AddChunk(body); + reader.CloseStream(); + } + + + static PostDataStatus ReadBodyWithContentLength(std::string& body, + struct mg_connection *connection, + const std::string& contentLength) + { + size_t length; + try + { + int64_t tmp = boost::lexical_cast(contentLength); + if (tmp < 0) + { + return PostDataStatus_NoLength; + } + + length = static_cast(tmp); + } + catch (boost::bad_lexical_cast&) + { + return PostDataStatus_NoLength; + } + + body.resize(length); + + size_t pos = 0; + while (length > 0) + { + int r = mg_read(connection, &body[pos], length); + if (r <= 0) + { + return PostDataStatus_Failure; + } + + assert(static_cast(r) <= length); + length -= r; + pos += r; + } + + return PostDataStatus_Success; + } + + + static PostDataStatus ReadBodyWithoutContentLength(std::string& body, + struct mg_connection *connection) + { + // Store the individual chunks in a temporary file, then read it + // back into the memory buffer "body" + FileBuffer buffer; + + std::string tmp(1024 * 1024, 0); + + for (;;) + { + int r = mg_read(connection, &tmp[0], tmp.size()); + if (r < 0) + { + return PostDataStatus_Failure; + } + else if (r == 0) + { + break; + } + else + { + buffer.Append(tmp.c_str(), r); + } + } + + buffer.Read(body); + + return PostDataStatus_Success; + } + + + static PostDataStatus ReadBodyToString(std::string& body, + struct mg_connection *connection, + const HttpToolbox::Arguments& headers) + { + HttpToolbox::Arguments::const_iterator contentLength = headers.find("content-length"); + + if (contentLength != headers.end()) + { + // "Content-Length" is available + return ReadBodyWithContentLength(body, connection, contentLength->second); + } + else + { + // No Content-Length + return ReadBodyWithoutContentLength(body, connection); + } + } + + + static PostDataStatus ReadBodyToStream(IHttpHandler::IChunkedRequestReader& stream, + struct mg_connection *connection, + const HttpToolbox::Arguments& headers) + { + HttpToolbox::Arguments::const_iterator contentLength = headers.find("content-length"); + + if (contentLength != headers.end()) + { + // "Content-Length" is available + std::string body; + PostDataStatus status = ReadBodyWithContentLength(body, connection, contentLength->second); + + if (status == PostDataStatus_Success && + !body.empty()) + { + stream.AddBodyChunk(body.c_str(), body.size()); + } + + return status; + } + else + { + // No Content-Length: This is a chunked transfer. Stream the HTTP connection. + std::string tmp(1024 * 1024, 0); + + for (;;) + { + int r = mg_read(connection, &tmp[0], tmp.size()); + if (r < 0) + { + return PostDataStatus_Failure; + } + else if (r == 0) + { + break; + } + else + { + stream.AddBodyChunk(tmp.c_str(), r); + } + } + + return PostDataStatus_Success; + } + } + + + enum AccessMode + { + AccessMode_Forbidden, + AccessMode_AuthorizationToken, + AccessMode_RegisteredUser + }; + + + static AccessMode IsAccessGranted(const HttpServer& that, + const HttpToolbox::Arguments& headers) + { + static const std::string BASIC = "Basic "; + static const std::string BEARER = "Bearer "; + + HttpToolbox::Arguments::const_iterator auth = headers.find("authorization"); + if (auth != headers.end()) + { + std::string s = auth->second; + if (boost::starts_with(s, BASIC)) + { + std::string b64 = s.substr(BASIC.length()); + if (that.IsValidBasicHttpAuthentication(b64)) + { + return AccessMode_RegisteredUser; + } + } + else if (boost::starts_with(s, BEARER) && + that.GetIncomingHttpRequestFilter() != NULL) + { + // New in Orthanc 1.8.1 + std::string token = s.substr(BEARER.length()); + if (that.GetIncomingHttpRequestFilter()->IsValidBearerToken(token)) + { + return AccessMode_AuthorizationToken; + } + } + } + + return AccessMode_Forbidden; + } + + + static std::string GetAuthenticatedUsername(const HttpToolbox::Arguments& headers) + { + HttpToolbox::Arguments::const_iterator auth = headers.find("authorization"); + + if (auth == headers.end()) + { + return ""; + } + + std::string s = auth->second; + if (s.size() <= 6 || + s.substr(0, 6) != "Basic ") + { + return ""; + } + + std::string b64 = s.substr(6); + std::string decoded; + Toolbox::DecodeBase64(decoded, b64); + size_t semicolons = decoded.find(':'); + + if (semicolons == std::string::npos) + { + // Bad-formatted request + return ""; + } + else + { + return decoded.substr(0, semicolons); + } + } + + + static bool ExtractMethod(HttpMethod& method, + const struct mg_request_info *request, + const HttpToolbox::Arguments& headers, + const HttpToolbox::GetArguments& argumentsGET) + { + std::string overridden; + + // Check whether some PUT/DELETE faking is done + + // 1. Faking with Google's approach + HttpToolbox::Arguments::const_iterator methodOverride = + headers.find("x-http-method-override"); + + if (methodOverride != headers.end()) + { + overridden = methodOverride->second; + } + else if (!strcmp(request->request_method, "GET")) + { + // 2. Faking with Ruby on Rail's approach + // GET /my/resource?_method=delete <=> DELETE /my/resource + for (size_t i = 0; i < argumentsGET.size(); i++) + { + if (argumentsGET[i].first == "_method") + { + overridden = argumentsGET[i].second; + break; + } + } + } + + if (overridden.size() > 0) + { + // A faking has been done within this request + Toolbox::ToUpperCase(overridden); + + CLOG(INFO, HTTP) << "HTTP method faking has been detected for " << overridden; + + if (overridden == "PUT") + { + method = HttpMethod_Put; + return true; + } + else if (overridden == "DELETE") + { + method = HttpMethod_Delete; + return true; + } + else + { + return false; + } + } + + // No PUT/DELETE faking was present + if (!strcmp(request->request_method, "GET")) + { + method = HttpMethod_Get; + } + else if (!strcmp(request->request_method, "POST")) + { + method = HttpMethod_Post; + } + else if (!strcmp(request->request_method, "DELETE")) + { + method = HttpMethod_Delete; + } + else if (!strcmp(request->request_method, "PUT")) + { + method = HttpMethod_Put; + } + else + { + return false; + } + + return true; + } + + + static void ConfigureHttpCompression(HttpOutput& output, + const HttpToolbox::Arguments& headers) + { + // Look if the client wishes HTTP compression + // https://en.wikipedia.org/wiki/HTTP_compression + HttpToolbox::Arguments::const_iterator it = headers.find("accept-encoding"); + if (it != headers.end()) + { + std::vector encodings; + Toolbox::TokenizeString(encodings, it->second, ','); + + for (size_t i = 0; i < encodings.size(); i++) + { + std::string s = Toolbox::StripSpaces(encodings[i]); + + if (s == "deflate") + { + output.SetDeflateAllowed(true); + } + else if (s == "gzip") + { + output.SetGzipAllowed(true); + } + } + } + } + + +#if ORTHANC_ENABLE_PUGIXML == 1 + +# if CIVETWEB_HAS_WEBDAV_WRITING == 0 + static void AnswerWebDavReadOnly(HttpOutput& output, + const std::string& uri) + { + CLOG(ERROR, HTTP) << "Orthanc was compiled without support for read-write access to WebDAV: " << uri; + output.SendStatus(HttpStatus_403_Forbidden); + } +# endif + + static bool HandleWebDav(HttpOutput& output, + const HttpServer::WebDavBuckets& buckets, + const std::string& method, + const HttpToolbox::Arguments& headers, + const std::string& uri, + struct mg_connection *connection /* to read the PUT body if need be */) + { + if (buckets.empty()) + { + return false; // Speed up things if WebDAV is not used + } + + /** + * The "buckets" maps an URI relative to the root of the + * bucket, to the content of the bucket. The root URI does *not* + * contain a trailing slash. + **/ + + if (method == "OPTIONS") + { + // Remove the trailing slash, if any (necessary for davfs2) + std::string s = uri; + if (!s.empty() && + s[s.size() - 1] == '/') + { + s.resize(s.size() - 1); + } + + HttpServer::WebDavBuckets::const_iterator bucket = buckets.find(s); + if (bucket == buckets.end()) + { + return false; + } + else + { + output.AddHeader("DAV", "1,2"); // Necessary for Windows XP + +#if CIVETWEB_HAS_WEBDAV_WRITING == 1 + output.AddHeader("Allow", "GET, PUT, DELETE, OPTIONS, PROPFIND, HEAD, LOCK, UNLOCK, PROPPATCH, MKCOL"); +#else + output.AddHeader("Allow", "GET, PUT, DELETE, OPTIONS, PROPFIND, HEAD"); +#endif + + output.SendStatus(HttpStatus_200_Ok); + return true; + } + } + else if (method == "GET" || + method == "PROPFIND" || + method == "PROPPATCH" || + method == "PUT" || + method == "DELETE" || + method == "HEAD" || + method == "LOCK" || + method == "UNLOCK" || + method == "MKCOL") + { + // Locate the WebDAV bucket of interest, if any + for (HttpServer::WebDavBuckets::const_iterator bucket = buckets.begin(); + bucket != buckets.end(); ++bucket) + { + assert(!bucket->first.empty() && + bucket->first[bucket->first.size() - 1] != '/' && + bucket->second != NULL); + + if (uri == bucket->first || + boost::starts_with(uri, bucket->first + "/")) + { + std::string s = uri.substr(bucket->first.size()); + if (s.empty()) + { + s = "/"; + } + + std::vector path; + Toolbox::SplitUriComponents(path, s); + + + /** + * WebDAV - PROPFIND + **/ + + if (method == "PROPFIND") + { + HttpToolbox::Arguments::const_iterator i = headers.find("depth"); + if (i == headers.end()) + { + throw OrthancException(ErrorCode_NetworkProtocol, "WebDAV PROPFIND without depth"); + } + + int depth = boost::lexical_cast(i->second); + if (depth != 0 && + depth != 1) + { + throw OrthancException( + ErrorCode_NetworkProtocol, + "WebDAV PROPFIND at unsupported depth (can only be 0 or 1): " + i->second); + } + + std::string answer; + + MimeType mime; + std::string content; + boost::posix_time::ptime modificationTime = boost::posix_time::second_clock::universal_time(); + + if (bucket->second->IsExistingFolder(path)) + { + if (depth == 0) + { + IWebDavBucket::Collection c; + c.Format(answer, uri); + } + else if (depth == 1) + { + IWebDavBucket::Collection c; + + if (!bucket->second->ListCollection(c, path)) + { + output.SendStatus(HttpStatus_404_NotFound); + return true; + } + + c.Format(answer, uri); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + else if (!path.empty() && + bucket->second->GetFileContent(mime, content, modificationTime, path)) + { + if (depth == 0 || + depth == 1) + { + std::unique_ptr f(new IWebDavBucket::File(path.back())); + f->SetContentLength(content.size()); + f->SetModificationTime(modificationTime); + f->SetMimeType(mime); + + IWebDavBucket::Collection c; + c.AddResource(f.release()); + + std::vector p; + Toolbox::SplitUriComponents(p, uri); + if (p.empty()) + { + throw OrthancException(ErrorCode_InternalError); + } + + p.resize(p.size() - 1); + c.Format(answer, Toolbox::FlattenUri(p)); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + output.SendStatus(HttpStatus_404_NotFound); + return true; + } + + output.AddHeader("Content-Type", "application/xml; charset=UTF-8"); + output.SendStatus(HttpStatus_207_MultiStatus, answer); + return true; + } + + + /** + * WebDAV - GET and HEAD + **/ + + else if (method == "GET" || + method == "HEAD") + { + MimeType mime; + std::string content; + boost::posix_time::ptime modificationTime; + + if (bucket->second->GetFileContent(mime, content, modificationTime, path)) + { + output.AddHeader("Content-Type", EnumerationToString(mime)); + + // "Last-Modified" is necessary on Windows XP. The "Z" + // suffix is mandatory on Windows >= 7. + output.AddHeader("Last-Modified", boost::posix_time::to_iso_extended_string(modificationTime) + "Z"); + + if (method == "GET") + { + output.Answer(content); + } + else + { + output.SendStatus(HttpStatus_200_Ok); + } + } + else + { + output.SendStatus(HttpStatus_404_NotFound); + } + + return true; + } + + + /** + * WebDAV - PUT + **/ + + else if (method == "PUT") + { +#if CIVETWEB_HAS_WEBDAV_WRITING == 1 + std::string body; + if (ReadBodyToString(body, connection, headers) == PostDataStatus_Success) + { + if (bucket->second->StoreFile(body, path)) + { + //output.SendStatus(HttpStatus_200_Ok); + output.SendStatus(HttpStatus_201_Created); + } + else + { + output.SendStatus(HttpStatus_403_Forbidden); + } + } + else + { + CLOG(ERROR, HTTP) << "Cannot read the content of a file to be stored in WebDAV"; + output.SendStatus(HttpStatus_400_BadRequest); + } +#else + AnswerWebDavReadOnly(output, uri); +#endif + + return true; + } + + + /** + * WebDAV - DELETE + **/ + + else if (method == "DELETE") + { + if (bucket->second->DeleteItem(path)) + { + output.SendStatus(HttpStatus_204_NoContent); + } + else + { + output.SendStatus(HttpStatus_403_Forbidden); + } + return true; + } + + + /** + * WebDAV - MKCOL + **/ + + else if (method == "MKCOL") + { +#if CIVETWEB_HAS_WEBDAV_WRITING == 1 + if (bucket->second->CreateFolder(path)) + { + //output.SendStatus(HttpStatus_200_Ok); + output.SendStatus(HttpStatus_201_Created); + } + else + { + output.SendStatus(HttpStatus_403_Forbidden); + } +#else + AnswerWebDavReadOnly(output, uri); +#endif + + return true; + } + + + /** + * WebDAV - Faking PROPPATCH, LOCK and UNLOCK + **/ + + else if (method == "PROPPATCH") + { +#if CIVETWEB_HAS_WEBDAV_WRITING == 1 + IWebDavBucket::AnswerFakedProppatch(output, uri); +#else + AnswerWebDavReadOnly(output, uri); +#endif + return true; + } + else if (method == "LOCK") + { +#if CIVETWEB_HAS_WEBDAV_WRITING == 1 + IWebDavBucket::AnswerFakedLock(output, uri); +#else + AnswerWebDavReadOnly(output, uri); +#endif + return true; + } + else if (method == "UNLOCK") + { +#if CIVETWEB_HAS_WEBDAV_WRITING == 1 + IWebDavBucket::AnswerFakedUnlock(output); +#else + AnswerWebDavReadOnly(output, uri); +#endif + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + return false; + } + else + { + /** + * WebDAV - Unapplicable method (such as POST and DELETE) + **/ + + return false; + } + } +#endif /* ORTHANC_ENABLE_PUGIXML == 1 */ + + + static void InternalCallback(HttpOutput& output /* out */, + HttpMethod& method /* out */, + HttpServer& server, + struct mg_connection *connection, + const struct mg_request_info *request) + { + bool localhost; + +#if ORTHANC_ENABLE_MONGOOSE == 1 + static const long LOCALHOST = (127ll << 24) + 1ll; + localhost = (request->remote_ip == LOCALHOST); +#elif ORTHANC_ENABLE_CIVETWEB == 1 + // The "remote_ip" field of "struct mg_request_info" is tagged as + // deprecated in Civetweb, using "remote_addr" instead. + localhost = (std::string(request->remote_addr) == "127.0.0.1"); +#else +# error +#endif + + // Check remote calls + if (!server.IsRemoteAccessAllowed() && + !localhost) + { + output.SendUnauthorized(server.GetRealm()); // 401 error + return; + } + + + // Extract the HTTP headers + HttpToolbox::Arguments headers; + for (int i = 0; i < request->num_headers; i++) + { + std::string name = request->http_headers[i].name; + std::string value = request->http_headers[i].value; + + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + headers.insert(std::make_pair(name, value)); + CLOG(TRACE, HTTP) << "HTTP header: [" << name << "]: [" << value << "]"; + } + + if (server.IsHttpCompressionEnabled()) + { + ConfigureHttpCompression(output, headers); + } + + + // Extract the GET arguments + HttpToolbox::GetArguments argumentsGET; + if (!strcmp(request->request_method, "GET")) + { + HttpToolbox::ParseGetArguments(argumentsGET, request->query_string); + } + + + AccessMode accessMode = IsAccessGranted(server, headers); + + // Authenticate this connection + if (server.IsAuthenticationEnabled() && + accessMode == AccessMode_Forbidden) + { + output.SendUnauthorized(server.GetRealm()); // 401 error + return; + } + + +#if ORTHANC_ENABLE_MONGOOSE == 1 + // Apply the filter, if it is installed + char remoteIp[24]; + sprintf(remoteIp, "%d.%d.%d.%d", + reinterpret_cast(&request->remote_ip) [3], + reinterpret_cast(&request->remote_ip) [2], + reinterpret_cast(&request->remote_ip) [1], + reinterpret_cast(&request->remote_ip) [0]); + + const char* requestUri = request->uri; + +#elif ORTHANC_ENABLE_CIVETWEB == 1 + const char* remoteIp = request->remote_addr; + const char* requestUri = request->local_uri; +#else +# error +#endif + + if (requestUri == NULL) + { + requestUri = ""; + } + + // Decompose the URI into its components + UriComponents uri; + try + { + Toolbox::SplitUriComponents(uri, requestUri); + } + catch (OrthancException&) + { + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + + + // Compute the HTTP method, taking method faking into consideration + method = HttpMethod_Get; + +#if ORTHANC_ENABLE_PUGIXML == 1 + bool isWebDav = false; +#endif + + HttpMethod filterMethod; + + std::unique_ptr apiLogTimer; // to log the time spent in the API call + + if (ExtractMethod(method, request, headers, argumentsGET)) + { + apiLogTimer.reset(new Toolbox::ApiElapsedTimeLogger(std::string(EnumerationToString(method)) + " " + Toolbox::FlattenUri(uri))); + + filterMethod = method; + } +#if ORTHANC_ENABLE_PUGIXML == 1 + else if (!strcmp(request->request_method, "OPTIONS") || + !strcmp(request->request_method, "PROPFIND") || + !strcmp(request->request_method, "HEAD")) + { + apiLogTimer.reset(new Toolbox::ApiElapsedTimeLogger(std::string("Incoming read-only WebDAV request: ") + request->request_method + " " + requestUri)); + filterMethod = HttpMethod_Get; + isWebDav = true; + } + else if (!strcmp(request->request_method, "PROPPATCH") || + !strcmp(request->request_method, "LOCK") || + !strcmp(request->request_method, "UNLOCK") || + !strcmp(request->request_method, "MKCOL")) + { + apiLogTimer.reset(new Toolbox::ApiElapsedTimeLogger(std::string("Incoming read-write WebDAV request: ") + request->request_method + " " + requestUri)); + filterMethod = HttpMethod_Put; + isWebDav = true; + } +#endif /* ORTHANC_ENABLE_PUGIXML == 1 */ + else + { + CLOG(INFO, HTTP) << "Unknown HTTP method: " << request->request_method; + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + + + const std::string username = GetAuthenticatedUsername(headers); + + if (accessMode != AccessMode_AuthorizationToken) + { + // Check that this access is granted by the user's authorization + // filter. In the case of an authorization bearer token, grant + // full access to the API. + + assert(accessMode == AccessMode_Forbidden || // Could be the case if "!server.IsAuthenticationEnabled()" + accessMode == AccessMode_RegisteredUser); + + IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); + if (filter != NULL && + !filter->IsAllowed(filterMethod, requestUri, remoteIp, + username.c_str(), headers, argumentsGET)) + { + output.SendStatus(HttpStatus_403_Forbidden); + return; + } + } + + +#if ORTHANC_ENABLE_PUGIXML == 1 + if (HandleWebDav(output, server.GetWebDavBuckets(), request->request_method, + headers, requestUri, connection)) + { + return; + } + else if (isWebDav) + { + CLOG(INFO, HTTP) << "No WebDAV bucket is registered against URI: " + << request->request_method << " " << requestUri; + output.SendStatus(HttpStatus_404_NotFound); + return; + } +#endif + + + bool found = false; + + // Extract the body of the request for PUT and POST, or process + // the body as a stream + + std::string body; + if (method == HttpMethod_Post || + method == HttpMethod_Put) + { + PostDataStatus status; + + bool isMultipartForm = false; + + std::string type, subType, boundary; + HttpToolbox::Arguments::const_iterator ct = headers.find("content-type"); + if (method == HttpMethod_Post && + ct != headers.end() && + MultipartStreamReader::ParseMultipartContentType(type, subType, boundary, ct->second) && + type == "multipart/form-data") + { + /** + * The user uses the "upload" form of Orthanc Explorer, for + * file uploads through a HTML form. + **/ + isMultipartForm = true; + + status = ReadBodyToString(body, connection, headers); + if (status == PostDataStatus_Success) + { + server.ProcessMultipartFormData(remoteIp, username, uri, headers, body, boundary); + output.SendStatus(HttpStatus_200_Ok); + return; + } + } + + if (!isMultipartForm) + { + std::unique_ptr stream; + + if (server.HasHandler()) + { + found = server.GetHandler().CreateChunkedRequestReader + (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers); + } + + if (found) + { + if (stream.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + status = ReadBodyToStream(*stream, connection, headers); + + if (status == PostDataStatus_Success) + { + stream->Execute(output); + } + } + else + { + status = ReadBodyToString(body, connection, headers); + } + } + + switch (status) + { + case PostDataStatus_NoLength: + output.SendStatus(HttpStatus_411_LengthRequired); + return; + + case PostDataStatus_Failure: + output.SendStatus(HttpStatus_400_BadRequest); + return; + + case PostDataStatus_Pending: + output.AnswerEmpty(); + return; + + case PostDataStatus_Success: + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + if (!found && + server.HasHandler()) + { + found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), + method, uri, headers, argumentsGET, body.c_str(), body.size()); + } + + if (!found) + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + static void ProtectedCallback(struct mg_connection *connection, + const struct mg_request_info *request) + { + try + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + void *that = request->user_data; + const char* requestUri = request->uri; +#elif ORTHANC_ENABLE_CIVETWEB == 1 + // https://github.com/civetweb/civetweb/issues/409 + void *that = mg_get_user_data(mg_get_context(connection)); + const char* requestUri = request->local_uri; +#else +# error +#endif + + if (requestUri == NULL) + { + requestUri = ""; + } + + HttpServer* server = reinterpret_cast(that); + + if (server == NULL) + { + MongooseOutputStream stream(connection); + HttpOutput output(stream, false /* assume no keep-alive */, 0); + output.SendStatus(HttpStatus_500_InternalServerError); + return; + } + + MongooseOutputStream stream(connection); + HttpOutput output(stream, server->IsKeepAliveEnabled(), server->GetKeepAliveTimeout()); + HttpMethod method = HttpMethod_Get; + + try + { + try + { + InternalCallback(output, method, *server, connection, request); + } + catch (OrthancException&) + { + throw; // Pass the exception to the main handler below + } + // Now convert native exceptions as OrthancException + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_BadParameterType, + "Syntax error in some user-supplied data"); + } + catch (boost::filesystem::filesystem_error& e) + { + throw OrthancException(ErrorCode_InternalError, + "Error while accessing the filesystem: " + e.path1().string()); + } + catch (std::runtime_error&) + { + throw OrthancException(ErrorCode_BadRequest, + "Presumably an error while parsing the JSON body"); + } + catch (std::bad_alloc&) + { + throw OrthancException(ErrorCode_NotEnoughMemory, + "The server hosting Orthanc is running out of memory"); + } + catch (...) + { + throw OrthancException(ErrorCode_InternalError, + "An unhandled exception was generated inside the HTTP server"); + } + } + catch (OrthancException& e) + { + assert(server != NULL); + + // Using this candidate handler results in an exception + try + { + if (server->GetExceptionFormatter() == NULL) + { + CLOG(ERROR, HTTP) << "Exception in the HTTP handler: " << e.What(); + output.SendStatus(e.GetHttpStatus()); + } + else + { + server->GetExceptionFormatter()->Format(output, e, method, requestUri); + } + } + catch (OrthancException&) + { + // An exception here reflects the fact that the status code + // was already set by the HTTP handler. + } + } + } + catch (...) + { + // We should never arrive at this point, where it is even impossible to send an answer + CLOG(ERROR, HTTP) << "Catastrophic error inside the HTTP server, giving up"; + } + } + + static uint16_t threadCounter = 0; + +#if MONGOOSE_USE_CALLBACKS == 0 + static void* Callback(enum mg_event event, + struct mg_connection *connection, + const struct mg_request_info *request) + { + if (event == MG_NEW_REQUEST) + { + ProtectedCallback(connection, request); + + // Mark as processed + return (void*) ""; + } + else + { + return NULL; + } + } + +#elif MONGOOSE_USE_CALLBACKS == 1 + static int Callback(struct mg_connection *connection) + { + const struct mg_request_info *request = mg_get_request_info(connection); + + if (!Logging::HasCurrentThreadName()) + { + Logging::SetCurrentThreadName(std::string("HTTP-") + boost::lexical_cast(threadCounter++)); + } + + ProtectedCallback(connection, request); + + return 1; // Do not let Mongoose handle the request by itself + } + +#else +# error Please set MONGOOSE_USE_CALLBACKS +#endif + + + + + + bool HttpServer::IsRunning() const + { + return (pimpl_->context_ != NULL); + } + + + HttpServer::HttpServer() : + pimpl_(new PImpl), + handler_(NULL), + remoteAllowed_(false), + authentication_(false), + sslVerifyPeers_(false), + ssl_(false), + sslMinimumVersion_(0), // Default to any of "SSL2+SSL3+TLS1.0+TLS1.1+TLS1.2" + sslHasCiphers_(false), + port_(8000), + filter_(NULL), + keepAlive_(false), + keepAliveTimeout_(1), + httpCompression_(true), + exceptionFormatter_(NULL), + realm_(ORTHANC_REALM), + threadsCount_(50), // Default value in mongoose/civetweb + tcpNoDelay_(true), + requestTimeout_(30) // Default value in mongoose/civetweb (30 seconds) + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + CLOG(INFO, HTTP) << "This Orthanc server uses Mongoose as its embedded HTTP server"; +#endif + +#if ORTHANC_ENABLE_CIVETWEB == 1 + CLOG(INFO, HTTP) << "This Orthanc server uses CivetWeb as its embedded HTTP server"; +#endif + +#if ORTHANC_ENABLE_SSL == 1 + // Check for the Heartbleed exploit + // https://en.wikipedia.org/wiki/OpenSSL#Heartbleed_bug + if (OPENSSL_VERSION_NUMBER < 0x1000107fL /* openssl-1.0.1g */ && + OPENSSL_VERSION_NUMBER >= 0x1000100fL /* openssl-1.0.1 */) + { + CLOG(WARNING, HTTP) << "This version of OpenSSL is vulnerable to the Heartbleed exploit"; + } +#endif + } + + + HttpServer::~HttpServer() + { + Stop(); + +#if ORTHANC_ENABLE_PUGIXML == 1 + for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } +#endif + } + + + void HttpServer::SetPortNumber(uint16_t port) + { + Stop(); + port_ = port; + } + + uint16_t HttpServer::GetPortNumber() const + { + return port_; + } + + void HttpServer::Start() + { + // reset thread counter used to generate HTTP thread names. + threadCounter = 0; + +#if ORTHANC_ENABLE_MONGOOSE == 1 + CLOG(INFO, HTTP) << "Starting embedded Web server using Mongoose"; +#elif ORTHANC_ENABLE_CIVETWEB == 1 + CLOG(INFO, HTTP) << "Starting embedded Web server using Civetweb"; +#else +# error +#endif + + if (!IsRunning()) + { + std::string port = boost::lexical_cast(port_); + std::string numThreads = boost::lexical_cast(threadsCount_); + std::string requestTimeoutMilliseconds = boost::lexical_cast(requestTimeout_ * 1000); + std::string keepAliveTimeoutMilliseconds = boost::lexical_cast(keepAliveTimeout_ * 1000); + std::string sslMinimumVersion = boost::lexical_cast(sslMinimumVersion_); + + if (ssl_) + { + port += "s"; + } + + std::vector options; + + // Set the TCP port for the HTTP server + options.push_back("listening_ports"); + options.push_back(port.c_str()); + + // Optimization reported by Chris Hafey + // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ + options.push_back("enable_keep_alive"); + options.push_back(keepAlive_ ? "yes" : "no"); + +#if ORTHANC_ENABLE_CIVETWEB == 1 + /** + * The "keep_alive_timeout_ms" cannot use milliseconds, as the + * value of "timeout" in the HTTP header "Keep-Alive" must be + * expressed in seconds (at least for the Java client). + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive + * https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no + * https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#keep_alive_timeout_ms-500-or-0 + **/ + options.push_back("keep_alive_timeout_ms"); + options.push_back(keepAlive_ ? keepAliveTimeoutMilliseconds.c_str() : "0"); +#endif + +#if ORTHANC_ENABLE_CIVETWEB == 1 + // Disable TCP Nagle's algorithm to maximize speed (this + // option is not available in Mongoose). + // https://groups.google.com/d/topic/civetweb/35HBR9seFjU/discussion + // https://eklitzke.org/the-caveats-of-tcp-nodelay + options.push_back("tcp_nodelay"); + options.push_back(tcpNoDelay_ ? "1" : "0"); +#endif + + // Set the number of threads + options.push_back("num_threads"); + options.push_back(numThreads.c_str()); + + // Set the timeout for the HTTP server + options.push_back("request_timeout_ms"); + options.push_back(requestTimeoutMilliseconds.c_str()); + + // Set the client authentication + options.push_back("ssl_verify_peer"); + options.push_back(sslVerifyPeers_ ? "yes" : "no"); + + if (sslVerifyPeers_) + { + // Set the trusted client certificates (for X509 mutual authentication) + options.push_back("ssl_ca_file"); + options.push_back(trustedClientCertificates_.c_str()); + } + + if (ssl_) + { + // Restrict minimum SSL/TLS protocol version + options.push_back("ssl_protocol_version"); + options.push_back(sslMinimumVersion.c_str()); + + // Set the accepted ciphers list + if (sslHasCiphers_) + { + options.push_back("ssl_cipher_list"); + options.push_back(sslCiphers_.c_str()); + } + + // Set the SSL certificate, if any + options.push_back("ssl_certificate"); + options.push_back(certificate_.c_str()); + }; + + assert(options.size() % 2 == 0); + options.push_back(NULL); + +#if MONGOOSE_USE_CALLBACKS == 0 + pimpl_->context_ = mg_start(&Callback, this, &options[0]); + +#elif MONGOOSE_USE_CALLBACKS == 1 + struct mg_callbacks callbacks; + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.begin_request = Callback; + pimpl_->context_ = mg_start(&callbacks, this, &options[0]); + +#else +# error Please set MONGOOSE_USE_CALLBACKS +#endif + + if (!pimpl_->context_) + { + bool isSslError = false; + +#if ORTHANC_ENABLE_SSL == 1 + for (;;) + { + unsigned long code = ERR_get_error(); + if (code == 0) + { + break; + } + else + { + isSslError = true; + char message[1024]; + ERR_error_string_n(code, message, sizeof(message) - 1); + CLOG(ERROR, HTTP) << "OpenSSL error: " << message; + } + } +#endif + + if (isSslError) + { + throw OrthancException(ErrorCode_SslInitialization); + } + else + { + throw OrthancException(ErrorCode_HttpPortInUse, + " (port = " + boost::lexical_cast(port_) + ")"); + } + } + +#if ORTHANC_ENABLE_PUGIXML == 1 + for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it) + { + assert(it->second != NULL); + it->second->Start(); + } +#endif + + CLOG(WARNING, HTTP) << "HTTP server listening on port: " << GetPortNumber() + << " (HTTPS encryption is " + << (IsSslEnabled() ? "enabled" : "disabled") + << ", remote access is " + << (IsRemoteAccessAllowed() ? "" : "not ") + << "allowed)"; + } + } + + void HttpServer::Stop() + { + if (IsRunning()) + { + mg_stop(pimpl_->context_); + +#if ORTHANC_ENABLE_PUGIXML == 1 + for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it) + { + assert(it->second != NULL); + it->second->Stop(); + } +#endif + + pimpl_->context_ = NULL; + } + } + + + void HttpServer::ClearUsers() + { + Stop(); + registeredUsers_.clear(); + } + + + void HttpServer::RegisterUser(const char* username, + const char* password) + { + Stop(); + + std::string tag = std::string(username) + ":" + std::string(password); + std::string encoded; + Toolbox::EncodeBase64(encoded, tag); + registeredUsers_.insert(encoded); + } + + bool HttpServer::IsAuthenticationEnabled() const + { + return authentication_; + } + + void HttpServer::SetSslEnabled(bool enabled) + { + Stop(); + +#if ORTHANC_ENABLE_SSL == 0 + if (enabled) + { + throw OrthancException(ErrorCode_SslDisabled); + } + else + { + ssl_ = false; + } +#else + ssl_ = enabled; +#endif + } + + void HttpServer::SetSslVerifyPeers(bool enabled) + { + Stop(); + +#if ORTHANC_ENABLE_SSL == 0 + if (enabled) + { + throw OrthancException(ErrorCode_SslDisabled); + } + else + { + sslVerifyPeers_ = false; + } +#else + sslVerifyPeers_ = enabled; +#endif + } + + void HttpServer::SetSslMinimumVersion(unsigned int version) + { + Stop(); + sslMinimumVersion_ = version; + + std::string info; + + switch (version) + { + case 0: + info = "SSL2+SSL3+TLS1.0+TLS1.1+TLS1.2"; + break; + + case 1: + info = "SSL3+TLS1.0+TLS1.1+TLS1.2"; + break; + + case 2: + info = "TLS1.0+TLS1.1+TLS1.2"; + break; + + case 3: + info = "TLS1.1+TLS1.2"; + break; + + case 4: + info = "TLS1.2"; + break; + + default: + info = "Unknown value (" + boost::lexical_cast(version) + ")"; + break; + } + + CLOG(INFO, HTTP) << "Minimal accepted version of SSL/TLS protocol: " << info; + } + + void HttpServer::SetSslCiphers(const std::list& ciphers) + { + Stop(); + + sslHasCiphers_ = true; + sslCiphers_.clear(); + + for (std::list::const_iterator + it = ciphers.begin(); it != ciphers.end(); ++it) + { + if (it->empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Empty name for a cipher"); + } + + if (!sslCiphers_.empty()) + { + sslCiphers_ += ':'; + } + + sslCiphers_ += (*it); + } + + CLOG(INFO, HTTP) << "List of accepted SSL ciphers: " << sslCiphers_; + + if (sslCiphers_.empty()) + { + CLOG(WARNING, HTTP) << "No cipher is accepted for SSL"; + } + } + + void HttpServer::SetKeepAliveEnabled(bool enabled) + { + Stop(); + keepAlive_ = enabled; + CLOG(INFO, HTTP) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled"); + +#if ORTHANC_ENABLE_MONGOOSE == 1 + if (enabled) + { + CLOG(WARNING, HTTP) << "You should disable HTTP keep alive, as you are using Mongoose"; + } +#endif + } + + void HttpServer::SetKeepAliveTimeout(unsigned int timeout) + { + Stop(); + keepAliveTimeout_ = timeout; + CLOG(INFO, HTTP) << "HTTP keep alive Timeout is now " << keepAliveTimeout_ << " seconds"; + +#if ORTHANC_ENABLE_MONGOOSE == 1 + if (enabled) + { + CLOG(WARNING, HTTP) << "You should disable HTTP keep alive, as you are using Mongoose"; + } +#endif + } + + const std::string &HttpServer::GetSslCertificate() const + { + return certificate_; + } + + + void HttpServer::SetAuthenticationEnabled(bool enabled) + { + Stop(); + authentication_ = enabled; + } + + bool HttpServer::IsSslEnabled() const + { + return ssl_; + } + + void HttpServer::SetSslCertificate(const char* path) + { + Stop(); + certificate_ = path; + } + + bool HttpServer::IsRemoteAccessAllowed() const + { + return remoteAllowed_; + } + + void HttpServer::SetSslTrustedClientCertificates(const char* path) + { + Stop(); + trustedClientCertificates_ = path; + } + + bool HttpServer::IsKeepAliveEnabled() const + { + return keepAlive_; + } + + unsigned int HttpServer::GetKeepAliveTimeout() const + { + return keepAliveTimeout_; + } + + void HttpServer::SetRemoteAccessAllowed(bool allowed) + { + Stop(); + remoteAllowed_ = allowed; + } + + bool HttpServer::IsHttpCompressionEnabled() const + { + return httpCompression_;; + } + + void HttpServer::SetHttpCompressionEnabled(bool enabled) + { + Stop(); + httpCompression_ = enabled; + CLOG(WARNING, HTTP) << "HTTP compression is " << (enabled ? "enabled" : "disabled"); + } + + IIncomingHttpRequestFilter *HttpServer::GetIncomingHttpRequestFilter() const + { + return filter_; + } + + void HttpServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter) + { + Stop(); + filter_ = &filter; + } + + + void HttpServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter) + { + Stop(); + exceptionFormatter_ = &formatter; + } + + IHttpExceptionFormatter *HttpServer::GetExceptionFormatter() + { + return exceptionFormatter_; + } + + const std::string &HttpServer::GetRealm() const + { + return realm_; + } + + void HttpServer::SetRealm(const std::string &realm) + { + realm_ = realm; + } + + + bool HttpServer::IsValidBasicHttpAuthentication(const std::string& basic) const + { + return registeredUsers_.find(basic) != registeredUsers_.end(); + } + + + void HttpServer::Register(IHttpHandler& handler) + { + Stop(); + handler_ = &handler; + } + + bool HttpServer::HasHandler() const + { + return handler_ != NULL; + } + + + IHttpHandler& HttpServer::GetHandler() const + { + if (handler_ == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + return *handler_; + } + + + void HttpServer::SetThreadsCount(unsigned int threads) + { + if (threads == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Stop(); + threadsCount_ = threads; + + CLOG(INFO, HTTP) << "The embedded HTTP server will use " << threads << " threads"; + } + + unsigned int HttpServer::GetThreadsCount() const + { + return threadsCount_; + } + + + void HttpServer::SetTcpNoDelay(bool tcpNoDelay) + { + Stop(); + tcpNoDelay_ = tcpNoDelay; + CLOG(INFO, HTTP) << "TCP_NODELAY for the HTTP sockets is set to " + << (tcpNoDelay ? "true" : "false"); + } + + bool HttpServer::IsTcpNoDelay() const + { + return tcpNoDelay_; + } + + + void HttpServer::SetRequestTimeout(unsigned int seconds) + { + if (seconds == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Request timeout must be a stricly positive integer"); + } + + Stop(); + requestTimeout_ = seconds; + CLOG(INFO, HTTP) << "Request timeout in the HTTP server is set to " << seconds << " seconds"; + } + + unsigned int HttpServer::GetRequestTimeout() const + { + return requestTimeout_; + } + + +#if ORTHANC_ENABLE_PUGIXML == 1 + HttpServer::WebDavBuckets& HttpServer::GetWebDavBuckets() + { + return webDavBuckets_; + } +#endif + + +#if ORTHANC_ENABLE_PUGIXML == 1 + void HttpServer::Register(const std::vector& root, + IWebDavBucket* bucket) + { + std::unique_ptr protection(bucket); + + if (bucket == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + Stop(); + +#if CIVETWEB_HAS_WEBDAV_WRITING == 0 + if (webDavBuckets_.size() == 0) + { + CLOG(WARNING, HTTP) << "Your version of the Orthanc framework was compiled " + << "without support for writing into WebDAV collections"; + } +#endif + + const std::string s = Toolbox::FlattenUri(root); + + if (webDavBuckets_.find(s) != webDavBuckets_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Cannot register two WebDAV buckets at the same root: " + s); + } + else + { + CLOG(INFO, HTTP) << "Branching WebDAV bucket at: " << s; + webDavBuckets_[s] = protection.release(); + } + } +#endif +} diff --git a/OrthancFramework/Sources/HttpServer/HttpServer.h b/OrthancFramework/Sources/HttpServer/HttpServer.h new file mode 100644 index 0000000..f11a956 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpServer.h @@ -0,0 +1,230 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +// To have ORTHANC_ENABLE_xxx defined if using the shared library +#include "../OrthancFramework.h" + +#if !defined(ORTHANC_ENABLE_MONGOOSE) +# error Macro ORTHANC_ENABLE_MONGOOSE must be defined to include this file +#endif + +#if !defined(ORTHANC_ENABLE_CIVETWEB) +# error Macro ORTHANC_ENABLE_CIVETWEB must be defined to include this file +#endif + +#if (ORTHANC_ENABLE_MONGOOSE == 0 && \ + ORTHANC_ENABLE_CIVETWEB == 0) +# error Either ORTHANC_ENABLE_MONGOOSE or ORTHANC_ENABLE_CIVETWEB must be set to 1 +#endif + +#if !defined(ORTHANC_ENABLE_PUGIXML) +# error The macro ORTHANC_ENABLE_PUGIXML must be defined +#endif + +#if ORTHANC_ENABLE_PUGIXML == 1 +# include "IWebDavBucket.h" +#endif + + +#include "IIncomingHttpRequestFilter.h" + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class OrthancException; + + class IHttpExceptionFormatter : public boost::noncopyable + { + public: + virtual ~IHttpExceptionFormatter() + { + } + + virtual void Format(HttpOutput& output, + const OrthancException& exception, + HttpMethod method, + const char* uri) = 0; + }; + + + class ORTHANC_PUBLIC HttpServer : public boost::noncopyable + { + public: +#if ORTHANC_ENABLE_PUGIXML == 1 + typedef std::map WebDavBuckets; +#endif + + private: + // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom + struct PImpl; + boost::shared_ptr pimpl_; + + class ChunkStore; + class MultipartFormDataHandler; + + IHttpHandler *handler_; + + typedef std::set RegisteredUsers; + RegisteredUsers registeredUsers_; + + bool remoteAllowed_; + bool authentication_; + bool sslVerifyPeers_; + std::string trustedClientCertificates_; + bool ssl_; + std::string certificate_; + unsigned int sslMinimumVersion_; + bool sslHasCiphers_; + std::string sslCiphers_; + uint16_t port_; + IIncomingHttpRequestFilter* filter_; + bool keepAlive_; + unsigned int keepAliveTimeout_; + bool httpCompression_; + IHttpExceptionFormatter* exceptionFormatter_; + std::string realm_; + unsigned int threadsCount_; + bool tcpNoDelay_; + unsigned int requestTimeout_; // In seconds + +#if ORTHANC_ENABLE_PUGIXML == 1 + WebDavBuckets webDavBuckets_; +#endif + + bool IsRunning() const; + + public: + HttpServer(); + + ~HttpServer(); + + void SetPortNumber(uint16_t port); + + uint16_t GetPortNumber() const; + + void Start(); + + void Stop(); + + void ClearUsers(); + + void RegisterUser(const char* username, + const char* password); + + bool IsAuthenticationEnabled() const; + + void SetAuthenticationEnabled(bool enabled); + + bool IsSslEnabled() const; + + void SetSslEnabled(bool enabled); + + void SetSslVerifyPeers(bool enabled); + + // set the minimum accepted version of SSL/TLS protocol according to the CivetWeb table published here: + // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#ssl_protocol_version-0 + void SetSslMinimumVersion(unsigned int version); + + void SetSslCiphers(const std::list& ciphers); + + void SetSslTrustedClientCertificates(const char* path); + + bool IsKeepAliveEnabled() const; + + unsigned int GetKeepAliveTimeout() const; + + void SetKeepAliveEnabled(bool enabled); + + void SetKeepAliveTimeout(unsigned int timeout); + + const std::string& GetSslCertificate() const; + + void SetSslCertificate(const char* path); + + bool IsRemoteAccessAllowed() const; + + void SetRemoteAccessAllowed(bool allowed); + + bool IsHttpCompressionEnabled() const; + + void SetHttpCompressionEnabled(bool enabled); + + IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const; + + void SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter); + + bool IsValidBasicHttpAuthentication(const std::string& basic) const; + + void Register(IHttpHandler& handler); + + bool HasHandler() const; + + IHttpHandler& GetHandler() const; + + void SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter); + + IHttpExceptionFormatter* GetExceptionFormatter(); + + const std::string& GetRealm() const; + + void SetRealm(const std::string& realm); + + void SetThreadsCount(unsigned int threads); + + unsigned int GetThreadsCount() const; + + // New in Orthanc 1.5.2, not available for Mongoose + void SetTcpNoDelay(bool tcpNoDelay); + + bool IsTcpNoDelay() const; + + void SetRequestTimeout(unsigned int seconds); + + unsigned int GetRequestTimeout() const; + +#if ORTHANC_ENABLE_PUGIXML == 1 + WebDavBuckets& GetWebDavBuckets(); +#endif + +#if ORTHANC_ENABLE_PUGIXML == 1 + void Register(const std::vector& root, + IWebDavBucket* bucket); // Takes ownership +#endif + + ORTHANC_LOCAL + void ProcessMultipartFormData(const std::string& remoteIp, + const std::string& username, + const UriComponents& uri, + const std::map& headers, + const std::string& body, + const std::string& boundary); + }; +} diff --git a/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp new file mode 100644 index 0000000..d679be9 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp @@ -0,0 +1,262 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "HttpStreamTranscoder.h" + +#include "../OrthancException.h" +#include "../Compression/ZlibCompressor.h" + +#include // For memcpy() +#include + +#include + +namespace Orthanc +{ + void HttpStreamTranscoder::ReadSource(std::string& buffer) + { + if (source_.SetupHttpCompression(false, false) != HttpCompression_None) + { + throw OrthancException(ErrorCode_InternalError); + } + + uint64_t size = source_.GetContentLength(); + if (static_cast(static_cast(size)) != size) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + buffer.resize(static_cast(size)); + size_t offset = 0; + + while (source_.ReadNextChunk()) + { + size_t chunkSize = static_cast(source_.GetChunkSize()); + memcpy(&buffer[offset], source_.GetChunkContent(), chunkSize); + offset += chunkSize; + } + + if (offset != size) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + + HttpCompression HttpStreamTranscoder::SetupZlibCompression(bool deflateAllowed) + { + uint64_t size = source_.GetContentLength(); + + if (size == 0) + { + return HttpCompression_None; + } + + if (size < sizeof(uint64_t)) + { + throw OrthancException(ErrorCode_CorruptedFile); + } + + if (deflateAllowed) + { + bytesToSkip_ = sizeof(uint64_t); + + return HttpCompression_Deflate; + } + else + { + // TODO Use stream-based zlib decoding to reduce memory usage + std::string compressed; + ReadSource(compressed); + + uncompressed_.reset(new BufferHttpSender); + + ZlibCompressor compressor; + IBufferCompressor::Uncompress(uncompressed_->GetBuffer(), compressor, compressed); + + return HttpCompression_None; + } + } + + HttpStreamTranscoder::HttpStreamTranscoder(IHttpStreamAnswer &source, CompressionType compression) : + source_(source), + sourceCompression_(compression), + bytesToSkip_(0), + skipped_(0), + currentChunkOffset_(0), + ready_(false) + { + } + + + HttpCompression HttpStreamTranscoder::SetupHttpCompression(bool gzipAllowed, + bool deflateAllowed) + { + if (ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + ready_ = true; + + switch (sourceCompression_) + { + case CompressionType_None: + return HttpCompression_None; + + case CompressionType_ZlibWithSize: + return SetupZlibCompression(deflateAllowed); + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + bool HttpStreamTranscoder::HasContentFilename(std::string &filename) + { + return source_.HasContentFilename(filename); + } + + std::string HttpStreamTranscoder::GetContentType() + { + return source_.GetContentType(); + } + + + uint64_t HttpStreamTranscoder::GetContentLength() + { + if (!ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (uncompressed_.get() != NULL) + { + return uncompressed_->GetContentLength(); + } + else + { + uint64_t length = source_.GetContentLength(); + if (length < bytesToSkip_) + { + throw OrthancException(ErrorCode_InternalError); + } + + return length - bytesToSkip_; + } + } + + + bool HttpStreamTranscoder::ReadNextChunk() + { + if (!ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (uncompressed_.get() != NULL) + { + return uncompressed_->ReadNextChunk(); + } + + assert(skipped_ <= bytesToSkip_); + if (skipped_ == bytesToSkip_) + { + // We have already skipped the first bytes of the stream + currentChunkOffset_ = 0; + return source_.ReadNextChunk(); + } + + // This condition can only be true on the first call to "ReadNextChunk()" + for (;;) + { + assert(skipped_ < bytesToSkip_); + + bool ok = source_.ReadNextChunk(); + if (!ok) + { + throw OrthancException(ErrorCode_CorruptedFile); + } + + size_t remaining = static_cast(bytesToSkip_ - skipped_); + size_t s = source_.GetChunkSize(); + + if (s < remaining) + { + skipped_ += s; + } + else if (s == remaining) + { + // We have skipped enough bytes, but we must read a new chunk + currentChunkOffset_ = 0; + skipped_ = bytesToSkip_; + return source_.ReadNextChunk(); + } + else + { + // We have skipped enough bytes, and we have enough data in the current chunk + assert(s > remaining); + currentChunkOffset_ = remaining; + skipped_ = bytesToSkip_; + return true; + } + } + } + + + const char* HttpStreamTranscoder::GetChunkContent() + { + if (!ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (uncompressed_.get() != NULL) + { + return uncompressed_->GetChunkContent(); + } + else + { + return source_.GetChunkContent() + currentChunkOffset_; + } + } + + size_t HttpStreamTranscoder::GetChunkSize() + { + if (!ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (uncompressed_.get() != NULL) + { + return uncompressed_->GetChunkSize(); + } + else + { + return static_cast(source_.GetChunkSize() - currentChunkOffset_); + } + } +} diff --git a/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h new file mode 100644 index 0000000..c54e3d7 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h @@ -0,0 +1,71 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "BufferHttpSender.h" + +#include "../Compatibility.h" + +#include // For std::unique_ptr + +namespace Orthanc +{ + class ORTHANC_PUBLIC HttpStreamTranscoder : public IHttpStreamAnswer + { + private: + IHttpStreamAnswer& source_; + CompressionType sourceCompression_; + uint64_t bytesToSkip_; + uint64_t skipped_; + uint64_t currentChunkOffset_; + bool ready_; + + std::unique_ptr uncompressed_; + + void ReadSource(std::string& buffer); + + HttpCompression SetupZlibCompression(bool deflateAllowed); + + public: + HttpStreamTranscoder(IHttpStreamAnswer& source, + CompressionType compression); + + // This is the first method to be called + virtual HttpCompression SetupHttpCompression(bool gzipAllowed, + bool deflateAllowed) ORTHANC_OVERRIDE; + + virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE; + + virtual std::string GetContentType() ORTHANC_OVERRIDE; + + virtual uint64_t GetContentLength() ORTHANC_OVERRIDE; + + virtual bool ReadNextChunk() ORTHANC_OVERRIDE; + + virtual const char* GetChunkContent() ORTHANC_OVERRIDE; + + virtual size_t GetChunkSize() ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp b/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp new file mode 100644 index 0000000..b62ed81 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp @@ -0,0 +1,236 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "HttpToolbox.h" + +#include + +#if (ORTHANC_ENABLE_MONGOOSE == 1 || ORTHANC_ENABLE_CIVETWEB == 1) +# include "IHttpHandler.h" +#endif + + +namespace Orthanc +{ + static void SplitGETNameValue(HttpToolbox::GetArguments& result, + const char* start, + const char* end) + { + std::string name, value; + + const char* equal = strchr(start, '='); + if (equal == NULL || equal >= end) + { + name = std::string(start, end - start); + //value = ""; + } + else + { + name = std::string(start, equal - start); + value = std::string(equal + 1, end); + } + +#if 0 + // These calls were present in Orthanc <= 1.9.7, but should not be + // used because mongoose/civetweb already implement URL decoding + // (cf. "mg_url_decode()") + Toolbox::UrlDecode(name); + Toolbox::UrlDecode(value); +#endif + + result.push_back(std::make_pair(name, value)); + } + + + void HttpToolbox::ParseGetArguments(GetArguments& result, + const char* query) + { + const char* pos = query; + + while (pos != NULL) + { + const char* ampersand = strchr(pos, '&'); + if (ampersand) + { + SplitGETNameValue(result, pos, ampersand); + pos = ampersand + 1; + } + else + { + // No more ampersand, this is the last argument + SplitGETNameValue(result, pos, pos + strlen(pos)); + pos = NULL; + } + } + } + + + void HttpToolbox::ParseGetQuery(UriComponents& uri, + GetArguments& getArguments, + const char* query) + { + const char *questionMark = ::strchr(query, '?'); + if (questionMark == NULL) + { + // No question mark in the string + Toolbox::SplitUriComponents(uri, query); + getArguments.clear(); + } + else + { + Toolbox::SplitUriComponents(uri, std::string(query, questionMark)); + HttpToolbox::ParseGetArguments(getArguments, questionMark + 1); + } + } + + + std::string HttpToolbox::GetArgument(const Arguments& getArguments, + const std::string& name, + const std::string& defaultValue) + { + Arguments::const_iterator it = getArguments.find(name); + if (it == getArguments.end()) + { + return defaultValue; + } + else + { + return it->second; + } + } + + + std::string HttpToolbox::GetArgument(const GetArguments& getArguments, + const std::string& name, + const std::string& defaultValue) + { + for (size_t i = 0; i < getArguments.size(); i++) + { + if (getArguments[i].first == name) + { + return getArguments[i].second; + } + } + + return defaultValue; + } + + + + void HttpToolbox::ParseCookies(Arguments& result, + const Arguments& httpHeaders) + { + result.clear(); + + Arguments::const_iterator it = httpHeaders.find("cookie"); + if (it != httpHeaders.end()) + { + const std::string& cookies = it->second; + + size_t pos = 0; + while (pos != std::string::npos) + { + size_t nextSemicolon = cookies.find(";", pos); + std::string cookie; + + if (nextSemicolon == std::string::npos) + { + cookie = cookies.substr(pos); + pos = std::string::npos; + } + else + { + cookie = cookies.substr(pos, nextSemicolon - pos); + pos = nextSemicolon + 1; + } + + size_t equal = cookie.find("="); + if (equal != std::string::npos) + { + std::string name = Toolbox::StripSpaces(cookie.substr(0, equal)); + std::string value = Toolbox::StripSpaces(cookie.substr(equal + 1)); + result[name] = value; + } + } + } + } + + + void HttpToolbox::CompileGetArguments(Arguments& compiled, + const GetArguments& source) + { + compiled.clear(); + + for (size_t i = 0; i < source.size(); i++) + { + compiled[source[i].first] = source[i].second; + } + } + + + +#if (ORTHANC_ENABLE_MONGOOSE == 1 || ORTHANC_ENABLE_CIVETWEB == 1) + bool HttpToolbox::SimpleGet(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const Arguments& httpHeaders) + { + return (IHttpHandler::SimpleGet(result, NULL, handler, origin, uri, httpHeaders) == HttpStatus_200_Ok); + } + + bool HttpToolbox::SimplePost(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const Arguments& httpHeaders) + { + return (IHttpHandler::SimplePost(result, NULL, handler, origin, uri, + bodyData, bodySize, httpHeaders) == HttpStatus_200_Ok); + } + + bool HttpToolbox::SimplePut(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const Arguments& httpHeaders) + { + return (IHttpHandler::SimplePut(result, NULL, handler, origin, uri, + bodyData, bodySize, httpHeaders) == HttpStatus_200_Ok); + } + + bool HttpToolbox::SimpleDelete(IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const Arguments& httpHeaders) + { + return (IHttpHandler::SimpleDelete(NULL, handler, origin, uri, httpHeaders) == HttpStatus_200_Ok); + } +#endif +} diff --git a/OrthancFramework/Sources/HttpServer/HttpToolbox.h b/OrthancFramework/Sources/HttpServer/HttpToolbox.h new file mode 100644 index 0000000..2eb835d --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.h @@ -0,0 +1,95 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Compatibility.h" +#include "../OrthancFramework.h" +#include "../Toolbox.h" + +#include +#include +#include + +namespace Orthanc +{ + class IHttpHandler; + + class ORTHANC_PUBLIC HttpToolbox : public boost::noncopyable + { + public: + typedef std::map Arguments; + typedef std::vector< std::pair > GetArguments; + + static void ParseGetArguments(GetArguments& result, + const char* query); + + static void ParseGetQuery(UriComponents& uri, + GetArguments& getArguments, + const char* query); + + static std::string GetArgument(const Arguments& getArguments, + const std::string& name, + const std::string& defaultValue); + + static std::string GetArgument(const GetArguments& getArguments, + const std::string& name, + const std::string& defaultValue); + + static void ParseCookies(Arguments& result, + const Arguments& httpHeaders); + + static void CompileGetArguments(Arguments& compiled, + const GetArguments& source); + +#if (ORTHANC_ENABLE_MONGOOSE == 1 || ORTHANC_ENABLE_CIVETWEB == 1) + ORTHANC_DEPRECATED(static bool SimpleGet(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const Arguments& httpHeaders)); + + ORTHANC_DEPRECATED(static bool SimplePost(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const Arguments& httpHeaders)); + + ORTHANC_DEPRECATED(static bool SimplePut(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const Arguments& httpHeaders)); + + ORTHANC_DEPRECATED(static bool SimpleDelete(IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const Arguments& httpHeaders)); +#endif + }; +} diff --git a/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp new file mode 100644 index 0000000..a0a719e --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp @@ -0,0 +1,165 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "IHttpHandler.h" + +#include "HttpOutput.h" +#include "HttpToolbox.h" +#include "StringHttpOutput.h" + +static const char* LOCALHOST = "127.0.0.1"; + + +namespace Orthanc +{ + HttpStatus IHttpHandler::SimpleGet(std::string& answerBody, + HttpToolbox::Arguments* answerHeaders, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const HttpToolbox::Arguments& httpHeaders) + { + UriComponents curi; + HttpToolbox::GetArguments getArguments; + HttpToolbox::ParseGetQuery(curi, getArguments, uri.c_str()); + + StringHttpOutput stream; + HttpOutput http(stream, false /* assume no keep-alive */, 0); + + if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, + httpHeaders, getArguments, NULL /* no body for GET */, 0)) + { + if (stream.GetStatus() == HttpStatus_200_Ok) + { + stream.GetBody(answerBody); + } + + if (answerHeaders != NULL) + { + stream.GetHeaders(*answerHeaders, true /* convert key to lower case */); + } + + return stream.GetStatus(); + } + else + { + return HttpStatus_404_NotFound; + } + } + + + static HttpStatus SimplePostOrPut(std::string& answerBody, + HttpToolbox::Arguments* answerHeaders, + IHttpHandler& handler, + RequestOrigin origin, + HttpMethod method, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const HttpToolbox::Arguments& httpHeaders) + { + HttpToolbox::GetArguments getArguments; // No GET argument for POST/PUT + + UriComponents curi; + Toolbox::SplitUriComponents(curi, uri); + + StringHttpOutput stream; + HttpOutput http(stream, false /* assume no keep-alive */, 0); + + if (handler.Handle(http, origin, LOCALHOST, "", method, curi, + httpHeaders, getArguments, bodyData, bodySize)) + { + stream.GetBody(answerBody); + + if (answerHeaders != NULL) + { + stream.GetHeaders(*answerHeaders, true /* convert key to lower case */); + } + + return stream.GetStatus(); + } + else + { + return HttpStatus_404_NotFound; + } + } + + + HttpStatus IHttpHandler::SimplePost(std::string& answerBody, + HttpToolbox::Arguments* answerHeaders, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const HttpToolbox::Arguments& httpHeaders) + { + return SimplePostOrPut(answerBody, answerHeaders, handler, origin, HttpMethod_Post, uri, bodyData, bodySize, httpHeaders); + } + + + HttpStatus IHttpHandler::SimplePut(std::string& answerBody, + HttpToolbox::Arguments* answerHeaders, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const HttpToolbox::Arguments& httpHeaders) + { + return SimplePostOrPut(answerBody, answerHeaders, handler, origin, HttpMethod_Put, uri, bodyData, bodySize, httpHeaders); + } + + + HttpStatus IHttpHandler::SimpleDelete(HttpToolbox::Arguments* answerHeaders, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const HttpToolbox::Arguments& httpHeaders) + { + UriComponents curi; + Toolbox::SplitUriComponents(curi, uri); + + HttpToolbox::GetArguments getArguments; // No GET argument for DELETE + + StringHttpOutput stream; + HttpOutput http(stream, false /* assume no keep-alive */, 0); + + if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, + httpHeaders, getArguments, NULL /* no body for DELETE */, 0)) + { + if (answerHeaders != NULL) + { + stream.GetHeaders(*answerHeaders, true /* convert key to lower case */); + } + + return stream.GetStatus(); + } + else + { + return HttpStatus_404_NotFound; + } + } +} diff --git a/OrthancFramework/Sources/HttpServer/IHttpHandler.h b/OrthancFramework/Sources/HttpServer/IHttpHandler.h new file mode 100644 index 0000000..54432a4 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.h @@ -0,0 +1,125 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if ORTHANC_SANDBOXED == 1 +# error This file cannot be used in sandboxed environments +#endif + +#include "../Compatibility.h" +#include "../Toolbox.h" +#include "HttpOutput.h" +#include "HttpToolbox.h" + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class IHttpHandler : public boost::noncopyable + { + public: + class IChunkedRequestReader : public boost::noncopyable + { + public: + virtual ~IChunkedRequestReader() + { + } + + virtual void AddBodyChunk(const void* data, + size_t size) = 0; + + virtual void Execute(HttpOutput& output) = 0; + }; + + + virtual ~IHttpHandler() + { + } + + /** + * This function allows one to deal with chunked transfers (new in + * Orthanc 1.5.7). It is only called if "method" is POST or PUT. + **/ + virtual bool CreateChunkedRequestReader(std::unique_ptr& target, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers) = 0; + + virtual bool Handle(HttpOutput& output, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers, + const HttpToolbox::GetArguments& getArguments, + const void* bodyData, + size_t bodySize) = 0; + + + /** + * In the static functions below, "answerHeaders" can be set to + * NULL if the caller has no interest in HTTP headers of the + * answer (this avoids some computation). + **/ + static HttpStatus SimpleGet(std::string& answerBody /* out */, + HttpToolbox::Arguments* answerHeaders /* out */, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const HttpToolbox::Arguments& httpHeaders); + + static HttpStatus SimplePost(std::string& answerBody /* out */, + HttpToolbox::Arguments* answerHeaders /* out */, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const HttpToolbox::Arguments& httpHeaders); + + static HttpStatus SimplePut(std::string& answerBody /* out */, + HttpToolbox::Arguments* answerHeaders /* out */, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const void* bodyData, + size_t bodySize, + const HttpToolbox::Arguments& httpHeaders); + + static HttpStatus SimpleDelete(HttpToolbox::Arguments* answerHeaders /* out */, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const HttpToolbox::Arguments& httpHeaders); + }; +} diff --git a/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h b/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h new file mode 100644 index 0000000..97714bd --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h @@ -0,0 +1,49 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Enumerations.h" + +#include +#include + +namespace Orthanc +{ + class IHttpOutputStream : public boost::noncopyable + { + public: + virtual ~IHttpOutputStream() + { + } + + virtual void OnHttpStatusReceived(HttpStatus status) = 0; + + virtual void Send(bool isHeader, const void* buffer, size_t length) = 0; + + // Disable HTTP keep alive for this single HTTP connection. Must + // be called before sending the "HTTP/1.1 200 OK" header. + virtual void DisableKeepAlive() = 0; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h b/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h new file mode 100644 index 0000000..5fb29e2 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h @@ -0,0 +1,58 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Enumerations.h" + +#include +#include +#include + +namespace Orthanc +{ + class IHttpStreamAnswer : public boost::noncopyable + { + public: + virtual ~IHttpStreamAnswer() + { + } + + // This is the first method to be called + virtual HttpCompression SetupHttpCompression(bool gzipAllowed, + bool deflateAllowed) = 0; + + virtual bool HasContentFilename(std::string& filename) = 0; + + virtual std::string GetContentType() = 0; + + virtual uint64_t GetContentLength() = 0; + + virtual bool ReadNextChunk() = 0; + + virtual const char* GetChunkContent() = 0; + + virtual size_t GetChunkSize() = 0; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h b/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h new file mode 100644 index 0000000..67cb184 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h @@ -0,0 +1,48 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "HttpToolbox.h" + +namespace Orthanc +{ + class IIncomingHttpRequestFilter : public boost::noncopyable + { + public: + virtual ~IIncomingHttpRequestFilter() + { + } + + // New in Orthanc 1.8.1 + virtual bool IsValidBearerToken(const std::string& token) = 0; + + virtual bool IsAllowed(HttpMethod method, + const char* uri, + const char* ip, + const char* username, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) = 0; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp new file mode 100644 index 0000000..fb4cccf --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp @@ -0,0 +1,351 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "IWebDavBucket.h" + +#include "HttpOutput.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + + +static boost::posix_time::ptime GetNow() +{ + return boost::posix_time::second_clock::universal_time(); +} + + +static std::string AddTrailingSlash(const std::string& s) +{ + if (s.empty() || + s[s.size() - 1] != '/') + { + return s + '/'; + } + else + { + return s; + } +} + + +namespace Orthanc +{ + IWebDavBucket::Resource::Resource(const std::string& displayName) : + displayName_(displayName), + hasModificationTime_(false), + creationTime_(GetNow()), + modificationTime_(GetNow()) + { + if (displayName.empty() || + displayName.find('/') != std::string::npos || + displayName.find('\\') != std::string::npos || + displayName.find('\0') != std::string::npos) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad resource name for WebDAV: " + displayName); + } + } + + + void IWebDavBucket::Resource::SetCreationTime(const boost::posix_time::ptime& t) + { + if (t.is_special()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time"); + } + else + { + creationTime_ = t; + + if (!hasModificationTime_) + { + modificationTime_ = t; + } + } + } + + + void IWebDavBucket::Resource::SetModificationTime(const boost::posix_time::ptime& t) + { + if (t.is_special()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time"); + } + else + { + modificationTime_ = t; + hasModificationTime_ = true; + } + } + + + static void FormatInternal(pugi::xml_node& node, + const std::string& href, + const std::string& displayName, + const boost::posix_time::ptime& creationTime, + const boost::posix_time::ptime& modificationTime) + { + node.set_name("D:response"); + + node.append_child("D:href").append_child(pugi::node_pcdata).set_value(href.c_str()); + + pugi::xml_node propstat = node.append_child("D:propstat"); + + static const HttpStatus status = HttpStatus_200_Ok; + std::string s = ("HTTP/1.1 " + boost::lexical_cast(status) + " " + + std::string(EnumerationToString(status))); + propstat.append_child("D:status").append_child(pugi::node_pcdata).set_value(s.c_str()); + + pugi::xml_node prop = propstat.append_child("D:prop"); + prop.append_child("D:displayname").append_child(pugi::node_pcdata).set_value(displayName.c_str()); + + // IMPORTANT: Adding the "Z" suffix is mandatory on Windows >= 7 (it indicates UTC) + assert(!creationTime.is_special()); + s = boost::posix_time::to_iso_extended_string(creationTime) + "Z"; + prop.append_child("D:creationdate").append_child(pugi::node_pcdata).set_value(s.c_str()); + + assert(!modificationTime.is_special()); + s = boost::posix_time::to_iso_extended_string(modificationTime) + "Z"; + prop.append_child("D:getlastmodified").append_child(pugi::node_pcdata).set_value(s.c_str()); + +#if 0 + // Maybe used by davfs2 + prop.append_child("D:quota-available-bytes"); + prop.append_child("D:quota-used-bytes"); +#endif + +#if 0 + prop.append_child("D:lockdiscovery"); + pugi::xml_node lock = prop.append_child("D:supportedlock"); + + pugi::xml_node lockentry = lock.append_child("D:lockentry"); + lockentry.append_child("D:lockscope").append_child("D:exclusive"); + lockentry.append_child("D:locktype").append_child("D:write"); + + lockentry = lock.append_child("D:lockentry"); + lockentry.append_child("D:lockscope").append_child("D:shared"); + lockentry.append_child("D:locktype").append_child("D:write"); +#endif + } + + + IWebDavBucket::File::File(const std::string& displayName) : + Resource(displayName), + contentLength_(0), + mime_(MimeType_Binary) + { + } + + + void IWebDavBucket::File::Format(pugi::xml_node& node, + const std::string& parentPath) const + { + std::string href; + Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName()); + FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime()); + + pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop"); + prop.append_child("D:resourcetype"); + + std::string s = boost::lexical_cast(contentLength_); + prop.append_child("D:getcontentlength").append_child(pugi::node_pcdata).set_value(s.c_str()); + + s = EnumerationToString(mime_); + prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value(s.c_str()); + } + + + void IWebDavBucket::Folder::Format(pugi::xml_node& node, + const std::string& parentPath) const + { + std::string href; + Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName()); + FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime()); + + pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop"); + prop.append_child("D:resourcetype").append_child("D:collection"); + + //prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value("httpd/unix-directory"); + } + + + IWebDavBucket::Collection::~Collection() + { + for (std::list::iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(*it != NULL); + delete(*it); + } + } + + + void IWebDavBucket::Collection::AddResource(Resource* resource) // Takes ownership + { + if (resource == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + resources_.push_back(resource); + } + } + + + void IWebDavBucket::Collection::ListDisplayNames(std::set& target) + { + for (std::list::iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(*it != NULL); + target.insert((*it)->GetDisplayName()); + } + } + + + void IWebDavBucket::Collection::Format(std::string& target, + const std::string& parentPath) const + { + pugi::xml_document doc; + + pugi::xml_node root = doc.append_child("D:multistatus"); + root.append_attribute("xmlns:D").set_value("DAV:"); + + { + pugi::xml_node self = root.append_child(); + + std::vector tokens; + Toolbox::SplitUriComponents(tokens, parentPath); + + std::string folder; + if (!tokens.empty()) + { + folder = tokens.back(); + } + + std::string href; + Toolbox::UriEncode(href, Toolbox::FlattenUri(tokens) + "/"); + + boost::posix_time::ptime now = GetNow(); + FormatInternal(self, href, folder, now, now); + + pugi::xml_node prop = self.first_element_by_path("D:propstat/D:prop"); + prop.append_child("D:resourcetype").append_child("D:collection"); + } + + for (std::list::const_iterator + it = resources_.begin(); it != resources_.end(); ++it) + { + assert(*it != NULL); + pugi::xml_node n = root.append_child(); + (*it)->Format(n, parentPath); + } + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("UTF-8"); + + Toolbox::XmlToString(target, doc); + } + + + void IWebDavBucket::AnswerFakedProppatch(HttpOutput& output, + const std::string& uri) + { + /** + * This is a fake implementation. The goal is to make happy the + * WebDAV clients that set properties (such as Windows >= 7). + **/ + + pugi::xml_document doc; + + pugi::xml_node root = doc.append_child("D:multistatus"); + root.append_attribute("xmlns:D").set_value("DAV:"); + + pugi::xml_node response = root.append_child("D:response"); + response.append_child("D:href").append_child(pugi::node_pcdata).set_value(uri.c_str()); + + response.append_child("D:propstat"); + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("UTF-8"); + + std::string s; + Toolbox::XmlToString(s, doc); + + output.AddHeader("Content-Type", "application/xml"); + output.SendStatus(HttpStatus_207_MultiStatus, s); + } + + + void IWebDavBucket::AnswerFakedLock(HttpOutput& output, + const std::string& uri) + { + /** + * This is a fake implementation. No lock is actually + * created. The goal is to make happy the WebDAV clients + * that use locking (such as Windows >= 7). + **/ + + pugi::xml_document doc; + + pugi::xml_node root = doc.append_child("D:prop"); + root.append_attribute("xmlns:D").set_value("DAV:"); + + pugi::xml_node activelock = root.append_child("D:lockdiscovery").append_child("D:activelock"); + activelock.append_child("D:locktype").append_child("D:write"); + activelock.append_child("D:lockscope").append_child("D:exclusive"); + activelock.append_child("D:depth").append_child(pugi::node_pcdata).set_value("0"); + activelock.append_child("D:timeout").append_child(pugi::node_pcdata).set_value("Second-3599"); + + activelock.append_child("D:lockroot").append_child("D:href") + .append_child(pugi::node_pcdata).set_value(uri.c_str()); + activelock.append_child("D:owner"); + + std::string token = Toolbox::GenerateUuid(); + boost::erase_all(token, "-"); + token = "opaquelocktoken:0x" + token; + + activelock.append_child("D:locktoken").append_child("D:href"). + append_child(pugi::node_pcdata).set_value(token.c_str()); + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("UTF-8"); + + std::string s; + Toolbox::XmlToString(s, doc); + + output.AddHeader("Lock-Token", token); // Necessary for davfs2 + output.AddHeader("Content-Type", "application/xml"); + output.SendStatus(HttpStatus_201_Created, s); + } + + + void IWebDavBucket::AnswerFakedUnlock(HttpOutput& output) + { + output.SendStatus(HttpStatus_204_NoContent); + } +} diff --git a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h new file mode 100644 index 0000000..fb1ef1b --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h @@ -0,0 +1,198 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_PUGIXML) +# error The macro ORTHANC_ENABLE_PUGIXML must be defined +#endif + +#if ORTHANC_ENABLE_PUGIXML != 1 +# error XML support is required to use this file +#endif + +#include "../Compatibility.h" +#include "../Enumerations.h" + +#include +#include +#include + +#include +#include +#include + +namespace Orthanc +{ + class HttpOutput; + + class IWebDavBucket : public boost::noncopyable + { + public: + class Resource : public boost::noncopyable + { + private: + std::string displayName_; + bool hasModificationTime_; + boost::posix_time::ptime creationTime_; + boost::posix_time::ptime modificationTime_; + + public: + explicit Resource(const std::string& displayName); + + virtual ~Resource() + { + } + + void SetCreationTime(const boost::posix_time::ptime& t); + + void SetModificationTime(const boost::posix_time::ptime& t); + + const std::string& GetDisplayName() const + { + return displayName_; + } + + const boost::posix_time::ptime& GetCreationTime() const + { + return creationTime_; + } + + const boost::posix_time::ptime& GetModificationTime() const + { + return modificationTime_; + } + + virtual void Format(pugi::xml_node& node, + const std::string& parentPath) const = 0; + }; + + + class File : public Resource + { + private: + uint64_t contentLength_; + MimeType mime_; + + public: + explicit File(const std::string& displayName); + + void SetContentLength(uint64_t contentLength) + { + contentLength_ = contentLength; + } + + void SetMimeType(MimeType mime) + { + mime_ = mime; + } + + uint64_t GetContentLength() const + { + return contentLength_; + } + + MimeType GetMimeType() const + { + return mime_; + } + + virtual void Format(pugi::xml_node& node, + const std::string& parentPath) const ORTHANC_OVERRIDE; + }; + + + class Folder : public Resource + { + public: + explicit Folder(const std::string& displayName) : + Resource(displayName) + { + } + + virtual void Format(pugi::xml_node& node, + const std::string& parentPath) const ORTHANC_OVERRIDE; + }; + + + class Collection : public boost::noncopyable + { + private: + std::list resources_; + + public: + ~Collection(); + + size_t GetSize() const + { + return resources_.size(); + } + + void ListDisplayNames(std::set& target); + + void AddResource(Resource* resource); // Takes ownership + + void Format(std::string& target, + const std::string& parentPath) const; + }; + + + virtual ~IWebDavBucket() + { + } + + virtual bool IsExistingFolder(const std::vector& path) = 0; + + virtual bool ListCollection(Collection& collection, + const std::vector& path) = 0; + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, + const std::vector& path) = 0; + + // "false" returns indicate a read-only target + virtual bool StoreFile(const std::string& content, + const std::vector& path) = 0; + + virtual bool CreateFolder(const std::vector& path) = 0; + + virtual bool DeleteItem(const std::vector& path) = 0; + + virtual void Start() = 0; + + // During the shutdown of the Web server, give a chance to the + // bucket to end its pending operations + virtual void Stop() = 0; + + + static void AnswerFakedProppatch(HttpOutput& output, + const std::string& uri); + + static void AnswerFakedLock(HttpOutput& output, + const std::string& uri); + + static void AnswerFakedUnlock(HttpOutput& output); + }; +} diff --git a/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp new file mode 100644 index 0000000..e79f00b --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp @@ -0,0 +1,469 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "MultipartStreamReader.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include +#include + +#if defined(_MSC_VER) +# include // Definition of ssize_t +#endif + +namespace Orthanc +{ + static void ParseHeaders(MultipartStreamReader::HttpHeaders& headers, + const char* start, + const char* end /* exclusive */) + { + assert(start <= end); + std::string tmp(start, end - start); + + std::vector lines; + Toolbox::TokenizeString(lines, tmp, '\n'); + + headers.clear(); + + for (size_t i = 0; i < lines.size(); i++) + { + size_t separator = lines[i].find(':'); + if (separator != std::string::npos) + { + std::string key = Toolbox::StripSpaces(lines[i].substr(0, separator)); + std::string value = Toolbox::StripSpaces(lines[i].substr(separator + 1)); + + Toolbox::ToLowerCase(key); + headers[key] = value; + } + } + } + + + static bool LookupHeaderSizeValue(size_t& target, + const MultipartStreamReader::HttpHeaders& headers, + const std::string& key) + { + MultipartStreamReader::HttpHeaders::const_iterator it = headers.find(key); + if (it == headers.end()) + { + return false; + } + else + { + int64_t value; + + try + { + value = boost::lexical_cast(it->second); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + target = static_cast(value); + return true; + } + } + } + + + void MultipartStreamReader::ParseBlock(const void* data, + size_t size) + { + if (handler_ == NULL || + state_ == State_Done || + size == 0) + { + return; + } + else + { + const char* current = reinterpret_cast(data); + const char* corpusEnd = current + size; + + if (state_ == State_UnusedArea) + { + /** + * "Before the first boundary is an area that is ignored by + * MIME-compliant clients. This area is generally used to put + * a message to users of old non-MIME clients." + * https://en.wikipedia.org/wiki/MIME#Multipart_messages + **/ + + if (boundaryMatcher_.Apply(current, corpusEnd)) + { + current = boundaryMatcher_.GetMatchBegin(); + state_ = State_Content; + } + else + { + // We have not seen the end of the unused area yet + assert(current <= corpusEnd); + buffer_.AddChunk(current, corpusEnd - current); + return; + } + } + + for (;;) + { + assert(current <= corpusEnd); + + size_t patternSize = boundaryMatcher_.GetPattern().size(); + size_t remainingSize = corpusEnd - current; + if (remainingSize < patternSize + 2) + { + break; // Not enough data available + } + + std::string boundary(current, current + patternSize + 2); + if (boundary == boundaryMatcher_.GetPattern() + "--") + { + state_ = State_Done; + return; + } + + if (boundary != boundaryMatcher_.GetPattern() + "\r\n") + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Garbage between two items in a multipart stream"); + } + + const char* start = current + patternSize + 2; + + if (!headersMatcher_.Apply(start, corpusEnd)) + { + break; // Not enough data available + } + + HttpHeaders headers; + ParseHeaders(headers, start, headersMatcher_.GetMatchBegin()); + + size_t contentLength = 0; + if (!LookupHeaderSizeValue(contentLength, headers, "content-length")) + { + if (boundaryMatcher_.Apply(headersMatcher_.GetMatchEnd(), corpusEnd)) + { + assert(headersMatcher_.GetMatchEnd() <= boundaryMatcher_.GetMatchBegin()); + size_t d = boundaryMatcher_.GetMatchBegin() - headersMatcher_.GetMatchEnd(); + if (d <= 1) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + else + { + contentLength = d - 2; + } + } + else + { + break; // Not enough data available to have a full part + } + } + + // "static_cast<>" to avoid warning about signed vs. unsigned comparison + assert(headersMatcher_.GetMatchEnd() <= corpusEnd); + if (contentLength + 2 > static_cast(corpusEnd - headersMatcher_.GetMatchEnd())) + { + break; // Not enough data available to have a full part + } + + const char* p = headersMatcher_.GetMatchEnd() + contentLength; + if (p[0] != '\r' || + p[1] != '\n') + { + throw OrthancException(ErrorCode_NetworkProtocol, + "No endline at the end of a part"); + } + + handler_->HandlePart(headers, headersMatcher_.GetMatchEnd(), contentLength); + current = headersMatcher_.GetMatchEnd() + contentLength + 2; + } + + if (current != corpusEnd) + { + assert(current < corpusEnd); + buffer_.AddChunk(current, corpusEnd - current); + } + } + } + + + void MultipartStreamReader::ParseStream() + { + if (handler_ == NULL || + state_ == State_Done) + { + return; + } + else + { + std::string corpus; + buffer_.Flatten(corpus); + + if (!corpus.empty()) + { + ParseBlock(corpus.c_str(), corpus.size()); + } + } + } + + + MultipartStreamReader::MultipartStreamReader(const std::string& boundary) : + state_(State_UnusedArea), + handler_(NULL), + headersMatcher_("\r\n\r\n"), + boundaryMatcher_("--" + boundary), + blockSize_(10 * 1024 * 1024) + { + } + + + void MultipartStreamReader::SetBlockSize(size_t size) + { + if (size == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + blockSize_ = size; + } + } + + size_t MultipartStreamReader::GetBlockSize() const + { + return blockSize_; + } + + void MultipartStreamReader::SetHandler(MultipartStreamReader::IHandler &handler) + { + handler_ = &handler; + } + + + void MultipartStreamReader::AddChunk(const void* chunk, + size_t size) + { + if (state_ != State_Done && + size != 0) + { + size_t oldSize = buffer_.GetNumBytes(); + if (oldSize == 0) + { + /** + * Optimization in Orthanc 1.9.3: Directly parse the input + * buffer instead of going through the ChunkedBuffer if the + * latter is still empty. This notably avoids one memcpy() in + * STOW-RS server if chunked transfers is disabled. + **/ + ParseBlock(chunk, size); + } + else + { + buffer_.AddChunk(chunk, size); + + if (oldSize / blockSize_ != buffer_.GetNumBytes() / blockSize_) + { + ParseStream(); + } + } + } + } + + + void MultipartStreamReader::AddChunk(const std::string& chunk) + { + if (!chunk.empty()) + { + AddChunk(chunk.c_str(), chunk.size()); + } + } + + + void MultipartStreamReader::CloseStream() + { + if (buffer_.GetNumBytes() != 0) + { + ParseStream(); + } + } + + + bool MultipartStreamReader::GetMainContentType(std::string& contentType, + const HttpHeaders& headers) + { + HttpHeaders::const_iterator it = headers.find("content-type"); + + if (it == headers.end()) + { + return false; + } + else + { + contentType = it->second; + return true; + } + } + + + static void RemoveSurroundingQuotes(std::string& value) + { + if (value.size() >= 2 && + value[0] == '"' && + value[value.size() - 1] == '"') + { + value = value.substr(1, value.size() - 2); + } + } + + + bool MultipartStreamReader::ParseMultipartContentType(std::string& contentType, + std::string& subType, + std::string& boundary, + const std::string& contentTypeHeader) + { + std::vector tokens; + Toolbox::TokenizeString(tokens, contentTypeHeader, ';'); + + if (tokens.empty()) + { + return false; + } + + contentType = Toolbox::StripSpaces(tokens[0]); + Toolbox::ToLowerCase(contentType); + + if (contentType.empty()) + { + return false; + } + + bool valid = false; + subType.clear(); + + for (size_t i = 1; i < tokens.size(); i++) + { + std::vector items; + Toolbox::TokenizeString(items, tokens[i], '='); + + if (items.size() == 2) + { + if (boost::iequals("boundary", Toolbox::StripSpaces(items[0]))) + { + boundary = Toolbox::StripSpaces(items[1]); + + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=190 + RemoveSurroundingQuotes(boundary); + + valid = !boundary.empty(); + } + else if (boost::iequals("type", Toolbox::StripSpaces(items[0]))) + { + subType = Toolbox::StripSpaces(items[1]); + Toolbox::ToLowerCase(subType); + + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=54 + // https://tools.ietf.org/html/rfc7231#section-3.1.1.1 + RemoveSurroundingQuotes(subType); + } + } + } + + return valid; + } + + + bool MultipartStreamReader::ParseHeaderArguments(std::string& main, + std::map& arguments, + const std::string& header) + { + std::vector tokens; + Toolbox::TokenizeString(tokens, header, ';'); + + if (tokens.empty()) + { + return false; + } + + main = Toolbox::StripSpaces(tokens[0]); + Toolbox::ToLowerCase(main); + if (main.empty()) + { + return false; + } + + arguments.clear(); + + for (size_t i = 1; i < tokens.size(); i++) + { + std::vector items; + Toolbox::TokenizeString(items, tokens[i], '='); + + if (items.size() > 2) + { + return false; + } + else if (!items.empty()) + { + std::string key = Toolbox::StripSpaces(items[0]); + Toolbox::ToLowerCase(key); + + if (arguments.find(key) != arguments.end()) + { + LOG(ERROR) << "The same argument was provided twice in an HTTP header: \"" + << key << "\" in \"" << header << "\""; + return false; + } + else if (!key.empty()) + { + if (items.size() == 1) + { + arguments[key] = ""; + } + else + { + assert(items.size() == 2); + std::string value = Toolbox::StripSpaces(items[1]); + RemoveSurroundingQuotes(value); + arguments[key] = value; + } + } + } + } + + return true; + } +} diff --git a/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h new file mode 100644 index 0000000..49db5f4 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h @@ -0,0 +1,99 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "CStringMatcher.h" +#include "../ChunkedBuffer.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC MultipartStreamReader : public boost::noncopyable + { + public: + typedef std::map HttpHeaders; + + class IHandler : public boost::noncopyable + { + public: + virtual ~IHandler() + { + } + + virtual void HandlePart(const HttpHeaders& headers, + const void* part, + size_t size) = 0; + }; + + private: + enum State + { + State_UnusedArea, + State_Content, + State_Done + }; + + State state_; + IHandler* handler_; + CStringMatcher headersMatcher_; + CStringMatcher boundaryMatcher_; + ChunkedBuffer buffer_; + size_t blockSize_; + + void ParseBlock(const void* data, + size_t size); + + void ParseStream(); + + public: + explicit MultipartStreamReader(const std::string& boundary); + + void SetBlockSize(size_t size); + + size_t GetBlockSize() const; + + void SetHandler(IHandler& handler); + + void AddChunk(const void* chunk, + size_t size); + + void AddChunk(const std::string& chunk); + + void CloseStream(); + + static bool GetMainContentType(std::string& contentType, + const HttpHeaders& headers); + + static bool ParseMultipartContentType(std::string& contentType, + std::string& subType, // Possibly empty + std::string& boundary, + const std::string& contentTypeHeader); + + static bool ParseHeaderArguments(std::string& main, + std::map& arguments, + const std::string& header); + }; +} diff --git a/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp b/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp new file mode 100644 index 0000000..da8b49a --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp @@ -0,0 +1,122 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "StringHttpOutput.h" + +#include "../OrthancException.h" +#include "../Toolbox.h" + +namespace Orthanc +{ + StringHttpOutput::StringHttpOutput() : + status_(HttpStatus_404_NotFound), + validBody_(true), + validHeaders_(true) + { + } + + + void StringHttpOutput::Send(bool isHeader, const void* buffer, size_t length) + { + if (isHeader) + { + if (validHeaders_) + { + headers_.AddChunk(buffer, length); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + else + { + if (validBody_) + { + body_.AddChunk(buffer, length); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + } + + + void StringHttpOutput::GetBody(std::string& output) + { + if (!validBody_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (status_ == HttpStatus_200_Ok) + { + body_.Flatten(output); + validBody_ = false; + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + void StringHttpOutput::GetHeaders(std::map& target, + bool keyToLowerCase) + { + if (!validHeaders_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + std::string s; + headers_.Flatten(s); + validHeaders_ = false; + + std::vector lines; + Orthanc::Toolbox::TokenizeString(lines, s, '\n'); + + target.clear(); + + for (size_t i = 1 /* skip the HTTP status line */; i < lines.size(); i++) + { + size_t colon = lines[i].find(':'); + if (colon != std::string::npos) + { + std::string key = lines[i].substr(0, colon); + + if (keyToLowerCase) + { + Toolbox::ToLowerCase(key); + } + + const std::string value = lines[i].substr(colon + 1); + target[Toolbox::StripSpaces(key)] = Toolbox::StripSpaces(value); + } + } + } + } +} diff --git a/OrthancFramework/Sources/HttpServer/StringHttpOutput.h b/OrthancFramework/Sources/HttpServer/StringHttpOutput.h new file mode 100644 index 0000000..164798c --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.h @@ -0,0 +1,70 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IHttpOutputStream.h" + +#include "../ChunkedBuffer.h" +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +#include + + +namespace Orthanc +{ + class StringHttpOutput : public IHttpOutputStream + { + private: + HttpStatus status_; + ChunkedBuffer body_; + ChunkedBuffer headers_; + bool validBody_; + bool validHeaders_; + + public: + StringHttpOutput(); + + virtual void OnHttpStatusReceived(HttpStatus status) ORTHANC_OVERRIDE + { + status_ = status; + } + + virtual void Send(bool isHeader, const void* buffer, size_t length) ORTHANC_OVERRIDE; + + virtual void DisableKeepAlive() ORTHANC_OVERRIDE + { + } + + HttpStatus GetStatus() const + { + return status_; + } + + void GetBody(std::string& output); + + void GetHeaders(std::map& target, + bool keyToLowerCase); + }; +} diff --git a/OrthancFramework/Sources/HttpServer/StringMatcher.cpp b/OrthancFramework/Sources/HttpServer/StringMatcher.cpp new file mode 100644 index 0000000..f9d6058 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/StringMatcher.cpp @@ -0,0 +1,148 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "StringMatcher.h" + +#include "../OrthancException.h" + +#include +//#include +//#include + +namespace Orthanc +{ + class StringMatcher::Search : public boost::noncopyable + { + private: + typedef boost::algorithm::boyer_moore Algorithm; + //typedef boost::algorithm::boyer_moore_horspool Algorithm; + + Algorithm algorithm_; + + public: + // WARNING - The lifetime of "pattern_" must be larger than + // "search_", as the latter internally keeps a pointer to "pattern" (*) + explicit Search(const std::string& pattern) : + algorithm_(pattern.begin(), pattern.end()) + { + } + + Iterator Apply(Iterator start, + Iterator end) const + { +#if BOOST_VERSION >= 106200 + return algorithm_(start, end).first; +#else + return algorithm_(start, end); +#endif + } + }; + + + StringMatcher::StringMatcher(const std::string& pattern) : + pattern_(pattern), + valid_(false) + { + // WARNING - Don't use "pattern" (local variable, will be + // destroyed once exiting the constructor) but "pattern_" + // (variable member, will last as long as the algorithm), + // otherwise lifetime is bad! (*) + search_.reset(new Search(pattern_)); + } + + const std::string& StringMatcher::GetPattern() const + { + return pattern_; + } + + bool StringMatcher::IsValid() const + { + return valid_; + } + + + bool StringMatcher::Apply(Iterator start, + Iterator end) + { + assert(search_.get() != NULL); + matchBegin_ = search_->Apply(start, end); + + if (matchBegin_ == end) + { + valid_ = false; + } + else + { + matchEnd_ = matchBegin_ + pattern_.size(); + assert(matchEnd_ <= end); + valid_ = true; + } + + return valid_; + } + + bool StringMatcher::Apply(const std::string& corpus) + { + return Apply(corpus.begin(), corpus.end()); + } + + + StringMatcher::Iterator StringMatcher::GetMatchBegin() const + { + if (valid_) + { + return matchBegin_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + StringMatcher::Iterator StringMatcher::GetMatchEnd() const + { + if (valid_) + { + return matchEnd_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const char* StringMatcher::GetPointerBegin() const + { + return &GetMatchBegin()[0]; + } + + + const char* StringMatcher::GetPointerEnd() const + { + return GetPointerBegin() + pattern_.size(); + } +} diff --git a/OrthancFramework/Sources/HttpServer/StringMatcher.h b/OrthancFramework/Sources/HttpServer/StringMatcher.h new file mode 100644 index 0000000..5be248e --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/StringMatcher.h @@ -0,0 +1,70 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../OrthancFramework.h" + +#include +#include +#include + +namespace Orthanc +{ + // Convenience class that wraps a Boost algorithm for string matching + class ORTHANC_PUBLIC StringMatcher : public boost::noncopyable + { + public: + typedef std::string::const_iterator Iterator; + + private: + class Search; + + boost::shared_ptr search_; // PImpl pattern + std::string pattern_; + bool valid_; + Iterator matchBegin_; + Iterator matchEnd_; + + public: + explicit StringMatcher(const std::string& pattern); + + const std::string& GetPattern() const; + + bool IsValid() const; + + bool Apply(Iterator start, + Iterator end); + + bool Apply(const std::string& corpus); + + Iterator GetMatchBegin() const; + + Iterator GetMatchEnd() const; + + const char* GetPointerBegin() const; + + const char* GetPointerEnd() const; + }; +} diff --git a/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp b/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp new file mode 100644 index 0000000..e424e16 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp @@ -0,0 +1,504 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "WebDavStorage.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SystemToolbox.h" +#include "../TemporaryFile.h" +#include "../Toolbox.h" + +namespace Orthanc +{ + class WebDavStorage::StorageFile : public boost::noncopyable + { + private: + std::unique_ptr file_; + std::string content_; + MimeType mime_; + boost::posix_time::ptime time_; + + void Touch() + { + time_ = boost::posix_time::second_clock::universal_time(); + } + + public: + StorageFile() : + mime_(MimeType_Binary) + { + Touch(); + } + + void SetContent(const std::string& content, + MimeType mime, + bool isMemory) + { + if (isMemory) + { + content_ = content; + file_.reset(); + } + else + { + content_.clear(); + file_.reset(new TemporaryFile); + file_->Write(content); + } + + mime_ = mime; + Touch(); + } + + MimeType GetMimeType() const + { + return mime_; + } + + void GetContent(std::string& target) const + { + if (file_.get() == NULL) + { + target = content_; + } + else + { + file_->Read(target); + } + } + + const boost::posix_time::ptime& GetTime() const + { + return time_; + } + + uint64_t GetContentLength() const + { + if (file_.get() == NULL) + { + return content_.size(); + } + else + { + return file_->GetFileSize(); + } + } + }; + + + class WebDavStorage::StorageFolder : public boost::noncopyable + { + private: + typedef std::map Files; + typedef std::map Subfolders; + + Files files_; + Subfolders subfolders_; + boost::posix_time::ptime time_; + + void Touch() + { + time_ = boost::posix_time::second_clock::universal_time(); + } + + void CheckName(const std::string& name) + { + if (name.empty() || + name.find('/') != std::string::npos || + name.find('\\') != std::string::npos || + name.find('\0') != std::string::npos) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Bad resource name for WebDAV: " + name); + } + } + + bool IsExisting(const std::string& name) const + { + return (files_.find(name) != files_.end() || + subfolders_.find(name) != subfolders_.end()); + } + + public: + StorageFolder() + { + Touch(); + } + + ~StorageFolder() + { + for (Files::iterator it = files_.begin(); it != files_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + for (Subfolders::iterator it = subfolders_.begin(); it != subfolders_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + size_t GetSize() const + { + return files_.size() + subfolders_.size(); + } + + const boost::posix_time::ptime& GetModificationTime() const + { + return time_; + } + + const StorageFile* LookupFile(const std::string& name) const + { + Files::const_iterator found = files_.find(name); + if (found == files_.end()) + { + return NULL; + } + else + { + assert(found->second != NULL); + return found->second; + } + } + + bool CreateSubfolder(const std::string& name) + { + CheckName(name); + + if (IsExisting(name)) + { + LOG(ERROR) << "WebDAV folder already existing: " << name; + return false; + } + else + { + subfolders_[name] = new StorageFolder; + Touch(); + return true; + } + } + + bool StoreFile(const std::string& name, + const std::string& content, + MimeType mime, + bool isMemory) + { + CheckName(name); + + if (subfolders_.find(name) != subfolders_.end()) + { + LOG(ERROR) << "WebDAV folder already existing: " << name; + return false; + } + + Files::iterator found = files_.find(name); + if (found == files_.end()) + { + std::unique_ptr f(new StorageFile); + f->SetContent(content, mime, isMemory); + files_[name] = f.release(); + } + else + { + assert(found->second != NULL); + found->second->SetContent(content, mime, isMemory); + } + + Touch(); + return true; + } + + StorageFolder* LookupFolder(const std::vector& path) + { + if (path.empty()) + { + return this; + } + else + { + Subfolders::const_iterator found = subfolders_.find(path[0]); + if (found == subfolders_.end()) + { + return NULL; + } + else + { + assert(found->second != NULL); + + std::vector p(path.begin() + 1, path.end()); + return found->second->LookupFolder(p); + } + } + } + + void ListCollection(Collection& collection) const + { + for (Files::const_iterator it = files_.begin(); it != files_.end(); ++it) + { + assert(it->second != NULL); + + std::unique_ptr f(new File(it->first)); + f->SetContentLength(it->second->GetContentLength()); + f->SetCreationTime(it->second->GetTime()); + collection.AddResource(f.release()); + } + + for (Subfolders::const_iterator it = subfolders_.begin(); it != subfolders_.end(); ++it) + { + std::unique_ptr f(new Folder(it->first)); + f->SetModificationTime(it->second->GetModificationTime()); + collection.AddResource(f.release()); + } + } + + bool DeleteItem(const std::vector& path) + { + if (path.size() == 0) + { + throw OrthancException(ErrorCode_InternalError); + } + else if (path.size() == 1) + { + { + Files::iterator f = files_.find(path[0]); + if (f != files_.end()) + { + assert(f->second != NULL); + delete f->second; + files_.erase(f); + Touch(); + return true; + } + } + + { + Subfolders::iterator f = subfolders_.find(path[0]); + if (f != subfolders_.end()) + { + assert(f->second != NULL); + delete f->second; + subfolders_.erase(f); + Touch(); + return true; + } + } + + return false; + } + else + { + Subfolders::iterator f = subfolders_.find(path[0]); + if (f != subfolders_.end()) + { + assert(f->second != NULL); + + std::vector p(path.begin() + 1, path.end()); + if (f->second->DeleteItem(p)) + { + Touch(); + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + } + + + void RemoveEmptyFolders() + { + std::list emptyFolders; + + for (Subfolders::const_iterator it = subfolders_.begin(); it != subfolders_.end(); ++it) + { + assert(it->second != NULL); + it->second->RemoveEmptyFolders(); + + if (it->second->GetSize() == 0) + { + assert(it->second != NULL); + delete it->second; + + emptyFolders.push_back(it->first); + } + } + + for (std::list::const_iterator it = emptyFolders.begin(); + it != emptyFolders.end(); ++it) + { + assert(subfolders_.find(*it) != subfolders_.end()); + subfolders_.erase(*it); + } + } + }; + + + WebDavStorage::StorageFolder* WebDavStorage::LookupParentFolder(const std::vector& path) + { + if (path.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + std::vector p(path.begin(), path.end() - 1); + return root_->LookupFolder(p); + } + + + WebDavStorage::WebDavStorage(bool isMemory) : + root_(new StorageFolder), + isMemory_(isMemory) + { + } + + + bool WebDavStorage::IsExistingFolder(const std::vector& path) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + return (root_->LookupFolder(path) != NULL); + } + + + bool WebDavStorage::ListCollection(Collection& collection, + const std::vector& path) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + const StorageFolder* folder = root_->LookupFolder(path); + if (folder == NULL) + { + return false; + } + else + { + folder->ListCollection(collection); + return true; + } + } + + + bool WebDavStorage::GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, + const std::vector& path) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + const StorageFolder* folder = LookupParentFolder(path); + if (folder == NULL) + { + return false; + } + else + { + const StorageFile* file = folder->LookupFile(path.back()); + if (file == NULL) + { + return false; + } + else + { + mime = file->GetMimeType(); + file->GetContent(content); + modificationTime = file->GetTime(); + return true; + } + } + } + + + bool WebDavStorage::StoreFile(const std::string& content, + const std::vector& path) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + StorageFolder* folder = LookupParentFolder(path); + if (folder == NULL) + { + LOG(WARNING) << "Inexisting folder in WebDAV: " << Toolbox::FlattenUri(path); + return false; + } + else + { + LOG(INFO) << "Storing " << content.size() + << " bytes in WebDAV bucket: " << Toolbox::FlattenUri(path);; + + MimeType mime = SystemToolbox::AutodetectMimeType(path.back()); + return folder->StoreFile(path.back(), content, mime, isMemory_); + } + } + + + bool WebDavStorage::CreateFolder(const std::vector& path) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + StorageFolder* folder = LookupParentFolder(path); + if (folder == NULL) + { + LOG(WARNING) << "Inexisting folder in WebDAV: " << Toolbox::FlattenUri(path); + return false; + } + else + { + LOG(INFO) << "Creating folder in WebDAV bucket: " << Toolbox::FlattenUri(path); + return folder->CreateSubfolder(path.back()); + } + } + + + bool WebDavStorage::DeleteItem(const std::vector& path) + { + if (path.empty()) + { + return false; // Cannot delete the root + } + else + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + LOG(INFO) << "Deleting from WebDAV bucket: " << Toolbox::FlattenUri(path); + return root_->DeleteItem(path); + } + } + + + void WebDavStorage::RemoveEmptyFolders() + { + boost::recursive_mutex::scoped_lock lock(mutex_); + root_->RemoveEmptyFolders(); + } +} diff --git a/OrthancFramework/Sources/HttpServer/WebDavStorage.h b/OrthancFramework/Sources/HttpServer/WebDavStorage.h new file mode 100644 index 0000000..7a24217 --- /dev/null +++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.h @@ -0,0 +1,76 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + + +#pragma once + +#include "IWebDavBucket.h" + +#include + +namespace Orthanc +{ + class WebDavStorage : public IWebDavBucket + { + private: + class StorageFile; + class StorageFolder; + + StorageFolder* LookupParentFolder(const std::vector& path); + + boost::shared_ptr root_; // PImpl + boost::recursive_mutex mutex_; + bool isMemory_; + + public: + explicit WebDavStorage(bool isMemory); + + virtual bool IsExistingFolder(const std::vector& path) ORTHANC_OVERRIDE; + + virtual bool ListCollection(Collection& collection, + const std::vector& path) ORTHANC_OVERRIDE; + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, + const std::vector& path) ORTHANC_OVERRIDE; + + virtual bool StoreFile(const std::string& content, + const std::vector& path) ORTHANC_OVERRIDE; + + virtual bool CreateFolder(const std::vector& path) ORTHANC_OVERRIDE; + + virtual bool DeleteItem(const std::vector& path) ORTHANC_OVERRIDE; + + virtual void Start() ORTHANC_OVERRIDE + { + } + + virtual void Stop() ORTHANC_OVERRIDE + { + } + + void RemoveEmptyFolders(); + }; +} diff --git a/OrthancFramework/Sources/IDynamicObject.h b/OrthancFramework/Sources/IDynamicObject.h new file mode 100644 index 0000000..6d4c69f --- /dev/null +++ b/OrthancFramework/Sources/IDynamicObject.h @@ -0,0 +1,74 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "OrthancFramework.h" + +#include + +namespace Orthanc +{ + /** + * This class should be the ancestor to any class whose type is + * determined at the runtime, and that can be dynamically allocated. + * Being a child of IDynamicObject only implies the existence of a + * virtual destructor. + **/ + class ORTHANC_PUBLIC IDynamicObject : public boost::noncopyable + { + public: + virtual ~IDynamicObject() + { + } + }; + + + /** + * This class is a simple implementation of a IDynamicObject that + * stores a single typed value. + */ + template + class SingleValueObject : public IDynamicObject + { + private: + T value_; + + public: + explicit SingleValueObject(const T& value) : + value_(value) + { + } + + const T& GetValue() const + { + return value_; + } + + void SetValue(const T& value) + { + value_ = value; + } + }; +} diff --git a/OrthancFramework/Sources/IMemoryBuffer.h b/OrthancFramework/Sources/IMemoryBuffer.h new file mode 100644 index 0000000..645eaf0 --- /dev/null +++ b/OrthancFramework/Sources/IMemoryBuffer.h @@ -0,0 +1,50 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include +#include + +namespace Orthanc +{ + /** + * This class abstracts a memory buffer and its memory unallocation + * function. + **/ + class IMemoryBuffer : public boost::noncopyable + { + public: + virtual ~IMemoryBuffer() + { + } + + // The content of the memory buffer will emptied after this call + virtual void MoveToString(std::string& target) = 0; + + virtual const void* GetData() const = 0; + + virtual size_t GetSize() const = 0; + }; +} diff --git a/OrthancFramework/Sources/Images/Font.cpp b/OrthancFramework/Sources/Images/Font.cpp new file mode 100644 index 0000000..2721925 --- /dev/null +++ b/OrthancFramework/Sources/Images/Font.cpp @@ -0,0 +1,427 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "Font.h" + +#if !defined(ORTHANC_ENABLE_LOCALE) +# error ORTHANC_ENABLE_LOCALE must be defined to use this file +#endif + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include "../OrthancException.h" +#include "../Toolbox.h" +#include "Image.h" +#include "ImageProcessing.h" + +#include +#include +#include +#include + +namespace Orthanc +{ + Font::Font() : + size_(0), + maxHeight_(0) + { + } + + + Font::~Font() + { + for (Characters::iterator it = characters_.begin(); + it != characters_.end(); ++it) + { + delete it->second; + } + } + + + void Font::LoadFromMemory(const std::string& font) + { + Json::Value v; + if (!Toolbox::ReadJson(v, font) || + v.type() != Json::objectValue || + !v.isMember("Name") || + !v.isMember("Size") || + !v.isMember("Characters") || + v["Name"].type() != Json::stringValue || + v["Size"].type() != Json::intValue || + v["Characters"].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFont); + } + + name_ = v["Name"].asString(); + size_ = v["Size"].asUInt(); + maxHeight_ = 0; + + Json::Value::Members characters = v["Characters"].getMemberNames(); + + for (size_t i = 0; i < characters.size(); i++) + { + const Json::Value& info = v["Characters"][characters[i]]; + if (info.type() != Json::objectValue || + !info.isMember("Advance") || + !info.isMember("Bitmap") || + !info.isMember("Height") || + !info.isMember("Top") || + !info.isMember("Width") || + info["Advance"].type() != Json::intValue || + info["Bitmap"].type() != Json::arrayValue || + info["Height"].type() != Json::intValue || + info["Top"].type() != Json::intValue || + info["Width"].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadFont); + } + + std::unique_ptr c(new Character); + + c->advance_ = info["Advance"].asUInt(); + c->height_ = info["Height"].asUInt(); + c->top_ = info["Top"].asUInt(); + c->width_ = info["Width"].asUInt(); + c->bitmap_.resize(info["Bitmap"].size()); + + if (c->height_ > maxHeight_) + { + maxHeight_ = c->height_; + } + + for (Json::Value::ArrayIndex j = 0; j < info["Bitmap"].size(); j++) + { + if (info["Bitmap"][j].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadFont); + } + + int value = info["Bitmap"][j].asInt(); + if (value < 0 || value > 255) + { + throw OrthancException(ErrorCode_BadFont); + } + + c->bitmap_[j] = static_cast(value); + } + + int index = boost::lexical_cast(characters[i]); + if (index < 0 || index > 255) + { + throw OrthancException(ErrorCode_BadFont); + } + + characters_[static_cast(index)] = c.release(); + } + } + + +#if ORTHANC_SANDBOXED == 0 + void Font::LoadFromFile(const std::string& path) + { + std::string font; + SystemToolbox::ReadFile(font, path); + LoadFromMemory(font); + } +#endif + + const std::string &Font::GetName() const + { + return name_; + } + + unsigned int Font::GetSize() const + { + return size_; + } + + + static unsigned int MyMin(unsigned int a, + unsigned int b) + { + return a < b ? a : b; + } + + + void Font::DrawCharacter(ImageAccessor& target, + const Character& character, + int x, + int y, + const uint8_t color[4]) const + { + // Compute the bounds of the character + if (x >= static_cast(target.GetWidth()) || + y >= static_cast(target.GetHeight())) + { + // The character is out of the image + return; + } + + unsigned int left = x < 0 ? -x : 0; + unsigned int top = y < 0 ? -y : 0; + unsigned int width = MyMin(character.width_, target.GetWidth() - x); + unsigned int height = MyMin(character.height_, target.GetHeight() - y); + + unsigned int bpp = target.GetBytesPerPixel(); + + // Blit the font bitmap OVER the target image + // https://en.wikipedia.org/wiki/Alpha_compositing + + for (unsigned int cy = top; cy < height; cy++) + { + uint8_t* p = reinterpret_cast(target.GetRow(y + cy)) + (x + left) * bpp; + unsigned int pos = cy * character.width_ + left; + + switch (target.GetFormat()) + { + case PixelFormat_Grayscale8: + { + assert(bpp == 1); + for (unsigned int cx = left; cx < width; cx++, pos++, p++) + { + uint16_t alpha = character.bitmap_[pos]; + uint16_t value = alpha * static_cast(color[0]) + (255 - alpha) * static_cast(*p); + *p = static_cast(value >> 8); + } + + break; + } + + case PixelFormat_RGB24: + { + assert(bpp == 3); + for (unsigned int cx = left; cx < width; cx++, pos++, p += 3) + { + uint16_t alpha = character.bitmap_[pos]; + for (uint8_t i = 0; i < 3; i++) + { + uint16_t value = alpha * static_cast(color[i]) + (255 - alpha) * static_cast(p[i]); + p[i] = static_cast(value >> 8); + } + } + + break; + } + + case PixelFormat_RGBA32: + case PixelFormat_BGRA32: + { + assert(bpp == 4); + + for (unsigned int cx = left; cx < width; cx++, pos++, p += 4) + { + float alpha = static_cast(character.bitmap_[pos]) / 255.0f; + float beta = (1.0f - alpha) * static_cast(p[3]) / 255.0f; + float denom = 1.0f / (alpha + beta); + + for (uint8_t i = 0; i < 3; i++) + { + p[i] = static_cast((alpha * static_cast(color[i]) + + beta * static_cast(p[i])) * denom); + } + + p[3] = static_cast(255.0f * (alpha + beta)); + } + + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + } + + + void Font::DrawInternal(ImageAccessor& target, + const std::string& utf8, + int x, + int y, + const uint8_t color[4]) const + { + if (target.GetFormat() != PixelFormat_Grayscale8 && + target.GetFormat() != PixelFormat_RGB24 && + target.GetFormat() != PixelFormat_RGBA32 && + target.GetFormat() != PixelFormat_BGRA32) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + int a = x; + +#if ORTHANC_ENABLE_LOCALE == 1 + std::string s = Toolbox::ConvertFromUtf8(utf8, Encoding_Latin1); +#else + // If the locale support is disabled, simply drop non-ASCII + // characters from the source UTF-8 string + std::string s = Toolbox::ConvertToAscii(utf8); +#endif + + for (size_t i = 0; i < s.size(); i++) + { + if (s[i] == '\n') + { + // Go to the next line + a = x; + y += maxHeight_ + 1; + } + else + { + Characters::const_iterator c = characters_.find(s[i]); + if (c != characters_.end()) + { + DrawCharacter(target, *c->second, a, y + static_cast(c->second->top_), color); + a += c->second->advance_; + } + } + } + } + + + void Font::Draw(ImageAccessor& target, + const std::string& utf8, + int x, + int y, + uint8_t grayscale) const + { + uint8_t color[4] = { grayscale, grayscale, grayscale, 255 }; + DrawInternal(target, utf8, x, y, color); + } + + + void Font::Draw(ImageAccessor& target, + const std::string& utf8, + int x, + int y, + uint8_t r, + uint8_t g, + uint8_t b) const + { + uint8_t color[4]; + + switch (target.GetFormat()) + { + case PixelFormat_BGRA32: + color[0] = b; + color[1] = g; + color[2] = r; + color[3] = 255; + break; + + default: + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = 255; + break; + } + + DrawInternal(target, utf8, x, y, color); + } + + + void Font::ComputeTextExtent(unsigned int& width, + unsigned int& height, + const std::string& utf8) const + { + width = 0; + height = 0; + +#if ORTHANC_ENABLE_LOCALE == 1 + std::string s = Toolbox::ConvertFromUtf8(utf8, Encoding_Latin1); +#else + // If the locale support is disabled, simply drop non-ASCII + // characters from the source UTF-8 string + std::string s = Toolbox::ConvertToAscii(utf8); +#endif + + // Compute the text extent + unsigned int x = 0; + unsigned int y = 0; + + for (size_t i = 0; i < s.size(); i++) + { + if (s[i] == '\n') + { + // Go to the next line + x = 0; + y += (maxHeight_ + 1); + } + else + { + Characters::const_iterator c = characters_.find(s[i]); + if (c != characters_.end()) + { + x += c->second->advance_; + + unsigned int bottom = y + c->second->top_ + c->second->height_; + if (bottom > height) + { + height = bottom; + } + + if (x > width) + { + width = x; + } + } + } + } + } + + + ImageAccessor* Font::Render(const std::string& utf8, + PixelFormat format, + uint8_t r, + uint8_t g, + uint8_t b) const + { + unsigned int width, height; + ComputeTextExtent(width, height, utf8); + + std::unique_ptr target(new Image(format, width, height, false)); + ImageProcessing::Set(*target, 0, 0, 0, 255); + Draw(*target, utf8, 0, 0, r, g, b); + + return target.release(); + } + + + ImageAccessor* Font::RenderAlpha(const std::string& utf8) const + { + unsigned int width, height; + ComputeTextExtent(width, height, utf8); + + std::unique_ptr target(new Image(PixelFormat_Grayscale8, width, height, false)); + ImageProcessing::Set(*target, 0); + Draw(*target, utf8, 0, 0, 255); + + return target.release(); + } +} diff --git a/OrthancFramework/Sources/Images/Font.h b/OrthancFramework/Sources/Images/Font.h new file mode 100644 index 0000000..11c0577 --- /dev/null +++ b/OrthancFramework/Sources/Images/Font.h @@ -0,0 +1,110 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../OrthancFramework.h" + +#include "ImageAccessor.h" + +#include +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC Font : public boost::noncopyable + { + private: + struct Character + { + unsigned int width_; + unsigned int height_; + unsigned int top_; + unsigned int advance_; + std::vector bitmap_; + }; + + typedef std::map Characters; + + std::string name_; + unsigned int size_; + Characters characters_; + unsigned int maxHeight_; + + void DrawCharacter(ImageAccessor& target, + const Character& character, + int x, + int y, + const uint8_t color[4]) const; + + void DrawInternal(ImageAccessor& target, + const std::string& utf8, + int x, + int y, + const uint8_t color[4]) const; + + public: + Font(); + + ~Font(); + + void LoadFromMemory(const std::string& font); + +#if ORTHANC_SANDBOXED == 0 + void LoadFromFile(const std::string& path); +#endif + + const std::string& GetName() const; + + unsigned int GetSize() const; + + void Draw(ImageAccessor& target, + const std::string& utf8, + int x, + int y, + uint8_t grayscale) const; + + void Draw(ImageAccessor& target, + const std::string& utf8, + int x, + int y, + uint8_t r, + uint8_t g, + uint8_t b) const; + + void ComputeTextExtent(unsigned int& width, + unsigned int& height, + const std::string& utf8) const; + + ImageAccessor* Render(const std::string& utf8, + PixelFormat format, + uint8_t r, + uint8_t g, + uint8_t b) const; + + ImageAccessor* RenderAlpha(const std::string& utf8) const; + }; +} diff --git a/OrthancFramework/Sources/Images/FontRegistry.cpp b/OrthancFramework/Sources/Images/FontRegistry.cpp new file mode 100644 index 0000000..5f2c975 --- /dev/null +++ b/OrthancFramework/Sources/Images/FontRegistry.cpp @@ -0,0 +1,89 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "FontRegistry.h" + +#include "../OrthancException.h" + +#include + +namespace Orthanc +{ + FontRegistry::~FontRegistry() + { + for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it) + { + delete *it; + } + } + + + void FontRegistry::AddFromMemory(const std::string& font) + { + std::unique_ptr f(new Font); + f->LoadFromMemory(font); + fonts_.push_back(f.release()); + } + + +#if ORTHANC_SANDBOXED == 0 + void FontRegistry::AddFromFile(const std::string& path) + { + std::unique_ptr f(new Font); + f->LoadFromFile(path); + fonts_.push_back(f.release()); + } +#endif + + size_t FontRegistry::GetSize() const + { + return fonts_.size(); + } + + const Font& FontRegistry::GetFont(size_t i) const + { + if (i >= fonts_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return *fonts_[i]; + } + } + + const Font* FontRegistry::FindFont(const std::string& fontName) const + { + for (Fonts::const_iterator it = fonts_.begin(); it != fonts_.end(); ++it) + { + if ((*it)->GetName() == fontName) + { + return *it; + } + } + + return NULL; + } +} diff --git a/OrthancFramework/Sources/Images/FontRegistry.h b/OrthancFramework/Sources/Images/FontRegistry.h new file mode 100644 index 0000000..40a1546 --- /dev/null +++ b/OrthancFramework/Sources/Images/FontRegistry.h @@ -0,0 +1,53 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "Font.h" + +namespace Orthanc +{ + class ORTHANC_PUBLIC FontRegistry : public boost::noncopyable + { + private: + typedef std::vector Fonts; + + Fonts fonts_; + + public: + ~FontRegistry(); + + void AddFromMemory(const std::string& font); + +#if ORTHANC_SANDBOXED == 0 + void AddFromFile(const std::string& path); +#endif + + size_t GetSize() const; + + const Font& GetFont(size_t i) const; + + const Font* FindFont(const std::string& fontName) const; + }; +} diff --git a/OrthancFramework/Sources/Images/IImageWriter.cpp b/OrthancFramework/Sources/Images/IImageWriter.cpp new file mode 100644 index 0000000..c8277f8 --- /dev/null +++ b/OrthancFramework/Sources/Images/IImageWriter.cpp @@ -0,0 +1,64 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "IImageWriter.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +namespace Orthanc +{ +#if ORTHANC_SANDBOXED == 0 + void IImageWriter::WriteToFileInternal(const std::string& path, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + std::string compressed; + WriteToMemoryInternal(compressed, width, height, pitch, format, buffer); + SystemToolbox::WriteFile(compressed, path); + } +#endif + + void IImageWriter::WriteToMemory(IImageWriter& writer, + std::string &compressed, + const ImageAccessor &accessor) + { + writer.WriteToMemoryInternal(compressed, accessor.GetWidth(), accessor.GetHeight(), + accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer()); + } + +#if ORTHANC_SANDBOXED == 0 + void IImageWriter::WriteToFile(IImageWriter& writer, + const std::string &path, + const ImageAccessor &accessor) + { + writer.WriteToFileInternal(path, accessor.GetWidth(), accessor.GetHeight(), + accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer()); + } +#endif +} diff --git a/OrthancFramework/Sources/Images/IImageWriter.h b/OrthancFramework/Sources/Images/IImageWriter.h new file mode 100644 index 0000000..5e56ae1 --- /dev/null +++ b/OrthancFramework/Sources/Images/IImageWriter.h @@ -0,0 +1,71 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "ImageAccessor.h" + +#include + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +namespace Orthanc +{ + class ORTHANC_PUBLIC IImageWriter : public boost::noncopyable + { + protected: + virtual void WriteToMemoryInternal(std::string& compressed, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) = 0; + +#if ORTHANC_SANDBOXED == 0 + virtual void WriteToFileInternal(const std::string& path, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer); +#endif + + public: + virtual ~IImageWriter() + { + } + + static void WriteToMemory(IImageWriter& writer, + std::string& compressed, + const ImageAccessor& accessor); + +#if ORTHANC_SANDBOXED == 0 + static void WriteToFile(IImageWriter& writer, + const std::string& path, + const ImageAccessor& accessor); +#endif + }; +} diff --git a/OrthancFramework/Sources/Images/Image.cpp b/OrthancFramework/Sources/Images/Image.cpp new file mode 100644 index 0000000..27d6c08 --- /dev/null +++ b/OrthancFramework/Sources/Images/Image.cpp @@ -0,0 +1,53 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "Image.h" + +#include "../Compatibility.h" +#include "ImageProcessing.h" + +#include + +namespace Orthanc +{ + Image::Image(PixelFormat format, + unsigned int width, + unsigned int height, + bool forceMinimalPitch) : + image_(format, width, height, forceMinimalPitch) + { + ImageAccessor accessor; + image_.GetWriteableAccessor(accessor); + + AssignWritable(format, width, height, accessor.GetPitch(), accessor.GetBuffer()); + } + + + Image* Image::Clone(const ImageAccessor& source) + { + std::unique_ptr target(new Image(source.GetFormat(), source.GetWidth(), source.GetHeight(), false)); + ImageProcessing::Copy(*target, source); + return target.release(); + } +} diff --git a/OrthancFramework/Sources/Images/Image.h b/OrthancFramework/Sources/Images/Image.h new file mode 100644 index 0000000..43ffc8e --- /dev/null +++ b/OrthancFramework/Sources/Images/Image.h @@ -0,0 +1,45 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "ImageAccessor.h" +#include "ImageBuffer.h" + +namespace Orthanc +{ + class ORTHANC_PUBLIC Image : public ImageAccessor + { + private: + ImageBuffer image_; + + public: + Image(PixelFormat format, + unsigned int width, + unsigned int height, + bool forceMinimalPitch); + + static Image* Clone(const ImageAccessor& source); + }; +} diff --git a/OrthancFramework/Sources/Images/ImageAccessor.cpp b/OrthancFramework/Sources/Images/ImageAccessor.cpp new file mode 100644 index 0000000..1c39535 --- /dev/null +++ b/OrthancFramework/Sources/Images/ImageAccessor.cpp @@ -0,0 +1,371 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "ImageAccessor.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../ChunkedBuffer.h" + +#include +#include +#include + + + +namespace Orthanc +{ + template + static void ToMatlabStringInternal(ChunkedBuffer& target, + const ImageAccessor& source) + { + target.AddChunk("double([ "); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + const PixelType* p = reinterpret_cast(source.GetConstRow(y)); + + std::string s; + if (y > 0) + { + s = "; "; + } + + s.reserve(width * 8); + + for (unsigned int x = 0; x < width; x++, p++) + { + s += boost::lexical_cast(static_cast(*p)) + " "; + } + + target.AddChunk(s); + } + + target.AddChunk("])"); + } + + + static void RGB24ToMatlabString(ChunkedBuffer& target, + const ImageAccessor& source) + { + assert(source.GetFormat() == PixelFormat_RGB24); + + target.AddChunk("double(permute(reshape([ "); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + + std::string s; + s.reserve(width * 3 * 8); + + for (unsigned int x = 0; x < 3 * width; x++, p++) + { + s += boost::lexical_cast(static_cast(*p)) + " "; + } + + target.AddChunk(s); + } + + target.AddChunk("], [ 3 " + boost::lexical_cast(height) + + " " + boost::lexical_cast(width) + " ]), [ 3 2 1 ]))"); + } + + + ImageAccessor::ImageAccessor() + { + AssignEmpty(PixelFormat_Grayscale8); + } + + ImageAccessor::~ImageAccessor() + { + } + + bool ImageAccessor::IsReadOnly() const + { + return readOnly_; + } + + PixelFormat ImageAccessor::GetFormat() const + { + return format_; + } + + unsigned int ImageAccessor::GetBytesPerPixel() const + { + return ::Orthanc::GetBytesPerPixel(format_); + } + + unsigned int ImageAccessor::GetWidth() const + { + return width_; + } + + unsigned int ImageAccessor::GetHeight() const + { + return height_; + } + + unsigned int ImageAccessor::GetPitch() const + { + return pitch_; + } + + size_t ImageAccessor::GetSize() const + { + return GetHeight() * GetPitch(); + } + + const void *ImageAccessor::GetConstBuffer() const + { + return buffer_; + } + + void* ImageAccessor::GetBuffer() + { + if (readOnly_) + { + throw OrthancException(ErrorCode_ReadOnly, + "Trying to write to a read-only image"); + } + + return buffer_; + } + + + const void* ImageAccessor::GetConstRow(unsigned int y) const + { + if (buffer_ != NULL) + { + return buffer_ + static_cast(y) * static_cast(pitch_); + } + else + { + return NULL; + } + } + + + void* ImageAccessor::GetRow(unsigned int y) + { + if (readOnly_) + { + throw OrthancException(ErrorCode_ReadOnly, + "Trying to write to a read-only image"); + } + + if (buffer_ != NULL) + { + return buffer_ + static_cast(y) * static_cast(pitch_); + } + else + { + return NULL; + } + } + + + void ImageAccessor::AssignEmpty(PixelFormat format) + { + readOnly_ = false; + format_ = format; + width_ = 0; + height_ = 0; + pitch_ = 0; + buffer_ = NULL; + } + + + void ImageAccessor::AssignReadOnly(PixelFormat format, + unsigned int width, + unsigned int height, + unsigned int pitch, + const void *buffer) + { + readOnly_ = true; + format_ = format; + width_ = width; + height_ = height; + pitch_ = pitch; + buffer_ = reinterpret_cast(const_cast(buffer)); + + if (GetBytesPerPixel() * width_ > pitch_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + void ImageAccessor::GetReadOnlyAccessor(ImageAccessor &target) const + { + target.AssignReadOnly(format_, width_, height_, pitch_, buffer_); + } + + + void ImageAccessor::AssignWritable(PixelFormat format, + unsigned int width, + unsigned int height, + unsigned int pitch, + void *buffer) + { + readOnly_ = false; + format_ = format; + width_ = width; + height_ = height; + pitch_ = pitch; + buffer_ = reinterpret_cast(buffer); + + if (GetBytesPerPixel() * width_ > pitch_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void ImageAccessor::GetWriteableAccessor(ImageAccessor& target) const + { + if (readOnly_) + { + throw OrthancException(ErrorCode_ReadOnly); + } + else + { + target.AssignWritable(format_, width_, height_, pitch_, buffer_); + } + } + + + void ImageAccessor::ToMatlabString(std::string& target) const + { + ChunkedBuffer buffer; + + switch (GetFormat()) + { + case PixelFormat_Grayscale8: + ToMatlabStringInternal(buffer, *this); + break; + + case PixelFormat_Grayscale16: + ToMatlabStringInternal(buffer, *this); + break; + + case PixelFormat_Grayscale32: + ToMatlabStringInternal(buffer, *this); + break; + + case PixelFormat_Grayscale64: + ToMatlabStringInternal(buffer, *this); + break; + + case PixelFormat_SignedGrayscale16: + ToMatlabStringInternal(buffer, *this); + break; + + case PixelFormat_Float32: + ToMatlabStringInternal(buffer, *this); + break; + + case PixelFormat_RGB24: + RGB24ToMatlabString(buffer, *this); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + buffer.Flatten(target); + } + + + + void ImageAccessor::GetRegion(ImageAccessor& accessor, + unsigned int x, + unsigned int y, + unsigned int width, + unsigned int height) const + { + if (x + width > width_ || + y + height > height_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (width == 0 || + height == 0) + { + accessor.AssignWritable(format_, 0, 0, 0, NULL); + } + else + { + uint8_t* p = (buffer_ + + static_cast(y) * static_cast(pitch_) + + static_cast(x) * static_cast(GetBytesPerPixel())); + + if (readOnly_) + { + accessor.AssignReadOnly(format_, width, height, pitch_, p); + } + else + { + accessor.AssignWritable(format_, width, height, pitch_, p); + } + } + } + + + void ImageAccessor::SetFormat(PixelFormat format) + { + if (readOnly_) + { + throw OrthancException(ErrorCode_ReadOnly, + "Trying to modify the format of a read-only image"); + } + + if (::Orthanc::GetBytesPerPixel(format) != ::Orthanc::GetBytesPerPixel(format_)) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + format_ = format; + } + + +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 + void* ImageAccessor::GetBuffer() const + { + return const_cast(*this).GetBuffer(); + } + + void* ImageAccessor::GetRow(unsigned int y) const + { + return const_cast(*this).GetRow(y); + } +#endif +} diff --git a/OrthancFramework/Sources/Images/ImageAccessor.h b/OrthancFramework/Sources/Images/ImageAccessor.h new file mode 100644 index 0000000..45790ab --- /dev/null +++ b/OrthancFramework/Sources/Images/ImageAccessor.h @@ -0,0 +1,127 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Compatibility.h" +#include "../Enumerations.h" + +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC ImageAccessor : public boost::noncopyable + { + private: + template + friend struct ImageTraits; + + bool readOnly_; + PixelFormat format_; + unsigned int width_; + unsigned int height_; + unsigned int pitch_; + uint8_t *buffer_; + + template + const T& GetPixelUnchecked(unsigned int x, + unsigned int y) const + { + const uint8_t* row = reinterpret_cast(buffer_) + y * pitch_; + return reinterpret_cast(row) [x]; + } + + + template + T& GetPixelUnchecked(unsigned int x, + unsigned int y) + { + uint8_t* row = reinterpret_cast(buffer_) + y * pitch_; + return reinterpret_cast(row) [x]; + } + +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 + // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore + void* GetBuffer() const; + void* GetRow(unsigned int y) const; +#endif + + public: + ImageAccessor(); + + virtual ~ImageAccessor(); + + bool IsReadOnly() const; + + PixelFormat GetFormat() const; + + unsigned int GetBytesPerPixel() const; + + unsigned int GetWidth() const; + + unsigned int GetHeight() const; + + unsigned int GetPitch() const; + + size_t GetSize() const; + + const void* GetConstBuffer() const; + + void* GetBuffer(); + + const void* GetConstRow(unsigned int y) const; + + void* GetRow(unsigned int y); + + void AssignEmpty(PixelFormat format); + + void AssignReadOnly(PixelFormat format, + unsigned int width, + unsigned int height, + unsigned int pitch, + const void *buffer); + + void GetReadOnlyAccessor(ImageAccessor& target) const; + + void AssignWritable(PixelFormat format, + unsigned int width, + unsigned int height, + unsigned int pitch, + void *buffer); + + void GetWriteableAccessor(ImageAccessor& target) const; + + void ToMatlabString(std::string& target) const; + + void GetRegion(ImageAccessor& accessor, + unsigned int x, + unsigned int y, + unsigned int width, + unsigned int height) const; + + void SetFormat(PixelFormat format); + }; +} diff --git a/OrthancFramework/Sources/Images/ImageBuffer.cpp b/OrthancFramework/Sources/Images/ImageBuffer.cpp new file mode 100644 index 0000000..99c8649 --- /dev/null +++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp @@ -0,0 +1,207 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "ImageBuffer.h" + +#include "../OrthancException.h" + +#include +#include +#include + +namespace Orthanc +{ + void ImageBuffer::Allocate() + { + if (changed_) + { + Deallocate(); + + /* + if (forceMinimalPitch_) + { + TODO: Align pitch and memory buffer to optimal size for SIMD. + } + */ + + pitch_ = GetBytesPerPixel() * width_; + size_t size = static_cast(pitch_) * static_cast(height_); + + if (size == 0) + { + buffer_ = NULL; + } + else + { + buffer_ = malloc(size); + if (buffer_ == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory, + "Failed to allocate an image buffer of size " + boost::lexical_cast(width_) + "x" + boost::lexical_cast(height_)); + } + } + + changed_ = false; + } + } + + + void ImageBuffer::Deallocate() + { + if (buffer_ != NULL) + { + free(buffer_); + buffer_ = NULL; + changed_ = true; + } + } + + + ImageBuffer::ImageBuffer(PixelFormat format, + unsigned int width, + unsigned int height, + bool forceMinimalPitch) : + forceMinimalPitch_(forceMinimalPitch) + { + Initialize(); + SetWidth(width); + SetHeight(height); + SetFormat(format); + } + + ImageBuffer::ImageBuffer() + { + Initialize(); + } + + ImageBuffer::~ImageBuffer() + { + Deallocate(); + } + + PixelFormat ImageBuffer::GetFormat() const + { + return format_; + } + + + void ImageBuffer::Initialize() + { + changed_ = false; + forceMinimalPitch_ = true; + format_ = PixelFormat_Grayscale8; + width_ = 0; + height_ = 0; + pitch_ = 0; + buffer_ = NULL; + } + + + void ImageBuffer::SetFormat(PixelFormat format) + { + if (format != format_) + { + changed_ = true; + format_ = format; + } + } + + unsigned int ImageBuffer::GetWidth() const + { + return width_; + } + + + void ImageBuffer::SetWidth(unsigned int width) + { + if (width != width_) + { + changed_ = true; + width_ = width; + } + } + + unsigned int ImageBuffer::GetHeight() const + { + return height_; + } + + + void ImageBuffer::SetHeight(unsigned int height) + { + if (height != height_) + { + changed_ = true; + height_ = height; + } + } + + unsigned int ImageBuffer::GetBytesPerPixel() const + { + return ::Orthanc::GetBytesPerPixel(format_); + } + + + void ImageBuffer::GetReadOnlyAccessor(ImageAccessor& accessor) + { + Allocate(); + accessor.AssignReadOnly(format_, width_, height_, pitch_, buffer_); + } + + + void ImageBuffer::GetWriteableAccessor(ImageAccessor& accessor) + { + Allocate(); + accessor.AssignWritable(format_, width_, height_, pitch_, buffer_); + } + + bool ImageBuffer::IsMinimalPitchForced() const + { + return forceMinimalPitch_; + } + + + void ImageBuffer::AcquireOwnership(ImageBuffer& other) + { + // Remove the content of the current image + Deallocate(); + + // Force the allocation of the other image (if not already + // allocated) + other.Allocate(); + + // Transfer the content of the other image + changed_ = false; + forceMinimalPitch_ = other.forceMinimalPitch_; + format_ = other.format_; + width_ = other.width_; + height_ = other.height_; + pitch_ = other.pitch_; + buffer_ = other.buffer_; + + // Force the reinitialization of the other image + other.Initialize(); + } +} diff --git a/OrthancFramework/Sources/Images/ImageBuffer.h b/OrthancFramework/Sources/Images/ImageBuffer.h new file mode 100644 index 0000000..0adb0a5 --- /dev/null +++ b/OrthancFramework/Sources/Images/ImageBuffer.h @@ -0,0 +1,85 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "ImageAccessor.h" + +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC ImageBuffer : public boost::noncopyable + { + private: + bool changed_; + + bool forceMinimalPitch_; // Currently unused + PixelFormat format_; + unsigned int width_; + unsigned int height_; + unsigned int pitch_; + void *buffer_; + + void Initialize(); + + void Allocate(); + + void Deallocate(); + + public: + ImageBuffer(PixelFormat format, + unsigned int width, + unsigned int height, + bool forceMinimalPitch); + + ImageBuffer(); + + ~ImageBuffer(); + + PixelFormat GetFormat() const; + + void SetFormat(PixelFormat format); + + unsigned int GetWidth() const; + + void SetWidth(unsigned int width); + + unsigned int GetHeight() const; + + void SetHeight(unsigned int height); + + unsigned int GetBytesPerPixel() const; + + void GetReadOnlyAccessor(ImageAccessor& accessor); + + void GetWriteableAccessor(ImageAccessor& accessor); + + bool IsMinimalPitchForced() const; + + void AcquireOwnership(ImageBuffer& other); + }; +} diff --git a/OrthancFramework/Sources/Images/ImageProcessing.cpp b/OrthancFramework/Sources/Images/ImageProcessing.cpp new file mode 100644 index 0000000..9d24c29 --- /dev/null +++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp @@ -0,0 +1,3176 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "ImageProcessing.h" + +#include "Image.h" +#include "ImageTraits.h" +#include "PixelTraits.h" +#include "../OrthancException.h" + +#ifdef __EMSCRIPTEN__ +/* + Avoid this error: + ----------------- + .../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion] + .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround' requested here + .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal' requested here +*/ +#pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion" +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Orthanc +{ + ImageProcessing::ImagePoint::ImagePoint(int32_t x, + int32_t y) : + x_(x), + y_(y) + { + } + + int32_t ImageProcessing::ImagePoint::GetX() const + { + return x_; + } + + int32_t ImageProcessing::ImagePoint::GetY() const + { + return y_; + } + + void ImageProcessing::ImagePoint::Set(int32_t x, int32_t y) + { + x_ = x; + y_ = y; + } + + void ImageProcessing::ImagePoint::ClipTo(int32_t minX, int32_t maxX, int32_t minY, int32_t maxY) + { + x_ = std::max(minX, std::min(maxX, x_)); + y_ = std::max(minY, std::min(maxY, y_)); + } + + double ImageProcessing::ImagePoint::GetDistanceTo(const ImagePoint& other) const + { + double dx = (double)(other.GetX() - GetX()); + double dy = (double)(other.GetY() - GetY()); + return sqrt(dx * dx + dy * dy); + } + + double ImageProcessing::ImagePoint::GetDistanceToLine(double a, double b, double c) const // where ax + by + c = 0 is the equation of the line + { + return std::abs(a * static_cast(GetX()) + b * static_cast(GetY()) + c) / pow(a * a + b * b, 0.5); + } + + template + static void ConvertInternal(ImageAccessor& target, + const ImageAccessor& source) + { + // WARNING - "::min()" should be replaced by "::lowest()" if + // dealing with float or double (which is not the case so far) + assert(sizeof(TargetType) <= 2); // Safeguard to remember about "float/double" + const TargetType minValue = std::numeric_limits::min(); + const TargetType maxValue = std::numeric_limits::max(); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + TargetType* t = reinterpret_cast(target.GetRow(y)); + const SourceType* s = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, t++, s++) + { + if (static_cast(*s) < static_cast(minValue)) + { + *t = minValue; + } + else if (static_cast(*s) > static_cast(maxValue)) + { + *t = maxValue; + } + else + { + *t = static_cast(*s); + } + } + } + } + + + template + static void ConvertGrayscaleToFloat(ImageAccessor& target, + const ImageAccessor& source) + { + assert(sizeof(float) == 4); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + float* t = reinterpret_cast(target.GetRow(y)); + const SourceType* s = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, t++, s++) + { + *t = static_cast(*s); + } + } + } + + + template + static void ConvertFloatToGrayscale(ImageAccessor& target, + const ImageAccessor& source) + { + typedef typename PixelTraits::PixelType TargetType; + + assert(sizeof(float) == 4); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + TargetType* q = reinterpret_cast(target.GetRow(y)); + const float* p = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + PixelTraits::FloatToPixel(*q, *p); + } + } + } + + + template + static void ConvertColorToGrayscale(ImageAccessor& target, + const ImageAccessor& source) + { + assert(source.GetFormat() == PixelFormat_RGB24); + + // WARNING - "::min()" should be replaced by "::lowest()" if + // dealing with float or double (which is not the case so far) + assert(sizeof(TargetType) <= 2); // Safeguard to remember about "float/double" + const TargetType minValue = std::numeric_limits::min(); + const TargetType maxValue = std::numeric_limits::max(); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + TargetType* t = reinterpret_cast(target.GetRow(y)); + const uint8_t* s = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, t++, s += 3) + { + // Y = 0.2126 R + 0.7152 G + 0.0722 B + int32_t v = (2126 * static_cast(s[0]) + + 7152 * static_cast(s[1]) + + 0722 * static_cast(s[2])) / 10000; + + if (static_cast(v) < static_cast(minValue)) + { + *t = minValue; + } + else if (static_cast(v) > static_cast(maxValue)) + { + *t = maxValue; + } + else + { + *t = static_cast(v); + } + } + } + } + + + static void MemsetZeroInternal(ImageAccessor& image) + { + const unsigned int height = image.GetHeight(); + const size_t lineSize = image.GetBytesPerPixel() * image.GetWidth(); + const size_t pitch = image.GetPitch(); + + uint8_t *p = reinterpret_cast(image.GetBuffer()); + + for (unsigned int y = 0; y < height; y++) + { + memset(p, 0, lineSize); + p += pitch; + } + } + + + template + static void SetInternal(ImageAccessor& image, + int64_t constant) + { + if (constant == 0 && + (image.GetFormat() == PixelFormat_Grayscale8 || + image.GetFormat() == PixelFormat_Grayscale16 || + image.GetFormat() == PixelFormat_Grayscale32 || + image.GetFormat() == PixelFormat_Grayscale64 || + image.GetFormat() == PixelFormat_SignedGrayscale16)) + { + MemsetZeroInternal(image); + } + else + { + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + PixelType* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + *p = static_cast(constant); + } + } + } + } + + + template + static void GetMinMaxValueInternal(PixelType& minValue, + PixelType& maxValue, + const ImageAccessor& source, + const PixelType LowestValue = std::numeric_limits::min()) + { + // Deal with the special case of empty image + if (source.GetWidth() == 0 || + source.GetHeight() == 0) + { + minValue = 0; + maxValue = 0; + return; + } + + minValue = std::numeric_limits::max(); + maxValue = LowestValue; + + const unsigned int height = source.GetHeight(); + const unsigned int width = source.GetWidth(); + + for (unsigned int y = 0; y < height; y++) + { + const PixelType* p = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + if (*p < minValue) + { + minValue = *p; + } + + if (*p > maxValue) + { + maxValue = *p; + } + } + } + } + + + + template + static void AddConstantInternal(ImageAccessor& image, + int64_t constant) + { + if (constant == 0) + { + return; + } + + // WARNING - "::min()" should be replaced by "::lowest()" if + // dealing with float or double (which is not the case so far) + assert(sizeof(PixelType) <= 2); // Safeguard to remember about "float/double" + const int64_t minValue = std::numeric_limits::min(); + const int64_t maxValue = std::numeric_limits::max(); + + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + PixelType* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + int64_t v = static_cast(*p) + constant; + + if (v > maxValue) + { + *p = std::numeric_limits::max(); + } + else if (v < minValue) + { + *p = std::numeric_limits::min(); + } + else + { + *p = static_cast(v); + } + } + } + } + + + + template + static void MultiplyConstantInternal(ImageAccessor& image, + float factor) + { + if (std::abs(factor - 1.0f) <= std::numeric_limits::epsilon()) + { + return; + } + + // WARNING - "::min()" should be replaced by "::lowest()" if + // dealing with float or double (which is not the case so far) + assert(sizeof(PixelType) <= 2); // Safeguard to remember about "float/double" + const int64_t minValue = std::numeric_limits::min(); + const int64_t maxValue = std::numeric_limits::max(); + + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + PixelType* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + int64_t v; + if (UseRound) + { + assert(sizeof(long long) == sizeof(int64_t)); + // The "round" operation is very costly + v = boost::math::llround(static_cast(*p) * factor); + } + else + { + v = static_cast(static_cast(*p) * factor); + } + + if (v > maxValue) + { + *p = std::numeric_limits::max(); + } + else if (v < minValue) + { + *p = std::numeric_limits::min(); + } + else + { + *p = static_cast(v); + } + } + } + } + + + // Computes "a * x + b" at each pixel => Note that this is not the + // same convention as in "ShiftScale()", but it is the convention of + // "ShiftScale2()" + template + static void ShiftScaleIntegerInternal(ImageAccessor& target, + const ImageAccessor& source, + float a, + float b) + // This function can be applied inplace (source == target) + { + assert(target.GetFormat() != PixelFormat_Float32); + + if (source.GetWidth() != target.GetWidth() || + source.GetHeight() != target.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + if (&source == &target && + source.GetFormat() != target.GetFormat()) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + const TargetType minPixelValue = std::numeric_limits::min(); + const TargetType maxPixelValue = std::numeric_limits::max(); + const float minFloatValue = static_cast(minPixelValue); + const float maxFloatValue = static_cast(maxPixelValue); + + const unsigned int height = target.GetHeight(); + const unsigned int width = target.GetWidth(); + + for (unsigned int y = 0; y < height; y++) + { + TargetType* p = reinterpret_cast(target.GetRow(y)); + const SourceType* q = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + float v = a * static_cast(*q) + b; + + if (v >= maxFloatValue) + { + *p = maxPixelValue; + } + else if (v <= minFloatValue) + { + *p = minPixelValue; + } + else if (UseRound) + { + // The "round" operation is very costly + assert(sizeof(TargetType) < sizeof(int)); + *p = static_cast(boost::math::iround(v)); + } + else + { + *p = static_cast(std::floor(v)); + } + + if (Invert) + { + *p = maxPixelValue - *p; + } + } + } + } + + + template + static void ShiftScaleFloatInternal(ImageAccessor& target, + const ImageAccessor& source, + float a, + float b) + // This function can be applied inplace (source == target) + { + assert(target.GetFormat() == PixelFormat_Float32); + + if (source.GetWidth() != target.GetWidth() || + source.GetHeight() != target.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + if (&source == &target && + source.GetFormat() != target.GetFormat()) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + const unsigned int height = target.GetHeight(); + const unsigned int width = target.GetWidth(); + + for (unsigned int y = 0; y < height; y++) + { + float* p = reinterpret_cast(target.GetRow(y)); + const SourceType* q = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + *p = a * static_cast(*q) + b; + } + } + } + + + template + static void ShiftRightInternal(ImageAccessor& image, + unsigned int shift) + { + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + + for (unsigned int y = 0; y < height; y++) + { + PixelType* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + *p = *p >> shift; + } + } + } + + template + static void ShiftLeftInternal(ImageAccessor& image, + unsigned int shift) + { + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + + for (unsigned int y = 0; y < height; y++) + { + PixelType* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + *p = *p << shift; + } + } + } + + void ImageProcessing::Copy(ImageAccessor& target, + const ImageAccessor& source) + { + if (target.GetWidth() != source.GetWidth() || + target.GetHeight() != source.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + if (target.GetFormat() != source.GetFormat()) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + const unsigned int lineSize = source.GetBytesPerPixel() * source.GetWidth(); + assert(source.GetPitch() >= lineSize && target.GetPitch() >= lineSize); + + const unsigned int height = source.GetHeight(); + for (unsigned int y = 0; y < height; y++) + { + memcpy(target.GetRow(y), source.GetConstRow(y), lineSize); + } + } + + template + static void ApplyWindowingInternal(ImageAccessor& target, + const ImageAccessor& source, + float windowCenter, + float windowWidth, + float rescaleSlope, + float rescaleIntercept, + bool invert) + { + assert(sizeof(SourceType) == source.GetBytesPerPixel() && + sizeof(TargetType) == target.GetBytesPerPixel()); + + const TargetType maxTargetValue = std::numeric_limits::max(); + const float maxFloatValue = static_cast(maxTargetValue); + + const float windowIntercept = windowCenter - windowWidth / 2.0f; + const float windowSlope = (maxFloatValue + 1.0f) / windowWidth; + + const float a = rescaleSlope * windowSlope; + const float b = (rescaleIntercept - windowIntercept) * windowSlope; + + if (invert) + { + ShiftScaleIntegerInternal(target, source, a, b); + } + else + { + ShiftScaleIntegerInternal(target, source, a, b); + } + } + + void ImageProcessing::ApplyWindowing_Deprecated(ImageAccessor& target, + const ImageAccessor& source, + float windowCenter, + float windowWidth, + float rescaleSlope, + float rescaleIntercept, + bool invert) + { + if (target.GetWidth() != source.GetWidth() || + target.GetHeight() != source.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + switch (source.GetFormat()) + { + case PixelFormat_Float32: + { + switch (target.GetFormat()) + { + case PixelFormat_Grayscale8: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case PixelFormat_Grayscale16: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + };break; + case PixelFormat_Grayscale8: + { + switch (target.GetFormat()) + { + case PixelFormat_Grayscale8: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case PixelFormat_Grayscale16: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + };break; + case PixelFormat_Grayscale16: + { + switch (target.GetFormat()) + { + case PixelFormat_Grayscale8: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case PixelFormat_Grayscale16: + ApplyWindowingInternal(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + };break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::Convert(ImageAccessor& target, + const ImageAccessor& source) + { + if (target.GetWidth() != source.GetWidth() || + target.GetHeight() != source.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + if (source.GetFormat() == target.GetFormat()) + { + Copy(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale16 && + source.GetFormat() == PixelFormat_Grayscale8) + { + ConvertInternal(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_SignedGrayscale16 && + source.GetFormat() == PixelFormat_Grayscale8) + { + ConvertInternal(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_Grayscale16) + { + ConvertInternal(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_SignedGrayscale16 && + source.GetFormat() == PixelFormat_Grayscale16) + { + ConvertInternal(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + ConvertInternal(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale16 && + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + ConvertInternal(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_RGB24) + { + ConvertColorToGrayscale(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale16 && + source.GetFormat() == PixelFormat_RGB24) + { + ConvertColorToGrayscale(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_SignedGrayscale16 && + source.GetFormat() == PixelFormat_RGB24) + { + ConvertColorToGrayscale(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_Grayscale8) + { + ConvertGrayscaleToFloat(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_Grayscale16) + { + ConvertGrayscaleToFloat(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_Grayscale32) + { + ConvertGrayscaleToFloat(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + ConvertGrayscaleToFloat(target, source); + return; + } + + + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_RGBA32) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++, q++) + { + *q = static_cast((2126 * static_cast(p[0]) + + 7152 * static_cast(p[1]) + + 0722 * static_cast(p[2])) / 10000); + p += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_BGRA32) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++, q++) + { + *q = static_cast((2126 * static_cast(p[2]) + + 7152 * static_cast(p[1]) + + 0722 * static_cast(p[0])) / 10000); + p += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_RGBA32) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[0]; + q[1] = p[1]; + q[2] = p[2]; + p += 4; + q += 3; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_BGRA32) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[2]; + q[1] = p[1]; + q[2] = p[0]; + p += 4; + q += 3; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_RGBA32 && + source.GetFormat() == PixelFormat_RGB24) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[0]; + q[1] = p[1]; + q[2] = p[2]; + q[3] = 255; // Set the alpha channel to full opacity + p += 3; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_Grayscale8) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = *p; + q[1] = *p; + q[2] = *p; + p += 1; + q += 3; + } + } + + return; + } + + if ((target.GetFormat() == PixelFormat_RGBA32 || + target.GetFormat() == PixelFormat_BGRA32) && + source.GetFormat() == PixelFormat_Grayscale8) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = *p; + q[1] = *p; + q[2] = *p; + q[3] = 255; + p += 1; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_BGRA32 && + source.GetFormat() == PixelFormat_Grayscale16) + { + for (unsigned int y = 0; y < height; y++) + { + const uint16_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + uint8_t value = (*p < 256 ? *p : 255); + q[0] = value; + q[1] = value; + q[2] = value; + q[3] = 255; + p += 1; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_BGRA32 && + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + for (unsigned int y = 0; y < height; y++) + { + const int16_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + uint8_t value; + if (*p < 0) + { + value = 0; + } + else if (*p > 255) + { + value = 255; + } + else + { + value = static_cast(*p); + } + + q[0] = value; + q[1] = value; + q[2] = value; + q[3] = 255; + p += 1; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_BGRA32 && + source.GetFormat() == PixelFormat_RGB24) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[2]; + q[1] = p[1]; + q[2] = p[0]; + q[3] = 255; + p += 3; + q += 4; + } + } + + return; + } + + if ((target.GetFormat() == PixelFormat_BGRA32 && + source.GetFormat() == PixelFormat_RGBA32) + || (target.GetFormat() == PixelFormat_RGBA32 && + source.GetFormat() == PixelFormat_BGRA32)) + { + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[2]; + q[1] = p[1]; + q[2] = p[0]; + q[3] = p[3]; + p += 4; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_RGB48) + { + for (unsigned int y = 0; y < height; y++) + { + const uint16_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[0] >> 8; + q[1] = p[1] >> 8; + q[2] = p[2] >> 8; + p += 3; + q += 3; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale16 && + source.GetFormat() == PixelFormat_Float32) + { + ConvertFloatToGrayscale(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_Float32) + { + ConvertFloatToGrayscale(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_Float32) + { + ConvertFloatToGrayscale(target, source); + return; + } + + throw OrthancException(ErrorCode_NotImplemented); + } + + + + void ImageProcessing::Set(ImageAccessor& image, + int64_t value) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + SetInternal(image, value); + return; + + case PixelFormat_Grayscale16: + SetInternal(image, value); + return; + + case PixelFormat_Grayscale32: + SetInternal(image, value); + return; + + case PixelFormat_Grayscale64: + SetInternal(image, value); + return; + + case PixelFormat_SignedGrayscale16: + SetInternal(image, value); + return; + + case PixelFormat_Float32: + assert(sizeof(float) == 4); + SetInternal(image, value); + return; + + case PixelFormat_RGBA32: + case PixelFormat_BGRA32: + case PixelFormat_RGB24: + { + uint8_t v = static_cast(value); + Set(image, v, v, v, v); // Use the color version + return; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::Set(ImageAccessor& image, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t alpha) + { + uint8_t p[4]; + unsigned int size; + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + { + // New in Orthanc 1.9.0 + uint8_t grayscale = (2126 * static_cast(red) + + 7152 * static_cast(green) + + 0722 * static_cast(blue)) / 10000; + Orthanc::ImageProcessing::Set(image, grayscale); + return; + } + + case PixelFormat_RGBA32: + p[0] = red; + p[1] = green; + p[2] = blue; + p[3] = alpha; + size = 4; + break; + + case PixelFormat_BGRA32: + p[0] = blue; + p[1] = green; + p[2] = red; + p[3] = alpha; + size = 4; + break; + + case PixelFormat_RGB24: + p[0] = red; + p[1] = green; + p[2] = blue; + size = 3; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + uint8_t* q = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + for (unsigned int i = 0; i < size; i++) + { + q[i] = p[i]; + } + + q += size; + } + } + } + + void ImageProcessing::Set(ImageAccessor& image, + uint8_t red, + uint8_t green, + uint8_t blue, + ImageAccessor& alpha) + { + uint8_t p[4]; + + if (alpha.GetWidth() != image.GetWidth() || alpha.GetHeight() != image.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + if (alpha.GetFormat() != PixelFormat_Grayscale8) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + switch (image.GetFormat()) + { + case PixelFormat_RGBA32: + p[0] = red; + p[1] = green; + p[2] = blue; + break; + + case PixelFormat_BGRA32: + p[0] = blue; + p[1] = green; + p[2] = red; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + uint8_t* q = reinterpret_cast(image.GetRow(y)); + uint8_t* a = reinterpret_cast(alpha.GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + for (unsigned int i = 0; i < 3; i++) + { + q[i] = p[i]; + } + q[3] = *a; + q += 4; + ++a; + } + } + } + + + void ImageProcessing::ShiftRight(ImageAccessor& image, + unsigned int shift) + { + if (image.GetWidth() == 0 || + image.GetHeight() == 0 || + shift == 0) + { + // Nothing to do + return; + } + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + { + ShiftRightInternal(image, shift); + break; + } + + case PixelFormat_Grayscale16: + { + ShiftRightInternal(image, shift); + break; + } + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void ImageProcessing::ShiftLeft(ImageAccessor& image, + unsigned int shift) + { + if (image.GetWidth() == 0 || + image.GetHeight() == 0 || + shift == 0) + { + // Nothing to do + return; + } + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + { + ShiftLeftInternal(image, shift); + break; + } + + case PixelFormat_Grayscale16: + { + ShiftLeftInternal(image, shift); + break; + } + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void ImageProcessing::GetMinMaxIntegerValue(int64_t& minValue, + int64_t& maxValue, + const ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + { + uint8_t a, b; + GetMinMaxValueInternal(a, b, image); + minValue = a; + maxValue = b; + break; + } + + case PixelFormat_Grayscale16: + { + uint16_t a, b; + GetMinMaxValueInternal(a, b, image); + minValue = a; + maxValue = b; + break; + } + + case PixelFormat_Grayscale32: + { + uint32_t a, b; + GetMinMaxValueInternal(a, b, image); + minValue = a; + maxValue = b; + break; + } + + case PixelFormat_SignedGrayscale16: + { + int16_t a, b; + GetMinMaxValueInternal(a, b, image); + minValue = a; + maxValue = b; + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::GetMinMaxFloatValue(float& minValue, + float& maxValue, + const ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Float32: + { + assert(sizeof(float) == 4); + float a, b; + + /** + * WARNING - On floating-point types, the minimal value is + * "-FLT_MAX" (as implemented by "::lowest()"), not "FLT_MIN" + * (as implemented by "::min()") + * https://en.cppreference.com/w/cpp/types/numeric_limits + **/ + GetMinMaxValueInternal(a, b, image, -std::numeric_limits::max()); + minValue = a; + maxValue = b; + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + + void ImageProcessing::AddConstant(ImageAccessor& image, + int64_t value) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + AddConstantInternal(image, value); + return; + + case PixelFormat_Grayscale16: + AddConstantInternal(image, value); + return; + + case PixelFormat_SignedGrayscale16: + AddConstantInternal(image, value); + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::MultiplyConstant(ImageAccessor& image, + float factor, + bool useRound) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + if (useRound) + { + MultiplyConstantInternal(image, factor); + } + else + { + MultiplyConstantInternal(image, factor); + } + return; + + case PixelFormat_Grayscale16: + if (useRound) + { + MultiplyConstantInternal(image, factor); + } + else + { + MultiplyConstantInternal(image, factor); + } + return; + + case PixelFormat_SignedGrayscale16: + if (useRound) + { + MultiplyConstantInternal(image, factor); + } + else + { + MultiplyConstantInternal(image, factor); + } + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + static bool IsIdentityRescaling(float offset, + float scaling) + { + return (std::abs(offset) <= 10.0f * std::numeric_limits::epsilon() && + std::abs(scaling - 1.0f) <= 10.0f * std::numeric_limits::epsilon()); + } + + + void ImageProcessing::ShiftScale2(ImageAccessor& image, + float offset, + float scaling, + bool useRound) + { + // We compute "a * x + b" + const float a = scaling; + const float b = offset; + + if (IsIdentityRescaling(offset, scaling)) + { + return; + } + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + if (useRound) + { + ShiftScaleIntegerInternal(image, image, a, b); + } + else + { + ShiftScaleIntegerInternal(image, image, a, b); + } + return; + + case PixelFormat_Grayscale16: + if (useRound) + { + ShiftScaleIntegerInternal(image, image, a, b); + } + else + { + ShiftScaleIntegerInternal(image, image, a, b); + } + return; + + case PixelFormat_SignedGrayscale16: + if (useRound) + { + ShiftScaleIntegerInternal(image, image, a, b); + } + else + { + ShiftScaleIntegerInternal(image, image, a, b); + } + return; + + case PixelFormat_Float32: + if (useRound) + { + ShiftScaleFloatInternal(image, image, a, b); + } + else + { + ShiftScaleFloatInternal(image, image, a, b); + } + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::ShiftScale2(ImageAccessor& target, + const ImageAccessor& source, + float offset, + float scaling, + bool useRound) + { + // We compute "a * x + b" + const float a = scaling; + const float b = offset; + + if (target.GetFormat() == source.GetFormat() && + IsIdentityRescaling(offset, scaling)) + { + Copy(target, source); + return; + } + + switch (target.GetFormat()) + { + case PixelFormat_Grayscale8: + + switch (source.GetFormat()) + { + case PixelFormat_Grayscale8: + if (useRound) + { + ShiftScaleIntegerInternal(target, source, a, b); + } + else + { + ShiftScaleIntegerInternal(target, source, a, b); + } + return; + + case PixelFormat_Grayscale16: + if (useRound) + { + ShiftScaleIntegerInternal(target, source, a, b); + } + else + { + ShiftScaleIntegerInternal(target, source, a, b); + } + return; + + case PixelFormat_SignedGrayscale16: + if (useRound) + { + ShiftScaleIntegerInternal(target, source, a, b); + } + else + { + ShiftScaleIntegerInternal(target, source, a, b); + } + return; + + case PixelFormat_Float32: + if (useRound) + { + ShiftScaleIntegerInternal(target, source, a, b); + } + else + { + ShiftScaleIntegerInternal(target, source, a, b); + } + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::ShiftScale(ImageAccessor& image, + float offset, + float scaling, + bool useRound) + { + // Rewrite "(x + offset) * scaling" as "a * x + b" + + const float a = scaling; + const float b = offset * scaling; + ShiftScale2(image, b, a, useRound); + } + + + void ImageProcessing::ShiftScale(ImageAccessor& target, + const ImageAccessor& source, + float offset, + float scaling, + bool useRound) + { + // Rewrite "(x + offset) * scaling" as "a * x + b" + + const float a = scaling; + const float b = offset * scaling; + ShiftScale2(target, source, b, a, useRound); + } + + + + void ImageProcessing::Invert(ImageAccessor& image, int64_t maxValue) + { + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale16: + { + uint16_t maxValueUint16 = (uint16_t)(std::min(maxValue, static_cast(std::numeric_limits::max()))); + + for (unsigned int y = 0; y < height; y++) + { + uint16_t* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + *p = maxValueUint16 - (*p); + } + } + + return; + } + case PixelFormat_Grayscale8: + { + uint8_t maxValueUint8 = (uint8_t)(std::min(maxValue, static_cast(std::numeric_limits::max()))); + + for (unsigned int y = 0; y < height; y++) + { + uint8_t* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p++) + { + *p = maxValueUint8 - (*p); + } + } + + return; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + } + + void ImageProcessing::Invert(ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + return Invert(image, 255); + default: + throw OrthancException(ErrorCode_NotImplemented); // you should use the Invert(image, maxValue) overload + } + } + + + + namespace + { + template + class BresenhamPixelWriter + { + private: + typedef typename PixelTraits::PixelType PixelType; + + ImageAccessor& image_; + PixelType value_; + + void PlotLineLow(int x0, + int y0, + int x1, + int y1) + { + int dx = x1 - x0; + int dy = y1 - y0; + int yi = 1; + + if (dy < 0) + { + yi = -1; + dy = -dy; + } + + int d = 2 * dy - dx; + int y = y0; + + for (int x = x0; x <= x1; x++) + { + Write(x, y); + + if (d > 0) + { + y = y + yi; + d = d - 2 * dx; + } + + d = d + 2*dy; + } + } + + void PlotLineHigh(int x0, + int y0, + int x1, + int y1) + { + int dx = x1 - x0; + int dy = y1 - y0; + int xi = 1; + + if (dx < 0) + { + xi = -1; + dx = -dx; + } + + int d = 2 * dx - dy; + int x = x0; + + for (int y = y0; y <= y1; y++) + { + Write(x, y); + + if (d > 0) + { + x = x + xi; + d = d - 2 * dy; + } + + d = d + 2 * dx; + } + } + + public: + BresenhamPixelWriter(ImageAccessor& image, + int64_t value) : + image_(image), + value_(PixelTraits::IntegerToPixel(value)) + { + } + + BresenhamPixelWriter(ImageAccessor& image, + const PixelType& value) : + image_(image), + value_(value) + { + } + + void Write(int x, + int y) + { + if (x >= 0 && + y >= 0 && + static_cast(x) < image_.GetWidth() && + static_cast(y) < image_.GetHeight()) + { + PixelType* p = reinterpret_cast(image_.GetRow(y)); + p[x] = value_; + } + } + + void DrawSegment(int x0, + int y0, + int x1, + int y1) + { + // This is an implementation of Bresenham's line algorithm + // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#All_cases + + if (abs(y1 - y0) < abs(x1 - x0)) + { + if (x0 > x1) + { + PlotLineLow(x1, y1, x0, y0); + } + else + { + PlotLineLow(x0, y0, x1, y1); + } + } + else + { + if (y0 > y1) + { + PlotLineHigh(x1, y1, x0, y0); + } + else + { + PlotLineHigh(x0, y0, x1, y1); + } + } + } + }; + } + + + void ImageProcessing::DrawLineSegment(ImageAccessor& image, + int x0, + int y0, + int x1, + int y1, + int64_t value) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + { + BresenhamPixelWriter writer(image, value); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case PixelFormat_Grayscale16: + { + BresenhamPixelWriter writer(image, value); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case PixelFormat_SignedGrayscale16: + { + BresenhamPixelWriter writer(image, value); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::DrawLineSegment(ImageAccessor& image, + int x0, + int y0, + int x1, + int y1, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t alpha) + { + switch (image.GetFormat()) + { + case PixelFormat_BGRA32: + { + PixelTraits::PixelType pixel; + pixel.red_ = red; + pixel.green_ = green; + pixel.blue_ = blue; + pixel.alpha_ = alpha; + + BresenhamPixelWriter writer(image, pixel); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case PixelFormat_RGBA32: + { + PixelTraits::PixelType pixel; + pixel.red_ = red; + pixel.green_ = green; + pixel.blue_ = blue; + pixel.alpha_ = alpha; + + BresenhamPixelWriter writer(image, pixel); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case PixelFormat_RGB24: + { + PixelTraits::PixelType pixel; + pixel.red_ = red; + pixel.green_ = green; + pixel.blue_ = blue; + + BresenhamPixelWriter writer(image, pixel); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void ComputePolygonExtent(int32_t& left, int32_t& right, int32_t& top, int32_t& bottom, const std::vector& points) + { + left = std::numeric_limits::max(); + right = std::numeric_limits::min(); + top = std::numeric_limits::max(); + bottom = std::numeric_limits::min(); + + for (size_t i = 0; i < points.size(); i++) + { + const ImageProcessing::ImagePoint& p = points[i]; + left = std::min(p.GetX(), left); + right = std::max(p.GetX(), right); + bottom = std::max(p.GetY(), bottom); + top = std::min(p.GetY(), top); + } + } + + + namespace + { +#define USE_POLYGON_FRACTIONS 1 + + class PolygonEdge + { + private: + int yUpper; + +#if USE_POLYGON_FRACTIONS == 1 + int x; + int xOffset; + int dxPerScanNumerator; + int dxPerScanDenominator; +#else + float xIntersect; + float dxPerScan; +#endif + + public: + PolygonEdge(const ImageProcessing::ImagePoint& lower, + const ImageProcessing::ImagePoint& upper, + int yComp) + { + // cf. "makeEdgeRec()" in textbook + + assert(upper.GetY() != lower.GetY()); + +#if USE_POLYGON_FRACTIONS == 1 + x = lower.GetX(); + xOffset = 0; + dxPerScanNumerator = upper.GetX() - lower.GetX(); + dxPerScanDenominator = upper.GetY() - lower.GetY(); +#else + dxPerScan = (static_cast(upper.GetX() - lower.GetX()) / + static_cast(upper.GetY() - lower.GetY())); + xIntersect = lower.GetX(); +#endif + + if (upper.GetY() < yComp) + { + yUpper = upper.GetY() - 1; + } + else + { + yUpper = upper.GetY(); + } + } + + void NextScanLine() + { +#if USE_POLYGON_FRACTIONS == 1 + xOffset += dxPerScanNumerator; + + while (xOffset >= dxPerScanDenominator) + { + x++; + xOffset -= dxPerScanDenominator; + } + + while (xOffset < 0) + { + x--; + xOffset += dxPerScanDenominator; + } + +#else + xIntersect += dxPerScan; +#endif + } + + + int GetEnterX() const + { +#if USE_POLYGON_FRACTIONS == 1 + assert(xOffset >= 0 && xOffset < dxPerScanDenominator); + if (xOffset == 0) + { + return x; + } + else + { + return x + 1; + } +#else + return static_cast(std::ceil(xIntersect)); +#endif + } + + int GetExitX() const + { +#if USE_POLYGON_FRACTIONS == 1 + assert(xOffset >= 0 && xOffset < dxPerScanDenominator); + return x; +#else + return static_cast(std::floor(xIntersect)); +#endif + } + + int GetUpperY() const + { + return yUpper; + } + + bool operator< (const PolygonEdge& other) const + { +#if USE_POLYGON_FRACTIONS == 1 + assert(xOffset >= 0 && xOffset < dxPerScanDenominator); + assert(other.xOffset >= 0 && other.xOffset < other.dxPerScanDenominator); + return x < other.x; +#else + // cf. "insertEdge()" in textbook + return (xIntersect < other.xIntersect); +#endif + } + }; + } + + + // For an index, return y-coordinate of next nonhorizontal line + static int GetPolygonNextY(const std::vector& points, + size_t k) + { + // cf. "yNext()" in textbook + size_t j = k; + + for (;;) + { + j++; + if (j == points.size()) + { + j = 0; + } + + if (points[k].GetY() != points[j].GetY()) + { + return points[j].GetY(); + } + } + } + + + static int GetPolygonPreviousY(const std::vector& points, + size_t k) + { + size_t j = k; + + for (;;) + { + if (j > 0) + { + j --; + } + else + { + j = points.size() - 1; + } + + if (points[k].GetY() != points[j].GetY()) + { + return points[j].GetY(); + } + } + } + + + + void ImageProcessing::FillPolygon(IPolygonFiller& filler, + const std::vector& points) + { + /** + * This implementation is a C++ adaption of Section 3.11 (pages + * 117-124) of textbook "Computer Graphics - C Version (2nd + * Edition)" by Hearn and Baker, 1997. + **/ + + typedef std::map > EdgeTable; + + if (points.size() < 2) + { + return; + } + + bool onlyHorizontalSegments = true; + for (size_t i = 1; i < points.size(); i++) + { + if (points[0].GetY() != points[i].GetY()) + { + onlyHorizontalSegments = false; + break; + } + } + + if (onlyHorizontalSegments) + { + // Degenerate case: There are only horizontal lines. If this is + // the case, "GetPolygonPreviousY()" would be an infinite loop + int x1 = points[0].GetX(); + int x2 = x1; + for (size_t i = 1; i < points.size(); i++) + { + assert(points[i].GetY() == points[0].GetY()); + + const int x = points[i].GetX(); + x1 = std::min(x1, x); + x2 = std::max(x2, x); + } + filler.Fill(points[0].GetY(), x1, x2); + return; + } + + EdgeTable globalEdgeTable; + + // cf. "buildEdgeList()" in textbook + + // Error in the textbook: we use "GetPolygonPreviousY()" instead of "points.size() - 2" + int yPrev = GetPolygonPreviousY(points, points.size() - 1); + ImagePoint v1(points[points.size() - 1]); + + for (size_t i = 0; i < points.size(); i++) + { + ImagePoint v2(points[i]); + + if (v1.GetY() != v2.GetY()) + { + // Non-horizontal line + if (v1.GetY() < v2.GetY()) + { + // Up-going edge + PolygonEdge edge(v1, v2, GetPolygonNextY(points, i)); + globalEdgeTable[v1.GetY()].push_back(edge); + } + else if (v1.GetY() > v2.GetY()) + { + // Down-going edge + PolygonEdge edge(v2, v1, yPrev); + globalEdgeTable[v2.GetY()].push_back(edge); + } + + // Error in the textbook: "yPrev" must NOT be updated on horizontal lines + yPrev = v1.GetY(); + } + + v1 = v2; + } + + assert(!globalEdgeTable.empty()); + + std::vector activeEdges; + + for (EdgeTable::const_iterator it = globalEdgeTable.begin(); it != globalEdgeTable.end(); ++it) + { + // cf. "buildActiveList()" in textbook + activeEdges.reserve(activeEdges.size() + it->second.size()); + for (std::list::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) + { + activeEdges.push_back(*it2); + } + + assert(!activeEdges.empty()); + + EdgeTable::const_iterator next = it; + ++next; + + int rampEnd; + if (next == globalEdgeTable.end()) + { + rampEnd = activeEdges[0].GetUpperY() + 1; + + for (size_t i = 1; i < activeEdges.size(); i++) + { + rampEnd = std::max(rampEnd, activeEdges[i].GetUpperY() + 1); + } + } + else + { + rampEnd = next->first; + } + + for (int y = it->first; y < rampEnd; y++) + { + // cf. "updateActiveList()" in textbook + std::vector stillActive; + stillActive.reserve(activeEdges.size()); + + for (size_t i = 0; i < activeEdges.size(); i++) + { + if (y <= activeEdges[i].GetUpperY()) + { + stillActive.push_back(activeEdges[i]); + } + } + + activeEdges.swap(stillActive); + + assert(activeEdges.size() % 2 == 0); + std::sort(activeEdges.begin(), activeEdges.end()); + + // cf. "fillScan()" in textbook + for (size_t k = 0; k + 1 < activeEdges.size(); ) + { + int a = activeEdges[k].GetExitX(); + int b = activeEdges[k + 1].GetEnterX(); + + // Fix wrt. the textbook: merge overlapping segments + k += 2; + while (k + 1 < activeEdges.size() && + activeEdges[k].GetExitX() == b) + { + assert(a <= b); + b = activeEdges[k + 1].GetEnterX(); + k += 2; + } + + assert(a <= b); + filler.Fill(y, a, b); + } + + // cf. "updateActiveList()" in textbook + for (size_t k = 0; k < activeEdges.size(); k++) + { + activeEdges[k].NextScanLine(); + } + } + } + } + + + void ImageProcessing::FillPolygon(ImageAccessor& image, + const std::vector& points, + int64_t value) + { + class Filler : public IPolygonFiller + { + private: + ImageAccessor& image_; + int64_t value_; + + public: + Filler(ImageAccessor& image, + int64_t value) : + image_(image), + value_(value) + { + } + + virtual void Fill(int y, + int x1, + int x2) ORTHANC_OVERRIDE + { + assert(x1 <= x2); + + if (x1 < static_cast(image_.GetWidth()) && + x2 >= 0 && + y >= 0 && + y < static_cast(image_.GetHeight())) + { + unsigned int yy = static_cast(y); + unsigned int a = static_cast(std::max(0, x1)); + unsigned int b = static_cast(std::min(x2, static_cast(image_.GetWidth()) - 1)); + + assert(a <= b); + + ImageAccessor region; + image_.GetRegion(region, a, yy, b - a + 1, 1); + Set(region, value_); + } + } + }; + + + if (image.GetFormat() == PixelFormat_Grayscale8 || + image.GetFormat() == PixelFormat_Grayscale16 || + image.GetFormat() == PixelFormat_SignedGrayscale16) + { + Filler filler(image, value); + FillPolygon(filler, points); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + template + static void ResizeInternal(ImageAccessor& target, + const ImageAccessor& source) + { + assert(target.GetFormat() == source.GetFormat() && + target.GetFormat() == Format); + + const unsigned int sourceWidth = source.GetWidth(); + const unsigned int sourceHeight = source.GetHeight(); + const unsigned int targetWidth = target.GetWidth(); + const unsigned int targetHeight = target.GetHeight(); + + if (targetWidth == 0 || targetHeight == 0) + { + return; + } + + if (sourceWidth == 0 || sourceHeight == 0) + { + // Avoids division by zero below + ImageProcessing::Set(target, 0); + return; + } + + const float scaleX = static_cast(sourceWidth) / static_cast(targetWidth); + const float scaleY = static_cast(sourceHeight) / static_cast(targetHeight); + + + /** + * Create two lookup tables to quickly know the (x,y) position + * in the source image, given the (x,y) position in the target + * image. + **/ + + std::vector lookupX(targetWidth); + + for (unsigned int x = 0; x < targetWidth; x++) + { + int sourceX = static_cast(std::floor((static_cast(x) + 0.5f) * scaleX)); + if (sourceX < 0) + { + sourceX = 0; // Should never happen + } + else if (sourceX >= static_cast(sourceWidth)) + { + sourceX = sourceWidth - 1; + } + + lookupX[x] = static_cast(sourceX); + } + + std::vector lookupY(targetHeight); + + for (unsigned int y = 0; y < targetHeight; y++) + { + int sourceY = static_cast(std::floor((static_cast(y) + 0.5f) * scaleY)); + if (sourceY < 0) + { + sourceY = 0; // Should never happen + } + else if (sourceY >= static_cast(sourceHeight)) + { + sourceY = sourceHeight - 1; + } + + lookupY[y] = static_cast(sourceY); + } + + + /** + * Actual resizing + **/ + + for (unsigned int targetY = 0; targetY < targetHeight; targetY++) + { + unsigned int sourceY = lookupY[targetY]; + + for (unsigned int targetX = 0; targetX < targetWidth; targetX++) + { + unsigned int sourceX = lookupX[targetX]; + + typename ImageTraits::PixelType pixel; + ImageTraits::GetPixel(pixel, source, sourceX, sourceY); + ImageTraits::SetPixel(target, pixel, targetX, targetY); + } + } + } + + + + void ImageProcessing::Resize(ImageAccessor& target, + const ImageAccessor& source) + { + if (source.GetFormat() != target.GetFormat()) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + if (source.GetWidth() == target.GetWidth() && + source.GetHeight() == target.GetHeight()) + { + Copy(target, source); + return; + } + + switch (source.GetFormat()) + { + case PixelFormat_Grayscale8: + ResizeInternal(target, source); + break; + + case PixelFormat_Float32: + ResizeInternal(target, source); + break; + + case PixelFormat_RGB24: + ResizeInternal(target, source); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + ImageAccessor* ImageProcessing::Halve(const ImageAccessor& source, + bool forceMinimalPitch) + { + std::unique_ptr target(new Image(source.GetFormat(), source.GetWidth() / 2, + source.GetHeight() / 2, forceMinimalPitch)); + Resize(*target, source); + return target.release(); + } + + + template + static void FlipXInternal(ImageAccessor& image) + { + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int x1 = 0; x1 < width / 2; x1++) + { + unsigned int x2 = width - 1 - x1; + + typename ImageTraits::PixelType a, b; + ImageTraits::GetPixel(a, image, x1, y); + ImageTraits::GetPixel(b, image, x2, y); + ImageTraits::SetPixel(image, a, x2, y); + ImageTraits::SetPixel(image, b, x1, y); + } + } + } + + + void ImageProcessing::FlipX(ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + FlipXInternal(image); + break; + + case PixelFormat_RGB24: + FlipXInternal(image); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + template + static void FlipYInternal(ImageAccessor& image) + { + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + + for (unsigned int y1 = 0; y1 < height / 2; y1++) + { + unsigned int y2 = height - 1 - y1; + + for (unsigned int x = 0; x < width; x++) + { + typename ImageTraits::PixelType a, b; + ImageTraits::GetPixel(a, image, x, y1); + ImageTraits::GetPixel(b, image, x, y2); + ImageTraits::SetPixel(image, a, x, y2); + ImageTraits::SetPixel(image, b, x, y1); + } + } + } + + + void ImageProcessing::FlipY(ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + FlipYInternal(image); + break; + + case PixelFormat_RGB24: + FlipYInternal(image); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + // This is a slow implementation of horizontal convolution on one + // individual channel, that checks for out-of-image values + template + static float GetHorizontalConvolutionFloatSecure(const ImageAccessor& source, + const std::vector& horizontal, + size_t horizontalAnchor, + unsigned int x, + unsigned int y, + float leftBorder, + float rightBorder, + unsigned int channel) + { + const RawPixel* row = reinterpret_cast(source.GetConstRow(y)) + channel; + + float p = 0; + + for (unsigned int k = 0; k < horizontal.size(); k++) + { + float value; + + if (x + k < horizontalAnchor) // Negation of "x - horizontalAnchor + k >= 0" + { + value = leftBorder; + } + else if (x + k >= source.GetWidth() + horizontalAnchor) // Negation of "x - horizontalAnchor + k < width" + { + value = rightBorder; + } + else + { + // The value lies within the image + value = row[(x - horizontalAnchor + k) * ChannelsCount]; + } + + p += value * horizontal[k]; + } + + return p; + } + + + + // This is an implementation of separable convolution that uses + // floating-point arithmetics, and an intermediate Float32 + // image. The out-of-image values are taken as the border + // value. Further optimization is possible. + template + static void SeparableConvolutionFloat(ImageAccessor& image /* inplace */, + const std::vector& horizontal, + size_t horizontalAnchor, + const std::vector& vertical, + size_t verticalAnchor, + float normalization) + { + // WARNING - "::min()" should be replaced by "::lowest()" if + // dealing with float or double (which is not the case so far) + assert(sizeof(RawPixel) <= 2); // Safeguard to remember about "float/double" + + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + + /** + * Horizontal convolution + **/ + + Image tmp(PixelFormat_Float32, ChannelsCount * width, height, false); + + for (unsigned int y = 0; y < height; y++) + { + const RawPixel* row = reinterpret_cast(image.GetConstRow(y)); + + float leftBorder[ChannelsCount], rightBorder[ChannelsCount]; + + for (unsigned int c = 0; c < ChannelsCount; c++) + { + leftBorder[c] = row[c]; + rightBorder[c] = row[ChannelsCount * (width - 1) + c]; + } + + float* p = static_cast(tmp.GetRow(y)); + + if (width < horizontal.size()) + { + // It is not possible to have the full kernel within the image, use the direct implementation + for (unsigned int x = 0; x < width; x++) + { + for (unsigned int c = 0; c < ChannelsCount; c++, p++) + { + *p = GetHorizontalConvolutionFloatSecure + (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c); + } + } + } + else + { + // Deal with the left border + for (unsigned int x = 0; x < horizontalAnchor; x++) + { + for (unsigned int c = 0; c < ChannelsCount; c++, p++) + { + *p = GetHorizontalConvolutionFloatSecure + (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c); + } + } + + // Deal with the central portion of the image (all pixel values + // scanned by the kernel lie inside the image) + + for (unsigned int x = 0; x < width - horizontal.size() + 1; x++) + { + for (unsigned int c = 0; c < ChannelsCount; c++, p++) + { + *p = 0; + for (unsigned int k = 0; k < horizontal.size(); k++) + { + *p += static_cast(row[(x + k) * ChannelsCount + c]) * horizontal[k]; + } + } + } + + // Deal with the right border + for (unsigned int x = static_cast( + horizontalAnchor + width - horizontal.size() + 1); x < width; x++) + { + for (unsigned int c = 0; c < ChannelsCount; c++, p++) + { + *p = GetHorizontalConvolutionFloatSecure + (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c); + } + } + } + } + + + /** + * Vertical convolution + **/ + + std::vector rows(vertical.size()); + + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int k = 0; k < vertical.size(); k++) + { + if (y + k < verticalAnchor) + { + rows[k] = reinterpret_cast(tmp.GetConstRow(0)); // Use top border + } + else if (y + k >= height + verticalAnchor) + { + rows[k] = reinterpret_cast(tmp.GetConstRow(height - 1)); // Use bottom border + } + else + { + rows[k] = reinterpret_cast(tmp.GetConstRow(static_cast(y + k - verticalAnchor))); + } + } + + RawPixel* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + for (unsigned int c = 0; c < ChannelsCount; c++, p++) + { + float accumulator = 0; + + for (unsigned int k = 0; k < vertical.size(); k++) + { + accumulator += rows[k][ChannelsCount * x + c] * vertical[k]; + } + + accumulator *= normalization; + + if (accumulator <= static_cast(std::numeric_limits::min())) + { + *p = std::numeric_limits::min(); + } + else if (accumulator >= static_cast(std::numeric_limits::max())) + { + *p = std::numeric_limits::max(); + } + else + { + if (UseRound) + { + assert(sizeof(RawPixel) < sizeof(int)); + *p = static_cast(boost::math::iround(accumulator)); + } + else + { + *p = static_cast(accumulator); + } + } + } + } + } + } + + + void ImageProcessing::SeparableConvolution(ImageAccessor& image /* inplace */, + const std::vector& horizontal, + size_t horizontalAnchor, + const std::vector& vertical, + size_t verticalAnchor, + bool useRound) + { + if (horizontal.size() == 0 || + vertical.size() == 0 || + horizontalAnchor >= horizontal.size() || + verticalAnchor >= vertical.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (image.GetWidth() == 0 || + image.GetHeight() == 0) + { + return; + } + + /** + * Compute normalization + **/ + + float sumHorizontal = 0; + for (size_t i = 0; i < horizontal.size(); i++) + { + sumHorizontal += horizontal[i]; + } + + float sumVertical = 0; + for (size_t i = 0; i < vertical.size(); i++) + { + sumVertical += vertical[i]; + } + + if (fabsf(sumHorizontal) <= std::numeric_limits::epsilon() || + fabsf(sumVertical) <= std::numeric_limits::epsilon()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Singular convolution kernel"); + } + + const float normalization = 1.0f / (sumHorizontal * sumVertical); + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + if (useRound) + { + SeparableConvolutionFloat + (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization); + } + else + { + SeparableConvolutionFloat + (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization); + } + break; + + case PixelFormat_RGB24: + if (useRound) + { + SeparableConvolutionFloat + (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization); + } + else + { + SeparableConvolutionFloat + (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization); + } + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::SmoothGaussian5x5(ImageAccessor& image, + bool useRound) + { + std::vector kernel(5); + kernel[0] = 1; + kernel[1] = 4; + kernel[2] = 6; + kernel[3] = 4; + kernel[4] = 1; + + SeparableConvolution(image, kernel, 2, kernel, 2, useRound); + } + + + void ImageProcessing::FitSize(ImageAccessor& target, + const ImageAccessor& source) + { + if (target.GetWidth() == 0 || + target.GetHeight() == 0) + { + return; + } + + if (source.GetWidth() == target.GetWidth() && + source.GetHeight() == target.GetHeight()) + { + Copy(target, source); + return; + } + + Set(target, 0); + + // Preserve the aspect ratio + float cw = static_cast(source.GetWidth()); + float ch = static_cast(source.GetHeight()); + float r = std::min( + static_cast(target.GetWidth()) / cw, + static_cast(target.GetHeight()) / ch); + + unsigned int sw = std::min(static_cast(boost::math::iround(cw * r)), target.GetWidth()); + unsigned int sh = std::min(static_cast(boost::math::iround(ch * r)), target.GetHeight()); + + Image resized(target.GetFormat(), sw, sh, false); + + //ImageProcessing::SmoothGaussian5x5(source); + ImageProcessing::Resize(resized, source); + + assert(target.GetWidth() >= resized.GetWidth() && + target.GetHeight() >= resized.GetHeight()); + unsigned int offsetX = (target.GetWidth() - resized.GetWidth()) / 2; + unsigned int offsetY = (target.GetHeight() - resized.GetHeight()) / 2; + + ImageAccessor region; + target.GetRegion(region, offsetX, offsetY, resized.GetWidth(), resized.GetHeight()); + ImageProcessing::Copy(region, resized); + } + + + ImageAccessor* ImageProcessing::FitSize(const ImageAccessor& source, + unsigned int width, + unsigned int height) + { + std::unique_ptr target(new Image(source.GetFormat(), width, height, false)); + FitSize(*target, source); + return target.release(); + } + + + ImageAccessor* ImageProcessing::FitSizeKeepAspectRatio(const ImageAccessor& source, + unsigned int width, + unsigned int height) + { + std::unique_ptr target(new Image(source.GetFormat(), width, height, false)); + Set(*target, 0); + + if (width != 0 && + height != 0 && + source.GetWidth() != 0 && + source.GetHeight() != 0) + { + float ratio = std::min(static_cast(width) / static_cast(source.GetWidth()), + static_cast(height) / static_cast(source.GetHeight())); + + unsigned int resizedWidth = static_cast( + boost::math::iround(ratio * static_cast(source.GetWidth()))); + + unsigned int resizedHeight = static_cast( + boost::math::iround(ratio * static_cast(source.GetHeight()))); + + std::unique_ptr resized(FitSize(source, resizedWidth, resizedHeight)); + + ImageAccessor region; + target->GetRegion(region, (width - resizedWidth) / 2, + (height - resizedHeight) / 2, resizedWidth, resizedHeight); + Copy(region, *resized); + } + + return target.release(); + } + + + void ImageProcessing::ConvertJpegYCbCrToRgb(ImageAccessor& image) + { + // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2 + // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion + + // TODO - Check out the outcome of Mathieu's discussion about + // truncation of YCbCr-to-RGB conversion: + // https://groups.google.com/forum/#!msg/comp.protocols.dicom/JHuGeyWbTz8/ARoTWrJzAQAJ + + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + const unsigned int pitch = image.GetPitch(); + + if (image.GetFormat() != PixelFormat_RGB24 || + pitch < 3 * width) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + for (unsigned int y = 0; y < height; y++) + { + uint8_t* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < width; x++, p += 3) + { + const float Y = p[0]; + const float Cb = p[1]; + const float Cr = p[2]; + + const float result[3] = { + Y + 1.402f * (Cr - 128.0f), + Y - 0.344136f * (Cb - 128.0f) - 0.714136f * (Cr - 128.0f), + Y + 1.772f * (Cb - 128.0f) + }; + + for (uint8_t i = 0; i < 3 ; i++) + { + if (result[i] < 0) + { + p[i] = 0; + } + else if (result[i] > 255) + { + p[i] = 255; + } + else + { + p[i] = static_cast(result[i]); + } + } + } + } + } + + + void ImageProcessing::SwapEndianness(ImageAccessor& image /* inplace */) + { + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + case PixelFormat_RGB24: + case PixelFormat_RGBA32: + case PixelFormat_BGRA32: + // No swapping required + break; + + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + for (unsigned int y = 0; y < height; y++) + { + uint8_t* t = reinterpret_cast(image.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + uint8_t a = t[0]; + t[0] = t[1]; + t[1] = a; + t += 2; + } + } + break; + + case PixelFormat_Grayscale32: + case PixelFormat_Float32: + for (unsigned int y = 0; y < height; y++) + { + uint8_t* t = reinterpret_cast(image.GetRow(y)); + for (unsigned int x = 0; x < width; x++) + { + uint8_t a = t[0]; + uint8_t b = t[1]; + t[0] = t[3]; + t[1] = t[2]; + t[2] = b; + t[3] = a; + t += 4; + } + } + break; + + case PixelFormat_RGB48: // uint16_t per channel + for (unsigned int y = 0; y < height; y++) + { + uint8_t* t = reinterpret_cast(image.GetRow(y)); + for (unsigned int x = 0; x < 3 * width; x++) + { + uint8_t a = t[0]; + t[0] = t[1]; + t[1] = a; + t += 2; + } + } + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + template + static void ApplyImageOntoImage(Functor f, + ImageAccessor& image /* inout */, + const ImageAccessor& other) + { + const unsigned int width = image.GetWidth(); + const unsigned int height = image.GetHeight(); + + if (width != other.GetWidth() || + height != other.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + else if (image.GetFormat() != other.GetFormat() || + GetBytesPerPixel(image.GetFormat()) != sizeof(PixelType)) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + else + { + for (unsigned int y = 0; y < height; y++) + { + PixelType* p = reinterpret_cast(image.GetRow(y)); + const PixelType* q = reinterpret_cast(other.GetConstRow(y)); + + for (unsigned int x = 0; x < width; x++, p++, q++) + { + f(*p, *q); + } + } + } + } + + + namespace + { + // For older version of gcc, templated functors cannot be defined + // as types internal to functions, hence the anonymous namespace + + struct MaximumFunctor + { + void operator() (uint8_t& a, const uint8_t& b) + { + a = std::max(a, b); + } + + void operator() (uint16_t& a, const uint16_t& b) + { + a = std::max(a, b); + } + }; + } + + + void ImageProcessing::Maximum(ImageAccessor& image, + const ImageAccessor& other) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + ApplyImageOntoImage(MaximumFunctor(), image, other); + return; + + case PixelFormat_Grayscale16: + ApplyImageOntoImage(MaximumFunctor(), image, other); + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::Render(ImageAccessor& target, + const DicomImageInformation& info, + const ImageAccessor& source, + const Window& window) + { + if (source.GetFormat() == PixelFormat_RGB24) + { + Copy(target, source); + } + else if (source.GetFormat() == PixelFormat_Grayscale8 || + source.GetFormat() == PixelFormat_Grayscale16 || + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + if (target.GetFormat() != PixelFormat_Grayscale8) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + double offset, scaling; + info.ComputeRenderingTransform(offset, scaling); + ShiftScale2(target, source, static_cast(offset), static_cast(scaling), false); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ImageProcessing::RenderDefaultWindow(ImageAccessor& target, + const DicomImageInformation& info, + const ImageAccessor& source) + { + if (source.GetFormat() == PixelFormat_RGB24) + { + Copy(target, source); + } + else if (source.GetFormat() == PixelFormat_Grayscale8 || + source.GetFormat() == PixelFormat_Grayscale16 || + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + if (target.GetFormat() != PixelFormat_Grayscale8) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + double offset, scaling; + if (info.HasWindows()) + { + info.ComputeRenderingTransform(offset, scaling); // Use the default windowing + } + else + { + // Use the full dynamic range of the image + int64_t minValue, maxValue; + GetMinMaxIntegerValue(minValue, maxValue, source); + double minRescaled = info.ApplyRescale(minValue); + double maxRescaled = info.ApplyRescale(maxValue); + info.ComputeRenderingTransform(offset, scaling, Window::FromBounds(minRescaled, maxRescaled)); + } + + ShiftScale2(target, source, static_cast(offset), static_cast(scaling), false); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } +} diff --git a/OrthancFramework/Sources/Images/ImageProcessing.h b/OrthancFramework/Sources/Images/ImageProcessing.h new file mode 100644 index 0000000..82da683 --- /dev/null +++ b/OrthancFramework/Sources/Images/ImageProcessing.h @@ -0,0 +1,236 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../OrthancFramework.h" + +#include "../DicomFormat/DicomImageInformation.h" +#include "ImageAccessor.h" + +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC ImageProcessing : public boost::noncopyable + { + public: + class ORTHANC_PUBLIC ImagePoint + { + private: + int32_t x_; + int32_t y_; + + public: + ImagePoint(int32_t x, + int32_t y); + + int32_t GetX() const; + + int32_t GetY() const; + + void Set(int32_t x, int32_t y); + + void ClipTo(int32_t minX, + int32_t maxX, + int32_t minY, + int32_t maxY); + + double GetDistanceTo(const ImagePoint& other) const; + + double GetDistanceToLine(double a, + double b, + double c) const; // where ax + by + c = 0 is the equation of the line + }; + + class ORTHANC_PUBLIC IPolygonFiller : public boost::noncopyable + { + public: + virtual ~IPolygonFiller() + { + } + + virtual void Fill(int y, + int x1, + int x2) = 0; + }; + + static void Copy(ImageAccessor& target, + const ImageAccessor& source); + + static void Convert(ImageAccessor& target, + const ImageAccessor& source); + + static void ApplyWindowing_Deprecated(ImageAccessor& target, + const ImageAccessor& source, + float windowCenter, + float windowWidth, + float rescaleSlope, + float rescaleIntercept, + bool invert); + + static void Set(ImageAccessor& image, + int64_t value); + + static void Set(ImageAccessor& image, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t alpha); + + static void Set(ImageAccessor& image, + uint8_t red, + uint8_t green, + uint8_t blue, + ImageAccessor& alpha); + + static void ShiftRight(ImageAccessor& target, + unsigned int shift); + + static void ShiftLeft(ImageAccessor& target, + unsigned int shift); + + static void GetMinMaxIntegerValue(int64_t& minValue, + int64_t& maxValue, + const ImageAccessor& image); + + static void GetMinMaxFloatValue(float& minValue, + float& maxValue, + const ImageAccessor& image); + + static void AddConstant(ImageAccessor& image, + int64_t value); + + // "useRound" is expensive + static void MultiplyConstant(ImageAccessor& image, + float factor, + bool useRound); + + // Computes "(x + offset) * scaling" inplace. "useRound" is expensive. + static void ShiftScale(ImageAccessor& image, + float offset, + float scaling, + bool useRound); + + static void ShiftScale(ImageAccessor& target, + const ImageAccessor& source, + float offset, + float scaling, + bool useRound); + + // Computes "x * scaling + offset" inplace. "useRound" is expensive. + static void ShiftScale2(ImageAccessor& image, + float offset, + float scaling, + bool useRound); + + static void ShiftScale2(ImageAccessor& target, + const ImageAccessor& source, + float offset, + float scaling, + bool useRound); + + static void Invert(ImageAccessor& image); + + static void Invert(ImageAccessor& image, int64_t maxValue); + + static void DrawLineSegment(ImageAccessor& image, + int x0, + int y0, + int x1, + int y1, + int64_t value); + + static void DrawLineSegment(ImageAccessor& image, + int x0, + int y0, + int x1, + int y1, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t alpha); + + static void FillPolygon(IPolygonFiller& filler, + const std::vector& points); + + static void FillPolygon(ImageAccessor& image, + const std::vector& points, + int64_t value); + + static void Resize(ImageAccessor& target, + const ImageAccessor& source); + + static ImageAccessor* Halve(const ImageAccessor& source, + bool forceMinimalPitch); + + static void FlipX(ImageAccessor& image); + + static void FlipY(ImageAccessor& image); + + static void SeparableConvolution(ImageAccessor& image /* inplace */, + const std::vector& horizontal, + size_t horizontalAnchor, + const std::vector& vertical, + size_t verticalAnchor, + bool useRound /* this is expensive */); + + static void SmoothGaussian5x5(ImageAccessor& image, + bool useRound /* this is expensive */); + + static void FitSize(ImageAccessor& target, + const ImageAccessor& source); + + // Resize the image to the given width/height. The resized image + // occupies the entire canvas (aspect ratio is not preserved). + static ImageAccessor* FitSize(const ImageAccessor& source, + unsigned int width, + unsigned int height); + + // Resize an image, but keeps its original aspect ratio. Zeros are + // added around the image to reach the specified size. + static ImageAccessor* FitSizeKeepAspectRatio(const ImageAccessor& source, + unsigned int width, + unsigned int height); + + // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion + static void ConvertJpegYCbCrToRgb(ImageAccessor& image /* inplace */); + + static void SwapEndianness(ImageAccessor& image /* inplace */); + + static void Maximum(ImageAccessor& image /* inout */, + const ImageAccessor& other); + + static void Render(ImageAccessor& target, + const DicomImageInformation& info, + const ImageAccessor& source, + const Window& window); + + static void RenderDefaultWindow(ImageAccessor& target, + const DicomImageInformation& info, + const ImageAccessor& source); + }; +} diff --git a/OrthancFramework/Sources/Images/ImageTraits.h b/OrthancFramework/Sources/Images/ImageTraits.h new file mode 100644 index 0000000..63844ca --- /dev/null +++ b/OrthancFramework/Sources/Images/ImageTraits.h @@ -0,0 +1,80 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "ImageAccessor.h" +#include "PixelTraits.h" + +#include + +namespace Orthanc +{ + template + struct ImageTraits + { + typedef ::Orthanc::PixelTraits PixelTraits; + typedef typename PixelTraits::PixelType PixelType; + + static PixelFormat GetPixelFormat() + { + return Format; + } + + static void GetPixel(PixelType& target, + const ImageAccessor& image, + unsigned int x, + unsigned int y) + { + assert(x < image.GetWidth() && y < image.GetHeight()); + PixelTraits::Copy(target, image.GetPixelUnchecked(x, y)); + } + + static void SetPixel(ImageAccessor& image, + const PixelType& value, + unsigned int x, + unsigned int y) + { + assert(x < image.GetWidth() && y < image.GetHeight()); + PixelTraits::Copy(image.GetPixelUnchecked(x, y), value); + } + + static float GetFloatPixel(const ImageAccessor& image, + unsigned int x, + unsigned int y) + { + assert(x < image.GetWidth() && y < image.GetHeight()); + return PixelTraits::PixelToFloat(image.GetPixelUnchecked(x, y)); + } + + static void SetFloatPixel(ImageAccessor& image, + float value, + unsigned int x, + unsigned int y) + { + assert(x < image.GetWidth() && y < image.GetHeight()); + PixelTraits::FloatToPixel(image.GetPixelUnchecked(x, y), value); + } + }; +} diff --git a/OrthancFramework/Sources/Images/JpegErrorManager.cpp b/OrthancFramework/Sources/Images/JpegErrorManager.cpp new file mode 100644 index 0000000..b5557de --- /dev/null +++ b/OrthancFramework/Sources/Images/JpegErrorManager.cpp @@ -0,0 +1,61 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JpegErrorManager.h" + +namespace Orthanc +{ + namespace Internals + { + void JpegErrorManager::OutputMessage(j_common_ptr cinfo) + { + char message[JMSG_LENGTH_MAX]; + (*cinfo->err->format_message) (cinfo, message); + + JpegErrorManager* that = reinterpret_cast(cinfo->err); + that->message = std::string(message); + } + + + void JpegErrorManager::ErrorExit(j_common_ptr cinfo) + { + (*cinfo->err->output_message) (cinfo); + + JpegErrorManager* that = reinterpret_cast(cinfo->err); + longjmp(that->setjmp_buffer, 1); + } + + + JpegErrorManager::JpegErrorManager() + { + memset(&pub, 0, sizeof(struct jpeg_error_mgr)); + memset(&setjmp_buffer, 0, sizeof(jmp_buf)); + + jpeg_std_error(&pub); + pub.error_exit = ErrorExit; + pub.output_message = OutputMessage; + } + } +} diff --git a/OrthancFramework/Sources/Images/JpegErrorManager.h b/OrthancFramework/Sources/Images/JpegErrorManager.h new file mode 100644 index 0000000..f59d6cb --- /dev/null +++ b/OrthancFramework/Sources/Images/JpegErrorManager.h @@ -0,0 +1,74 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + +#pragma once + +#if !defined(ORTHANC_ENABLE_JPEG) +# error The macro ORTHANC_ENABLE_JPEG must be defined +#endif + +#if ORTHANC_ENABLE_JPEG != 1 +# error JPEG support must be enabled to include this file +#endif + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + namespace Internals + { + class JpegErrorManager + { + private: + struct jpeg_error_mgr pub; /* "public" fields */ + jmp_buf setjmp_buffer; /* for return to caller */ + std::string message; + + static void OutputMessage(j_common_ptr cinfo); + + static void ErrorExit(j_common_ptr cinfo); + + public: + JpegErrorManager(); + + struct jpeg_error_mgr* GetPublic() + { + return &pub; + } + + jmp_buf& GetJumpBuffer() + { + return setjmp_buffer; + } + + const std::string& GetMessage() const + { + return message; + } + }; + } +} diff --git a/OrthancFramework/Sources/Images/JpegReader.cpp b/OrthancFramework/Sources/Images/JpegReader.cpp new file mode 100644 index 0000000..27be71e --- /dev/null +++ b/OrthancFramework/Sources/Images/JpegReader.cpp @@ -0,0 +1,189 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JpegReader.h" + +#include "JpegErrorManager.h" +#include "../OrthancException.h" +#include "../Logging.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + + +namespace Orthanc +{ + static void Uncompress(struct jpeg_decompress_struct& cinfo, + std::string& content, + ImageAccessor& accessor) + { + // The "static_cast" is necessary on OS X: + // https://github.com/simonfuhrmann/mve/issues/371 + jpeg_read_header(&cinfo, static_cast(true)); + + jpeg_start_decompress(&cinfo); + + PixelFormat format; + if (cinfo.output_components == 1 && + cinfo.out_color_space == JCS_GRAYSCALE) + { + format = PixelFormat_Grayscale8; + } + else if (cinfo.output_components == 3 && + cinfo.out_color_space == JCS_RGB) + { + format = PixelFormat_RGB24; + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + + unsigned int pitch = cinfo.output_width * cinfo.output_components; + + /* Make a one-row-high sample array that will go away when done with image */ + JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, pitch, 1); + + try + { + content.resize(pitch * cinfo.output_height); + } + catch (...) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + accessor.AssignWritable(format, cinfo.output_width, cinfo.output_height, pitch, + content.empty() ? NULL : &content[0]); + + uint8_t* target = reinterpret_cast(&content[0]); + while (cinfo.output_scanline < cinfo.output_height) + { + jpeg_read_scanlines(&cinfo, buffer, 1); + memcpy(target, buffer[0], pitch); + target += pitch; + } + + // Everything went fine, "setjmp()" didn't get called + + jpeg_finish_decompress(&cinfo); + } + + +#if ORTHANC_SANDBOXED == 0 + void JpegReader::ReadFromFile(const std::string& filename) + { + FILE* fp = SystemToolbox::OpenFile(filename, FileMode_ReadBinary); + if (!fp) + { + throw OrthancException(ErrorCode_InexistentFile); + } + + struct jpeg_decompress_struct cinfo; + memset(&cinfo, 0, sizeof(struct jpeg_decompress_struct)); + + Internals::JpegErrorManager jerr; + cinfo.err = jerr.GetPublic(); + + if (setjmp(jerr.GetJumpBuffer())) + { + jpeg_destroy_decompress(&cinfo); + fclose(fp); + + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG decoding: " + jerr.GetMessage()); + } + + // Below this line, we are under the scope of a "setjmp" + + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, fp); + + try + { + Uncompress(cinfo, content_, *this); + } + catch (OrthancException&) + { + jpeg_destroy_decompress(&cinfo); + fclose(fp); + throw; + } + + jpeg_destroy_decompress(&cinfo); + fclose(fp); + } +#endif + + + void JpegReader::ReadFromMemory(const void* buffer, + size_t size) + { + struct jpeg_decompress_struct cinfo; + memset(&cinfo, 0, sizeof(struct jpeg_decompress_struct)); + + Internals::JpegErrorManager jerr; + cinfo.err = jerr.GetPublic(); + + if (setjmp(jerr.GetJumpBuffer())) + { + jpeg_destroy_decompress(&cinfo); + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG decoding: " + jerr.GetMessage()); + } + + // Below this line, we are under the scope of a "setjmp" + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, + const_cast( + reinterpret_cast(buffer)), size); + + try + { + Uncompress(cinfo, content_, *this); + } + catch (OrthancException&) + { + jpeg_destroy_decompress(&cinfo); + throw; + } + + jpeg_destroy_decompress(&cinfo); + } + + + void JpegReader::ReadFromMemory(const std::string& buffer) + { + if (buffer.empty()) + { + ReadFromMemory(NULL, 0); + } + else + { + ReadFromMemory(buffer.c_str(), buffer.size()); + } + } +} diff --git a/OrthancFramework/Sources/Images/JpegReader.h b/OrthancFramework/Sources/Images/JpegReader.h new file mode 100644 index 0000000..1b12c71 --- /dev/null +++ b/OrthancFramework/Sources/Images/JpegReader.h @@ -0,0 +1,60 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if !defined(ORTHANC_ENABLE_JPEG) +# error The macro ORTHANC_ENABLE_JPEG must be defined +#endif + +#if ORTHANC_ENABLE_JPEG != 1 +# error JPEG support must be enabled to include this file +#endif + +#include "ImageAccessor.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC JpegReader : public ImageAccessor + { + private: + std::string content_; + + public: +#if ORTHANC_SANDBOXED == 0 + void ReadFromFile(const std::string& filename); +#endif + + void ReadFromMemory(const void* buffer, + size_t size); + + void ReadFromMemory(const std::string& buffer); + }; +} diff --git a/OrthancFramework/Sources/Images/JpegWriter.cpp b/OrthancFramework/Sources/Images/JpegWriter.cpp new file mode 100644 index 0000000..9497d1b --- /dev/null +++ b/OrthancFramework/Sources/Images/JpegWriter.cpp @@ -0,0 +1,224 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JpegWriter.h" + +#include "../OrthancException.h" +#include "../Logging.h" +#include "JpegErrorManager.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include +#include + +namespace Orthanc +{ + static void GetLines(std::vector& lines, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + if (format != PixelFormat_Grayscale8 && + format != PixelFormat_RGB24) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + lines.resize(height); + + uint8_t* base = const_cast(reinterpret_cast(buffer)); + for (unsigned int y = 0; y < height; y++) + { + lines[y] = base + static_cast(y) * static_cast(pitch); + } + } + + + static void Compress(struct jpeg_compress_struct& cinfo, + std::vector& lines, + unsigned int width, + unsigned int height, + PixelFormat format, + uint8_t quality) + { + cinfo.image_width = width; + cinfo.image_height = height; + + switch (format) + { + case PixelFormat_Grayscale8: + cinfo.input_components = 1; + cinfo.in_color_space = JCS_GRAYSCALE; + break; + + case PixelFormat_RGB24: + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + jpeg_set_defaults(&cinfo); + + // The "static_cast" is necessary on OS X: + // https://github.com/simonfuhrmann/mve/issues/371 + jpeg_set_quality(&cinfo, quality, static_cast(true)); + jpeg_start_compress(&cinfo, static_cast(true)); + + jpeg_write_scanlines(&cinfo, &lines[0], height); + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + } + + + JpegWriter::JpegWriter() : quality_(90) + { + } + + + void JpegWriter::SetQuality(uint8_t quality) + { + if (quality == 0 || quality > 100) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + quality_ = quality; + } + + +#if ORTHANC_SANDBOXED == 0 + void JpegWriter::WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + FILE* fp = SystemToolbox::OpenFile(filename, FileMode_WriteBinary); + if (fp == NULL) + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + + std::vector lines; + GetLines(lines, height, pitch, format, buffer); + + struct jpeg_compress_struct cinfo; + memset(&cinfo, 0, sizeof(struct jpeg_compress_struct)); + + Internals::JpegErrorManager jerr; + cinfo.err = jerr.GetPublic(); + + if (setjmp(jerr.GetJumpBuffer())) + { + /* If we get here, the JPEG code has signaled an error. + * We need to clean up the JPEG object, close the input file, and return. + */ + jpeg_destroy_compress(&cinfo); + fclose(fp); + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG encoding: " + jerr.GetMessage()); + } + + // Do not allocate data on the stack below this line! + + jpeg_create_compress(&cinfo); + jpeg_stdio_dest(&cinfo, fp); + Compress(cinfo, lines, width, height, format, quality_); + + // Everything went fine, "setjmp()" didn't get called + + fclose(fp); + } +#endif + + + void JpegWriter::WriteToMemoryInternal(std::string& jpeg, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + std::vector lines; + GetLines(lines, height, pitch, format, buffer); + + struct jpeg_compress_struct cinfo; + memset(&cinfo, 0, sizeof(struct jpeg_compress_struct)); + + Internals::JpegErrorManager jerr; + + unsigned char* data = NULL; + +#if ((JPEG_LIB_VERSION_MAJOR < 9) || \ + (JPEG_LIB_VERSION_MAJOR == 9 && JPEG_LIB_VERSION_MINOR <= 3)) + /** + * jpeg_mem_dest() has "unsigned long*" as its 3rd parameter until + * jpeg-9c. Since jpeg-9d, this is a "size_t*". + **/ + unsigned long size; +#else + size_t size; +#endif + + if (setjmp(jerr.GetJumpBuffer())) + { + jpeg_destroy_compress(&cinfo); + + if (data != NULL) + { + free(data); + } + + throw OrthancException(ErrorCode_InternalError, + "Error during JPEG encoding: " + jerr.GetMessage()); + } + + // Do not allocate data on the stack below this line! + + jpeg_create_compress(&cinfo); + cinfo.err = jerr.GetPublic(); + jpeg_mem_dest(&cinfo, &data, &size); + + Compress(cinfo, lines, width, height, format, quality_); + + // Everything went fine, "setjmp()" didn't get called + + jpeg.assign(reinterpret_cast(data), static_cast(size)); + free(data); + } + + uint8_t JpegWriter::GetQuality() const + { + return quality_; + } +} diff --git a/OrthancFramework/Sources/Images/JpegWriter.h b/OrthancFramework/Sources/Images/JpegWriter.h new file mode 100644 index 0000000..44f596c --- /dev/null +++ b/OrthancFramework/Sources/Images/JpegWriter.h @@ -0,0 +1,69 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_JPEG) +# error The macro ORTHANC_ENABLE_JPEG must be defined +#endif + +#if ORTHANC_ENABLE_JPEG != 1 +# error JPEG support must be enabled to include this file +#endif + +#include "IImageWriter.h" +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + class ORTHANC_PUBLIC JpegWriter : public IImageWriter + { + protected: +#if ORTHANC_SANDBOXED == 0 + virtual void WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; +#endif + + virtual void WriteToMemoryInternal(std::string& jpeg, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; + + private: + uint8_t quality_; + + public: + JpegWriter(); + + void SetQuality(uint8_t quality); + + uint8_t GetQuality() const; + }; +} diff --git a/OrthancFramework/Sources/Images/NumpyWriter.cpp b/OrthancFramework/Sources/Images/NumpyWriter.cpp new file mode 100644 index 0000000..b3fe06c --- /dev/null +++ b/OrthancFramework/Sources/Images/NumpyWriter.cpp @@ -0,0 +1,243 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "NumpyWriter.h" + +#if ORTHANC_ENABLE_ZLIB == 1 +# include "../Compression/ZipWriter.h" +#endif + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + +namespace Orthanc +{ + void NumpyWriter::WriteHeader(ChunkedBuffer& target, + unsigned int depth, + unsigned int width, + unsigned int height, + PixelFormat format) + { + // https://numpy.org/devdocs/reference/generated/numpy.lib.format.html + static const unsigned char VERSION[] = { + 0x93, 'N', 'U', 'M', 'P', 'Y', + 0x01 /* major version: 1 */, + 0x00 /* minor version: 0 */ + }; + + std::string datatype; + + switch (Toolbox::DetectEndianness()) + { + case Endianness_Little: + datatype = "<"; + break; + + case Endianness_Big: + datatype = ">"; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + unsigned int channels; + + switch (format) + { + case PixelFormat_Grayscale8: + datatype += "u1"; + channels = 1; + break; + + case PixelFormat_Grayscale16: + datatype += "u2"; + channels = 1; + break; + + case PixelFormat_SignedGrayscale16: + datatype += "i2"; + channels = 1; + break; + + case PixelFormat_RGB24: + datatype += "u1"; + channels = 3; + break; + + case PixelFormat_Float32: + datatype += "f4"; + channels = 1; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + std::string depthString; + if (depth != 0) + { + depthString = boost::lexical_cast(depth) + ", "; + } + + const std::string info = ("{'descr': '" + datatype + "', 'fortran_order': False, " + + "'shape': (" + depthString + boost::lexical_cast(height) + + "," + boost::lexical_cast(width) + + "," + boost::lexical_cast(channels) + "), }"); + + const uint16_t minimumLength = sizeof(VERSION) + sizeof(uint16_t) + info.size() + 1 /* trailing '\n' */; + + // The length of the header must be evenly divisible by 64. This + // loop could be optimized by a "ceil()" operation, but we keep + // the code as simple as possible + uint16_t length = 64; + while (length < minimumLength) + { + length += 64; + } + + uint16_t countZeros = length - minimumLength; + uint16_t headerLength = info.size() + countZeros + 1 /* trailing '\n' */; + uint8_t highByte = headerLength / 256; + uint8_t lowByte = headerLength % 256; + + target.AddChunk(VERSION, sizeof(VERSION)); + target.AddChunk(&lowByte, 1); + target.AddChunk(&highByte, 1); + target.AddChunk(info); + target.AddChunk(std::string(countZeros, ' ')); + target.AddChunk("\n"); + } + + + void NumpyWriter::WritePixels(ChunkedBuffer& target, + const ImageAccessor& image) + { + size_t rowSize = image.GetBytesPerPixel() * image.GetWidth(); + + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + target.AddChunk(image.GetConstRow(y), rowSize); + } + } + + + void NumpyWriter::Finalize(std::string& target, + ChunkedBuffer& source, + bool compress) + { + if (compress) + { +#if (ORTHANC_ENABLE_ZLIB == 1) && (ORTHANC_SANDBOXED == 0) + // This is the default name of the first array if arrays are + // specified as positional arguments in "numpy.savez()" + // https://numpy.org/doc/stable/reference/generated/numpy.savez.html + const char* ARRAY_NAME = "arr_0"; + + std::string uncompressed; + source.Flatten(uncompressed); + + const bool isZip64 = (uncompressed.size() >= 1lu * 1024lu * 1024lu * 1024lu); + + ZipWriter writer; + writer.SetMemoryOutput(target, isZip64); + writer.Open(); + writer.OpenFile(ARRAY_NAME); + writer.Write(uncompressed); + writer.Close(); +#else + throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for ZIP"); +#endif + } + else + { + source.Flatten(target); + } + } + + +#if ORTHANC_SANDBOXED == 0 + void NumpyWriter::WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + std::string content; + WriteToMemoryInternal(content, width, height, pitch, format, buffer); + + SystemToolbox::WriteFile(content, filename); + } +#endif + + + void NumpyWriter::WriteToMemoryInternal(std::string& content, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + ChunkedBuffer chunks; + WriteHeader(chunks, 0 /* no depth */, width, height, format); + + ImageAccessor image; + image.AssignReadOnly(format, width, height, pitch, buffer); + WritePixels(chunks, image); + + Finalize(content, chunks, compressed_); + } + + + NumpyWriter::NumpyWriter() + { + compressed_ = false; + } + + + void NumpyWriter::SetCompressed(bool compressed) + { +#if ORTHANC_ENABLE_ZLIB == 1 + compressed_ = compressed; +#else + if (compressed) + { + throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for zlib"); + } +#endif + } + + + bool NumpyWriter::IsCompressed() const + { + return compressed_; + } +} diff --git a/OrthancFramework/Sources/Images/NumpyWriter.h b/OrthancFramework/Sources/Images/NumpyWriter.h new file mode 100644 index 0000000..fb84262 --- /dev/null +++ b/OrthancFramework/Sources/Images/NumpyWriter.h @@ -0,0 +1,79 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_ZLIB) +# error The macro ORTHANC_ENABLE_ZLIB must be defined +#endif + +#include "IImageWriter.h" +#include "../ChunkedBuffer.h" +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + class ORTHANC_PUBLIC NumpyWriter : public IImageWriter + { + protected: +#if ORTHANC_SANDBOXED == 0 + virtual void WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; +#endif + + virtual void WriteToMemoryInternal(std::string& content, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; + + private: + bool compressed_; + + public: + NumpyWriter(); + + void SetCompressed(bool compressed); + + bool IsCompressed() const; + + static void WriteHeader(ChunkedBuffer& target, + unsigned int depth, // Must be "0" for 2D images + unsigned int width, + unsigned int height, + PixelFormat format); + + static void WritePixels(ChunkedBuffer& target, + const ImageAccessor& image); + + static void Finalize(std::string& target, + ChunkedBuffer& source, + bool compress); + }; +} diff --git a/OrthancFramework/Sources/Images/PamReader.cpp b/OrthancFramework/Sources/Images/PamReader.cpp new file mode 100644 index 0000000..001dd78 --- /dev/null +++ b/OrthancFramework/Sources/Images/PamReader.cpp @@ -0,0 +1,303 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "PamReader.h" + +#include "../Endianness.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include // For malloc/free +#include +#include + + +namespace Orthanc +{ + static void GetPixelFormat(PixelFormat& format, + unsigned int& bytesPerChannel, + const unsigned int& maxValue, + const unsigned int& channelCount, + const std::string& tupleType) + { + if (tupleType == "GRAYSCALE" && + channelCount == 1) + { + switch (maxValue) + { + case 255: + format = PixelFormat_Grayscale8; + bytesPerChannel = 1; + return; + + case 65535: + format = PixelFormat_Grayscale16; + bytesPerChannel = 2; + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + else if (tupleType == "RGB" && + channelCount == 3) + { + switch (maxValue) + { + case 255: + format = PixelFormat_RGB24; + bytesPerChannel = 1; + return; + + case 65535: + format = PixelFormat_RGB48; + bytesPerChannel = 2; + return; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + typedef std::map Parameters; + + + static std::string LookupStringParameter(const Parameters& parameters, + const std::string& key) + { + Parameters::const_iterator found = parameters.find(key); + + if (found == parameters.end()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return found->second; + } + } + + + static unsigned int LookupIntegerParameter(const Parameters& parameters, + const std::string& key) + { + try + { + int value = boost::lexical_cast(LookupStringParameter(parameters, key)); + + if (value < 0) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return static_cast(value); + } + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void PamReader::ParseContent() + { + static const std::string headerDelimiter = "ENDHDR\n"; + + boost::iterator_range headerRange = + boost::algorithm::find_first(content_, headerDelimiter); + + if (!headerRange) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + std::string header(static_cast(content_).begin(), headerRange.begin()); + + std::vector lines; + Toolbox::TokenizeString(lines, header, '\n'); + + if (lines.size() < 2 || + lines.front() != "P7" || + !lines.back().empty()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Parameters parameters; + + for (size_t i = 1; i + 1 < lines.size(); i++) + { + std::vector tokens; + Toolbox::TokenizeString(tokens, lines[i], ' '); + + if (tokens.size() != 2) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + parameters[tokens[0]] = tokens[1]; + } + } + + const unsigned int width = LookupIntegerParameter(parameters, "WIDTH"); + const unsigned int height = LookupIntegerParameter(parameters, "HEIGHT"); + const unsigned int channelCount = LookupIntegerParameter(parameters, "DEPTH"); + const unsigned int maxValue = LookupIntegerParameter(parameters, "MAXVAL"); + const std::string tupleType = LookupStringParameter(parameters, "TUPLTYPE"); + + unsigned int bytesPerChannel; + PixelFormat format; + GetPixelFormat(format, bytesPerChannel, maxValue, channelCount, tupleType); + + unsigned int pitch = width * channelCount * bytesPerChannel; + + if (content_.size() != header.size() + headerDelimiter.size() + pitch * height) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + size_t offset = content_.size() - pitch * height; + + { + intptr_t bufferAddr = reinterpret_cast(&content_[offset]); + if((bufferAddr % 8) == 0) + LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr; + else + LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr << " (not a multiple of 8!)"; + } + + // if we want to enforce alignment, we need to use a freshly allocated + // buffer, since we have no alignment guarantees on the original one + if (enforceAligned_) + { + if (alignedImageBuffer_ != NULL) + free(alignedImageBuffer_); + alignedImageBuffer_ = malloc(pitch * height); + memcpy(alignedImageBuffer_, &content_[offset], pitch* height); + content_ = ""; + AssignWritable(format, width, height, pitch, alignedImageBuffer_); + } + else + { + AssignWritable(format, width, height, pitch, &content_[offset]); + } + + // Byte swapping if needed + if (bytesPerChannel != 1 && + bytesPerChannel != 2) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + if (Toolbox::DetectEndianness() == Endianness_Little && + bytesPerChannel == 2) + { + for (unsigned int h = 0; h < height; ++h) + { + uint16_t* pixel = reinterpret_cast(GetRow(h)); + + for (unsigned int w = 0; w < width; ++w, ++pixel) + { + /** + * This is Little-Endian computer, and PAM uses + * Big-Endian. Need to do a 16-bit swap. We DON'T use + * "htobe16()", as the latter only works if the "pixel" + * pointer is 16-bit aligned (which is not the case if + * "offset" is an odd number), and the trick that was used + * in Orthanc <= 1.8.0 (i.e. make a "memcpy()" to a local + * uint16_t variable) doesn't seem work for WebAssembly. We + * thus use a plain old C implementation. Check out issue + * #99: https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=99 + * + * Here is the crash log on WebAssembly (2019-08-05): + * + * Uncaught abort(alignment fault) at Error + * at jsStackTrace + * at stackTrace + * at abort + * at alignfault + * at SAFE_HEAP_LOAD_i32_2_2 (wasm-function[251132]:39) + * at __ZN7Orthanc9PamReader12ParseContentEv (wasm-function[11457]:8088) + **/ + uint8_t* srcdst = reinterpret_cast(pixel); + uint8_t tmp = srcdst[0]; + srcdst[0] = srcdst[1]; + srcdst[1] = tmp; + } + } + } + } + + PamReader::PamReader(bool enforceAligned) : + enforceAligned_(enforceAligned), + alignedImageBuffer_(NULL) + { + } + + +#if ORTHANC_SANDBOXED == 0 + void PamReader::ReadFromFile(const std::string& filename) + { + SystemToolbox::ReadFile(content_, filename); + ParseContent(); + } +#endif + + + void PamReader::ReadFromMemory(const std::string& buffer) + { + content_ = buffer; + ParseContent(); + } + + void PamReader::ReadFromMemory(const void* buffer, + size_t size) + { + content_.assign(reinterpret_cast(buffer), size); + ParseContent(); + } + + PamReader::~PamReader() + { + if (alignedImageBuffer_ != NULL) + { + free(alignedImageBuffer_); + } + } +} diff --git a/OrthancFramework/Sources/Images/PamReader.h b/OrthancFramework/Sources/Images/PamReader.h new file mode 100644 index 0000000..41d8a53 --- /dev/null +++ b/OrthancFramework/Sources/Images/PamReader.h @@ -0,0 +1,83 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "ImageAccessor.h" + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +namespace Orthanc +{ + class ORTHANC_PUBLIC PamReader : public ImageAccessor + { + private: + void ParseContent(); + + /** + Whether we want to use the default malloc alignment in the image buffer, + at the expense of an extra copy + */ + bool enforceAligned_; + + /** + This is actually a copy of wrappedContent_, but properly aligned. + + It is only used if the enforceAligned parameter is set to true in the + constructor. + */ + void* alignedImageBuffer_; + + /** + Points somewhere in the content_ buffer. + */ + ImageAccessor wrappedContent_; + + /** + Raw content (file bytes or answer from the server, for instance). + */ + std::string content_; + + public: + /** + See doc for field enforceAligned_. Setting "enforceAligned" is slower, + but avoids possible crashes due to non-aligned memory access. It is + recommended to set this parameter to "true". + */ + explicit PamReader(bool enforceAligned); + + virtual ~PamReader(); + +#if ORTHANC_SANDBOXED == 0 + void ReadFromFile(const std::string& filename); +#endif + + void ReadFromMemory(const std::string& buffer); + + void ReadFromMemory(const void* buffer, + size_t size); + }; +} diff --git a/OrthancFramework/Sources/Images/PamWriter.cpp b/OrthancFramework/Sources/Images/PamWriter.cpp new file mode 100644 index 0000000..98cc47f --- /dev/null +++ b/OrthancFramework/Sources/Images/PamWriter.cpp @@ -0,0 +1,161 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "PamWriter.h" + +#include "../Endianness.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include +#include + + +namespace Orthanc +{ + static void GetPixelFormatInfo(const PixelFormat& format, + unsigned int& maxValue, + unsigned int& channelCount, + unsigned int& bytesPerChannel, + std::string& tupleType) + { + switch (format) + { + case PixelFormat_Grayscale8: + maxValue = 255; + channelCount = 1; + bytesPerChannel = 1; + tupleType = "GRAYSCALE"; + break; + + case PixelFormat_SignedGrayscale16: + case PixelFormat_Grayscale16: + maxValue = 65535; + channelCount = 1; + bytesPerChannel = 2; + tupleType = "GRAYSCALE"; + break; + + case PixelFormat_RGB24: + maxValue = 255; + channelCount = 3; + bytesPerChannel = 1; + tupleType = "RGB"; + break; + + case PixelFormat_RGB48: + maxValue = 255; + channelCount = 3; + bytesPerChannel = 2; + tupleType = "RGB"; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void PamWriter::WriteToMemoryInternal(std::string& target, + unsigned int width, + unsigned int height, + unsigned int sourcePitch, + PixelFormat format, + const void* buffer) + { + unsigned int maxValue, channelCount, bytesPerChannel; + std::string tupleType; + GetPixelFormatInfo(format, maxValue, channelCount, bytesPerChannel, tupleType); + + target = (std::string("P7") + + std::string("\nWIDTH ") + boost::lexical_cast(width) + + std::string("\nHEIGHT ") + boost::lexical_cast(height) + + std::string("\nDEPTH ") + boost::lexical_cast(channelCount) + + std::string("\nMAXVAL ") + boost::lexical_cast(maxValue) + + std::string("\nTUPLTYPE ") + tupleType + + std::string("\nENDHDR\n")); + + if (bytesPerChannel != 1 && + bytesPerChannel != 2) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + size_t targetPitch = channelCount * bytesPerChannel * width; + size_t offset = target.size(); + + target.resize(offset + targetPitch * height); + + assert(target.size() != 0); + + if (Toolbox::DetectEndianness() == Endianness_Little && + bytesPerChannel == 2) + { + // Byte swapping + for (unsigned int h = 0; h < height; ++h) + { + const uint16_t* p = reinterpret_cast + (reinterpret_cast(buffer) + h * sourcePitch); + uint16_t* q = reinterpret_cast + (reinterpret_cast(&target[offset]) + h * targetPitch); + + for (unsigned int w = 0; w < width * channelCount; ++w) + { + /** + * This is Little-Endian computer, and PAM uses + * Big-Endian. Need to do a 16-bit swap. We DON'T use + * "htobe16()", as the latter only works if the "pixel" + * pointer is 16-bit aligned (which is not the case if + * "offset" is an odd number), and the trick that was used + * in Orthanc <= 1.8.0 (i.e. make a "memcpy()" to a local + * uint16_t variable) doesn't seem work for WebAssembly. We + * thus use a plain old C implementation. Check out issue + * #99: https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=99 + **/ + const uint8_t* a = reinterpret_cast(p); + uint8_t* b = reinterpret_cast(q); + b[0] = a[1]; + b[1] = a[0]; + + p++; + q++; + } + } + } + else + { + // Either "bytesPerChannel == 1" (and endianness is not + // relevant), or we run on a big endian architecture (and no + // byte swapping is necessary, as PAM uses big endian) + + for (unsigned int h = 0; h < height; ++h) + { + const void* p = reinterpret_cast(buffer) + h * sourcePitch; + void* q = reinterpret_cast(&target[offset]) + h * targetPitch; + memcpy(q, p, targetPitch); + } + } + } +} diff --git a/OrthancFramework/Sources/Images/PamWriter.h b/OrthancFramework/Sources/Images/PamWriter.h new file mode 100644 index 0000000..22cff7c --- /dev/null +++ b/OrthancFramework/Sources/Images/PamWriter.h @@ -0,0 +1,44 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IImageWriter.h" + +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + // https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format + class ORTHANC_PUBLIC PamWriter : public IImageWriter + { + protected: + virtual void WriteToMemoryInternal(std::string& target, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/Images/PixelTraits.h b/OrthancFramework/Sources/Images/PixelTraits.h new file mode 100644 index 0000000..6cb5042 --- /dev/null +++ b/OrthancFramework/Sources/Images/PixelTraits.h @@ -0,0 +1,404 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Compatibility.h" // For ORTHANC_FORCE_INLINE +#include "../Enumerations.h" + +#include + +namespace Orthanc +{ + template + struct IntegerPixelTraits + { + typedef _PixelType PixelType; + + ORTHANC_FORCE_INLINE + static PixelFormat GetPixelFormat() + { + return format; + } + + ORTHANC_FORCE_INLINE + static PixelType IntegerToPixel(int64_t value) + { + if (value < static_cast(std::numeric_limits::min())) + { + return std::numeric_limits::min(); + } + else if (value > static_cast(std::numeric_limits::max())) + { + return std::numeric_limits::max(); + } + else + { + return static_cast(value); + } + } + + ORTHANC_FORCE_INLINE + static void SetZero(PixelType& target) + { + target = 0; + } + + ORTHANC_FORCE_INLINE + static void SetMinValue(PixelType& target) + { + target = std::numeric_limits::min(); + } + + ORTHANC_FORCE_INLINE + static void SetMaxValue(PixelType& target) + { + target = std::numeric_limits::max(); + } + + ORTHANC_FORCE_INLINE + static void Copy(PixelType& target, + const PixelType& source) + { + target = source; + } + + ORTHANC_FORCE_INLINE + static float PixelToFloat(const PixelType& source) + { + return static_cast(source); + } + + ORTHANC_FORCE_INLINE + static void FloatToPixel(PixelType& target, + float value) + { + value += 0.5f; + if (value < static_cast(std::numeric_limits::min())) + { + target = std::numeric_limits::min(); + } + else if (value > static_cast(std::numeric_limits::max())) + { + target = std::numeric_limits::max(); + } + else + { + target = static_cast(value); + } + } + + ORTHANC_FORCE_INLINE + static bool IsEqual(const PixelType& a, + const PixelType& b) + { + return a == b; + } + }; + + + template + struct PixelTraits; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits + { + struct PixelType + { + uint8_t red_; + uint8_t green_; + uint8_t blue_; + }; + + ORTHANC_FORCE_INLINE + static PixelFormat GetPixelFormat() + { + return PixelFormat_RGB24; + } + + ORTHANC_FORCE_INLINE + static void SetZero(PixelType& target) + { + target.red_ = 0; + target.green_ = 0; + target.blue_ = 0; + } + + ORTHANC_FORCE_INLINE + static void Copy(PixelType& target, + const PixelType& source) + { + target.red_ = source.red_; + target.green_ = source.green_; + target.blue_ = source.blue_; + } + + ORTHANC_FORCE_INLINE + static bool IsEqual(const PixelType& a, + const PixelType& b) + { + return (a.red_ == b.red_ && + a.green_ == b.green_ && + a.blue_ == b.blue_); + } + + ORTHANC_FORCE_INLINE + static void FloatToPixel(PixelType& target, + float value) + { + uint8_t v; + PixelTraits::FloatToPixel(v, value); + + target.red_ = v; + target.green_ = v; + target.blue_ = v; + } + }; + + + template <> + struct PixelTraits + { + struct PixelType + { + uint8_t blue_; + uint8_t green_; + uint8_t red_; + uint8_t alpha_; + }; + + ORTHANC_FORCE_INLINE + static PixelFormat GetPixelFormat() + { + return PixelFormat_BGRA32; + } + + ORTHANC_FORCE_INLINE + static void SetZero(PixelType& target) + { + target.blue_ = 0; + target.green_ = 0; + target.red_ = 0; + target.alpha_ = 0; + } + + ORTHANC_FORCE_INLINE + static void Copy(PixelType& target, + const PixelType& source) + { + target.blue_ = source.blue_; + target.green_ = source.green_; + target.red_ = source.red_; + target.alpha_ = source.alpha_; + } + + ORTHANC_FORCE_INLINE + static bool IsEqual(const PixelType& a, + const PixelType& b) + { + return (a.blue_ == b.blue_ && + a.green_ == b.green_ && + a.red_ == b.red_ && + a.alpha_ == b.alpha_); + } + + ORTHANC_FORCE_INLINE + static void FloatToPixel(PixelType& target, + float value) + { + uint8_t v; + PixelTraits::FloatToPixel(v, value); + + target.blue_ = v; + target.green_ = v; + target.red_ = v; + target.alpha_ = 255; + } + }; + + + template <> + struct PixelTraits + { + struct PixelType + { + uint8_t red_; + uint8_t green_; + uint8_t blue_; + uint8_t alpha_; + }; + + ORTHANC_FORCE_INLINE + static PixelFormat GetPixelFormat() + { + return PixelFormat_RGBA32; + } + + ORTHANC_FORCE_INLINE + static void SetZero(PixelType& target) + { + target.red_ = 0; + target.green_ = 0; + target.blue_ = 0; + target.alpha_ = 0; + } + + ORTHANC_FORCE_INLINE + static void Copy(PixelType& target, + const PixelType& source) + { + target.red_ = source.red_; + target.green_ = source.green_; + target.blue_ = source.blue_; + target.alpha_ = source.alpha_; + } + + ORTHANC_FORCE_INLINE + static bool IsEqual(const PixelType& a, + const PixelType& b) + { + return (a.red_ == b.red_ && + a.green_ == b.green_ && + a.blue_ == b.blue_ && + a.alpha_ == b.alpha_); + } + + ORTHANC_FORCE_INLINE + static void FloatToPixel(PixelType& target, + float value) + { + uint8_t v; + PixelTraits::FloatToPixel(v, value); + + target.red_ = v; + target.green_ = v; + target.blue_ = v; + target.alpha_ = 255; + } + }; + + + template <> + struct PixelTraits + { + typedef float PixelType; + + ORTHANC_FORCE_INLINE + static PixelFormat GetPixelFormat() + { + return PixelFormat_Float32; + } + + ORTHANC_FORCE_INLINE + static void SetZero(PixelType& target) + { + target = 0.0f; + } + + ORTHANC_FORCE_INLINE + static void Copy(PixelType& target, + const PixelType& source) + { + target = source; + } + + ORTHANC_FORCE_INLINE + static bool IsEqual(const PixelType& a, + const PixelType& b) + { + float tmp = (a - b); + + if (tmp < 0) + { + tmp = -tmp; + } + + return tmp <= std::numeric_limits::epsilon(); + } + + ORTHANC_FORCE_INLINE + static void SetMinValue(PixelType& target) + { + // std::numeric_limits::lowest is not supported on + // all compilers (for instance, Visual Studio 9.0 2008) + target = -std::numeric_limits::max(); + } + + ORTHANC_FORCE_INLINE + static void SetMaxValue(PixelType& target) + { + target = std::numeric_limits::max(); + } + + ORTHANC_FORCE_INLINE + static void FloatToPixel(PixelType& target, + float value) + { + target = value; + } + + ORTHANC_FORCE_INLINE + static float PixelToFloat(const PixelType& source) + { + return source; + } + }; +} diff --git a/OrthancFramework/Sources/Images/PngReader.cpp b/OrthancFramework/Sources/Images/PngReader.cpp new file mode 100644 index 0000000..da24149 --- /dev/null +++ b/OrthancFramework/Sources/Images/PngReader.cpp @@ -0,0 +1,325 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "PngReader.h" + +#include "../OrthancException.h" +#include "../Toolbox.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include +#include // For memcpy() + +namespace Orthanc +{ +#if ORTHANC_SANDBOXED == 0 + namespace + { + struct FileRabi + { + FILE* fp_; + + explicit FileRabi(const char* filename) + { + fp_ = SystemToolbox::OpenFile(filename, FileMode_ReadBinary); + if (!fp_) + { + throw OrthancException(ErrorCode_InexistentFile); + } + } + + ~FileRabi() + { + if (fp_) + { + fclose(fp_); + } + } + }; + } +#endif + + + struct PngReader::PngRabi + { + png_structp png_; + png_infop info_; + png_infop endInfo_; + + void Destruct() + { + if (png_) + { + png_destroy_read_struct(&png_, &info_, &endInfo_); + + png_ = NULL; + info_ = NULL; + endInfo_ = NULL; + } + } + + PngRabi() : + png_(NULL), + info_(NULL), + endInfo_(NULL) + { + png_ = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png_) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + info_ = png_create_info_struct(png_); + if (!info_) + { + png_destroy_read_struct(&png_, NULL, NULL); + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + endInfo_ = png_create_info_struct(png_); + if (!info_) + { + png_destroy_read_struct(&png_, &info_, NULL); + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + ~PngRabi() + { + Destruct(); + } + + static void MemoryCallback(png_structp png_ptr, + png_bytep data, + png_size_t size); + }; + + + void PngReader::CheckHeader(const void* header) + { + int is_png = !png_sig_cmp((png_bytep) header, 0, 8); + if (!is_png) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + PngReader::PngReader() + { + } + + void PngReader::Read(PngRabi& rabi) + { + png_set_sig_bytes(rabi.png_, 8); + + png_read_info(rabi.png_, rabi.info_); + + png_uint_32 width, height; + int bit_depth, color_type, interlace_type; + int compression_type, filter_method; + // get size and bit-depth of the PNG-image + png_get_IHDR(rabi.png_, rabi.info_, + &width, &height, + &bit_depth, &color_type, &interlace_type, + &compression_type, &filter_method); + + PixelFormat format; + unsigned int pitch; + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth == 8) + { + format = PixelFormat_Grayscale8; + pitch = width; + } + else if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth == 16) + { + format = PixelFormat_Grayscale16; + pitch = 2 * width; + + if (Toolbox::DetectEndianness() == Endianness_Little) + { + png_set_swap(rabi.png_); + } + } + else if (color_type == PNG_COLOR_TYPE_RGB && bit_depth == 8) + { + format = PixelFormat_RGB24; + pitch = 3 * width; + } + else if (color_type == PNG_COLOR_TYPE_RGBA && bit_depth == 8) + { + format = PixelFormat_RGBA32; + pitch = 4 * width; + } + else if (color_type == PNG_COLOR_TYPE_RGBA && bit_depth == 16) + { + format = PixelFormat_RGBA64; + pitch = 8 * width; + + if (Toolbox::DetectEndianness() == Endianness_Little) + { + png_set_swap(rabi.png_); + } + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + + data_.resize(height * pitch); + + if (height == 0 || width == 0) + { + // Empty image, we are done + AssignEmpty(format); + return; + } + + png_read_update_info(rabi.png_, rabi.info_); + + std::vector rows(height); + for (size_t i = 0; i < height; i++) + { + rows[i] = &data_[0] + i * pitch; + } + + png_read_image(rabi.png_, &rows[0]); + + AssignWritable(format, width, height, pitch, &data_[0]); + } + + +#if ORTHANC_SANDBOXED == 0 + void PngReader::ReadFromFile(const std::string& filename) + { + FileRabi f(filename.c_str()); + + char header[8]; + if (fread(header, 1, 8, f.fp_) != 8) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + CheckHeader(header); + + PngRabi rabi; + + if (setjmp(png_jmpbuf(rabi.png_))) + { + rabi.Destruct(); + throw OrthancException(ErrorCode_BadFileFormat); + } + + png_init_io(rabi.png_, f.fp_); + + Read(rabi); + } +#endif + + + namespace + { + struct MemoryBuffer + { + const uint8_t* buffer_; + size_t size_; + size_t pos_; + bool ok_; + }; + } + + + void PngReader::PngRabi::MemoryCallback(png_structp png_ptr, + png_bytep outBytes, + png_size_t byteCountToRead) + { + MemoryBuffer* from = reinterpret_cast(png_get_io_ptr(png_ptr)); + + if (!from->ok_) + { + return; + } + + if (from->pos_ + byteCountToRead > from->size_) + { + from->ok_ = false; + return; + } + + memcpy(outBytes, from->buffer_ + from->pos_, byteCountToRead); + + from->pos_ += byteCountToRead; + } + + + void PngReader::ReadFromMemory(const void* buffer, + size_t size) + { + if (size < 8) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + CheckHeader(buffer); + + PngRabi rabi; + + if (setjmp(png_jmpbuf(rabi.png_))) + { + rabi.Destruct(); + throw OrthancException(ErrorCode_BadFileFormat); + } + + MemoryBuffer tmp; + tmp.buffer_ = reinterpret_cast(buffer) + 8; // We skip the header + tmp.size_ = size - 8; + tmp.pos_ = 0; + tmp.ok_ = true; + + png_set_read_fn(rabi.png_, &tmp, PngRabi::MemoryCallback); + + Read(rabi); + + if (!tmp.ok_) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + void PngReader::ReadFromMemory(const std::string& buffer) + { + if (buffer.size() != 0) + { + ReadFromMemory(&buffer[0], buffer.size()); + } + else + { + ReadFromMemory(NULL, 0); + } + } +} diff --git a/OrthancFramework/Sources/Images/PngReader.h b/OrthancFramework/Sources/Images/PngReader.h new file mode 100644 index 0000000..b97707f --- /dev/null +++ b/OrthancFramework/Sources/Images/PngReader.h @@ -0,0 +1,72 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_PNG) +# error The macro ORTHANC_ENABLE_PNG must be defined +#endif + +#if ORTHANC_ENABLE_PNG != 1 +# error PNG support must be enabled to include this file +#endif + +#include "ImageAccessor.h" + +#include "../Enumerations.h" + +#include +#include +#include + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +namespace Orthanc +{ + class ORTHANC_PUBLIC PngReader : public ImageAccessor + { + private: + struct PngRabi; + + std::vector data_; + + void CheckHeader(const void* header); + + void Read(PngRabi& rabi); + + public: + PngReader(); + +#if ORTHANC_SANDBOXED == 0 + void ReadFromFile(const std::string& filename); +#endif + + void ReadFromMemory(const void* buffer, + size_t size); + + void ReadFromMemory(const std::string& buffer); + }; +} diff --git a/OrthancFramework/Sources/Images/PngWriter.cpp b/OrthancFramework/Sources/Images/PngWriter.cpp new file mode 100644 index 0000000..44aa0d3 --- /dev/null +++ b/OrthancFramework/Sources/Images/PngWriter.cpp @@ -0,0 +1,289 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "PngWriter.h" + +#include +#include +#include +#include "../OrthancException.h" +#include "../ChunkedBuffer.h" +#include "../Toolbox.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + + +// http://www.libpng.org/pub/png/libpng-1.2.5-manual.html#section-4 +// http://zarb.org/~gc/html/libpng.html +/* + void write_row_callback(png_ptr, png_uint_32 row, int pass) + { + }*/ + + + + +/* bool isError_; + +// http://www.libpng.org/pub/png/book/chapter14.html#png.ch14.div.2 + +static void ErrorHandler(png_structp png, png_const_charp message) +{ +printf("** [%s]\n", message); + +PngWriter* that = (PngWriter*) png_get_error_ptr(png); +that->isError_ = true; +printf("** %d\n", (int)that); + +//((PngWriter*) payload)->isError_ = true; +} + +static void WarningHandler(png_structp png, png_const_charp message) +{ +printf("++ %d\n", (int)message); +}*/ + + +namespace Orthanc +{ + /** + * The "png_" structure cannot be safely reused if the bpp changes + * between successive invocations. This can lead to "Invalid reads" + * reported by valgrind if writing a 16bpp image, then a 8bpp image + * using the same "PngWriter" object. Starting with Orthanc 1.9.3, + * we recreate a new "png_" context each time a PNG image must be + * written so as to prevent such invalid reads. + **/ + class PngWriter::Context : public boost::noncopyable + { + private: + png_structp png_; + png_infop info_; + + // Filled by Prepare() + std::vector rows_; + int bitDepth_; + int colorType_; + + public: + Context() : + png_(NULL), + info_(NULL), + bitDepth_(0), // Dummy initialization + colorType_(0) // Dummy initialization + { + png_ = png_create_write_struct + (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); //this, ErrorHandler, WarningHandler); + if (!png_) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + info_ = png_create_info_struct(png_); + if (!info_) + { + png_destroy_write_struct(&png_, NULL); + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + + ~Context() + { + if (info_) + { + png_destroy_info_struct(png_, &info_); + } + + if (png_) + { + png_destroy_write_struct(&png_, NULL); + } + } + + + png_structp GetObject() const + { + return png_; + } + + + void Prepare(unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + rows_.resize(height); + for (unsigned int y = 0; y < height; y++) + { + rows_[y] = const_cast(reinterpret_cast(buffer)) + y * pitch; + } + + switch (format) + { + case PixelFormat_RGB24: + bitDepth_ = 8; + colorType_ = PNG_COLOR_TYPE_RGB; + break; + + case PixelFormat_RGBA32: + bitDepth_ = 8; + colorType_ = PNG_COLOR_TYPE_RGBA; + break; + + case PixelFormat_Grayscale8: + bitDepth_ = 8; + colorType_ = PNG_COLOR_TYPE_GRAY; + break; + + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + bitDepth_ = 16; + colorType_ = PNG_COLOR_TYPE_GRAY; + break; + + case PixelFormat_RGBA64: + bitDepth_ = 16; + colorType_ = PNG_COLOR_TYPE_RGBA; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void Compress(unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format) + { + png_set_IHDR(png_, info_, width, height, + bitDepth_, colorType_, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + + png_write_info(png_, info_); + + if (height > 0) + { + switch (format) + { + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + case PixelFormat_RGBA64: + { + int transforms = 0; + if (Toolbox::DetectEndianness() == Endianness_Little) + { + transforms = PNG_TRANSFORM_SWAP_ENDIAN; + } + + png_set_rows(png_, info_, &rows_[0]); + png_write_png(png_, info_, transforms, NULL); + + break; + } + + default: + png_write_image(png_, &rows_[0]); + } + } + + png_write_end(png_, NULL); + } + }; + + +#if ORTHANC_SANDBOXED == 0 + void PngWriter::WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + Context context; + + context.Prepare(width, height, pitch, format, buffer); + + FILE* fp = SystemToolbox::OpenFile(filename, FileMode_WriteBinary); + if (!fp) + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + + png_init_io(context.GetObject(), fp); + + if (setjmp(png_jmpbuf(context.GetObject()))) + { + // Error during writing PNG + throw OrthancException(ErrorCode_CannotWriteFile); + } + + context.Compress(width, height, pitch, format); + + fclose(fp); + } +#endif + + + static void MemoryCallback(png_structp png_ptr, + png_bytep data, + png_size_t size) + { + ChunkedBuffer* buffer = reinterpret_cast(png_get_io_ptr(png_ptr)); + buffer->AddChunk(data, size); + } + + + void PngWriter::WriteToMemoryInternal(std::string& png, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) + { + Context context; + + ChunkedBuffer chunks; + + context.Prepare(width, height, pitch, format, buffer); + + if (setjmp(png_jmpbuf(context.GetObject()))) + { + // Error during writing PNG + throw OrthancException(ErrorCode_InternalError); + } + + png_set_write_fn(context.GetObject(), &chunks, MemoryCallback, NULL); + + context.Compress(width, height, pitch, format); + + chunks.Flatten(png); + } +} diff --git a/OrthancFramework/Sources/Images/PngWriter.h b/OrthancFramework/Sources/Images/PngWriter.h new file mode 100644 index 0000000..492b1c2 --- /dev/null +++ b/OrthancFramework/Sources/Images/PngWriter.h @@ -0,0 +1,64 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_PNG) +# error The macro ORTHANC_ENABLE_PNG must be defined +#endif + +#if ORTHANC_ENABLE_PNG != 1 +# error PNG support must be enabled to include this file +#endif + +#include "IImageWriter.h" +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC PngWriter : public IImageWriter + { + private: + class Context; + + protected: +#if ORTHANC_SANDBOXED == 0 + virtual void WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; +#endif + + virtual void WriteToMemoryInternal(std::string& png, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp new file mode 100644 index 0000000..8bc943d --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp @@ -0,0 +1,90 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "GenericJobUnserializer.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include "Operations/LogJobOperation.h" +#include "Operations/NullOperationValue.h" +#include "Operations/SequenceOfOperationsJob.h" +#include "Operations/StringOperationValue.h" + +namespace Orthanc +{ + IJob* GenericJobUnserializer::UnserializeJob(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "SequenceOfOperations") + { + return new SequenceOfOperationsJob(*this, source); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot unserialize job of type: " + type); + } + } + + + IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "Log") + { + return new LogJobOperation; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot unserialize operation of type: " + type); + } + } + + + IJobOperationValue* GenericJobUnserializer::UnserializeValue(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "String") + { + return new StringOperationValue(SerializationToolbox::ReadString(source, "Content")); + } + else if (type == "Null") + { + return new NullOperationValue; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot unserialize value of type: " + type); + } + } +} + diff --git a/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h new file mode 100644 index 0000000..b7de484 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h @@ -0,0 +1,42 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJobUnserializer.h" + +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + class ORTHANC_PUBLIC GenericJobUnserializer : public IJobUnserializer + { + public: + virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE; + + virtual IJobOperation* UnserializeOperation(const Json::Value& value) ORTHANC_OVERRIDE; + + virtual IJobOperationValue* UnserializeValue(const Json::Value& value) ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/IJob.h b/OrthancFramework/Sources/JobsEngine/IJob.h new file mode 100644 index 0000000..bc73990 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/IJob.h @@ -0,0 +1,80 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "JobStepResult.h" + +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC IJob : public boost::noncopyable + { + public: + virtual ~IJob() + { + } + + // Method called once the job enters the jobs engine + virtual void Start() = 0; + + virtual JobStepResult Step(const std::string& jobId) = 0; + + // Method called once the job is resubmitted after a failure + virtual void Reset() = 0; + + // For pausing/canceling/ending jobs: This method must release allocated resources + virtual void Stop(JobStopReason reason) = 0; + + virtual float GetProgress() const = 0; + + virtual bool NeedsProgressUpdateBetweenSteps() const // only for jobs whose progress is updated by outside events (like C-Move and C-Get) + { + return false; + } + + virtual void GetJobType(std::string& target) const = 0; + + virtual void GetPublicContent(Json::Value& value) const = 0; + + virtual bool Serialize(Json::Value& value) const = 0; + + // This function can only be called if the job has reached its + // "success" state + virtual bool GetOutput(std::string& output, + MimeType& mime, + std::string& filename, + const std::string& key) = 0; + + // This function can only be called if the job has reached its + // "success" state + virtual bool DeleteOutput(const std::string& key) = 0; + + // This function can only be called if the job has reached its + // "success" state + virtual void DeleteAllOutputs() {} + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h b/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h new file mode 100644 index 0000000..1e352bf --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h @@ -0,0 +1,48 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJob.h" +#include "Operations/IJobOperationValue.h" +#include "Operations/IJobOperation.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC IJobUnserializer : public boost::noncopyable + { + public: + virtual ~IJobUnserializer() + { + } + + virtual IJob* UnserializeJob(const Json::Value& value) = 0; + + virtual IJobOperation* UnserializeOperation(const Json::Value& value) = 0; + + virtual IJobOperationValue* UnserializeValue(const Json::Value& value) = 0; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp new file mode 100644 index 0000000..4995a6b --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp @@ -0,0 +1,207 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" + +#ifdef __EMSCRIPTEN__ +/* +Avoid this error: + +.../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion] +.../boost/math/special_functions/round.hpp:125:11: note: in instantiation of function template specialization 'boost::math::llround >' requested here +.../orthanc/Core/JobsEngine/JobInfo.cpp:69:44: note: in instantiation of function template specialization 'boost::math::llround' requested here + +.../boost/math/special_functions/round.hpp:86:12: warning: implicit conversion from 'std::__2::numeric_limits::type' (aka 'int') to 'float' changes value from 2147483647 to 2147483648 [-Wimplicit-int-float-conversion] +.../boost/math/special_functions/round.hpp:93:11: note: in instantiation of function template specialization 'boost::math::iround >' requested here +.../orthanc/Core/JobsEngine/JobInfo.cpp:133:39: note: in instantiation of function template specialization 'boost::math::iround' requested here +*/ +#pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion" +#endif + +#include "JobInfo.h" + +#include "../OrthancException.h" + +// This "include" is mandatory for Release builds using Linux Standard Base +#include + +namespace Orthanc +{ + JobInfo::JobInfo(const std::string& id, + int priority, + JobState state, + const JobStatus& status, + const boost::posix_time::ptime& creationTime, + const boost::posix_time::ptime& lastStateChangeTime, + const boost::posix_time::time_duration& runtime, + const IJob& job) : + id_(id), + priority_(priority), + state_(state), + timestamp_(boost::posix_time::microsec_clock::universal_time()), + creationTime_(creationTime), + lastStateChangeTime_(lastStateChangeTime), + runtime_(runtime), + hasEta_(false), + status_(status) + { + if (state_ == JobState_Running) + { + float ms = static_cast(runtime_.total_milliseconds()); + if (job.NeedsProgressUpdateBetweenSteps()) + { + status_.UpdateProgress(job); + } + + float progress = status_.GetProgress(); + + if (progress > 0.01f && + ms > 0.01f) + { + long long remaining = boost::math::llround(ms / progress * (1.0f - progress)); + eta_ = timestamp_ + boost::posix_time::milliseconds(remaining); + hasEta_ = true; + } + } + } + + + JobInfo::JobInfo() : + priority_(0), + state_(JobState_Failure), + timestamp_(boost::posix_time::microsec_clock::universal_time()), + creationTime_(timestamp_), + lastStateChangeTime_(timestamp_), + runtime_(boost::posix_time::milliseconds(0)), + hasEta_(false) + { + } + + const std::string &JobInfo::GetIdentifier() const + { + return id_; + } + + int JobInfo::GetPriority() const + { + return priority_; + } + + JobState JobInfo::GetState() const + { + return state_; + } + + const boost::posix_time::ptime &JobInfo::GetInfoTime() const + { + return timestamp_; + } + + const boost::posix_time::ptime &JobInfo::GetCreationTime() const + { + return creationTime_; + } + + const boost::posix_time::time_duration &JobInfo::GetRuntime() const + { + return runtime_; + } + + bool JobInfo::HasEstimatedTimeOfArrival() const + { + return hasEta_; + } + + + bool JobInfo::HasCompletionTime() const + { + return (state_ == JobState_Success || + state_ == JobState_Failure); + } + + + const boost::posix_time::ptime& JobInfo::GetEstimatedTimeOfArrival() const + { + if (hasEta_) + { + return eta_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const boost::posix_time::ptime& JobInfo::GetCompletionTime() const + { + if (HasCompletionTime()) + { + return lastStateChangeTime_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + const JobStatus &JobInfo::GetStatus() const + { + return status_; + } + + JobStatus &JobInfo::GetStatus() + { + return status_; + } + + + void JobInfo::Format(Json::Value& target) const + { + target = Json::objectValue; + target["ID"] = id_; + target["Priority"] = priority_; + target["ErrorCode"] = static_cast(status_.GetErrorCode()); + target["ErrorDescription"] = EnumerationToString(status_.GetErrorCode()); + target["ErrorDetails"] = status_.GetDetails(); + target["State"] = EnumerationToString(state_); + target["Timestamp"] = boost::posix_time::to_iso_string(timestamp_); + target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_); + target["EffectiveRuntime"] = static_cast(runtime_.total_milliseconds()) / 1000.0; + target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f); + + target["Type"] = status_.GetJobType(); + target["Content"] = status_.GetPublicContent(); + + if (HasEstimatedTimeOfArrival()) + { + target["EstimatedTimeOfArrival"] = boost::posix_time::to_iso_string(GetEstimatedTimeOfArrival()); + } + + if (HasCompletionTime()) + { + target["CompletionTime"] = boost::posix_time::to_iso_string(GetCompletionTime()); + } + } +} diff --git a/OrthancFramework/Sources/JobsEngine/JobInfo.h b/OrthancFramework/Sources/JobsEngine/JobInfo.h new file mode 100644 index 0000000..15214b4 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobInfo.h @@ -0,0 +1,85 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "JobStatus.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC JobInfo + { + private: + std::string id_; + int priority_; + JobState state_; + boost::posix_time::ptime timestamp_; + boost::posix_time::ptime creationTime_; + boost::posix_time::ptime lastStateChangeTime_; + boost::posix_time::time_duration runtime_; + bool hasEta_; + boost::posix_time::ptime eta_; + JobStatus status_; + + public: + JobInfo(const std::string& id, + int priority, + JobState state, + const JobStatus& status, + const boost::posix_time::ptime& creationTime, + const boost::posix_time::ptime& lastStateChangeTime, + const boost::posix_time::time_duration& runtime, + const IJob& job) ORTHANC_LOCAL; + + JobInfo(); + + const std::string& GetIdentifier() const; + + int GetPriority() const; + + JobState GetState() const; + + const boost::posix_time::ptime& GetInfoTime() const; + + const boost::posix_time::ptime& GetCreationTime() const; + + const boost::posix_time::time_duration& GetRuntime() const; + + bool HasEstimatedTimeOfArrival() const; + + bool HasCompletionTime() const; + + const boost::posix_time::ptime& GetEstimatedTimeOfArrival() const; + + const boost::posix_time::ptime& GetCompletionTime() const; + + const JobStatus& GetStatus() const; + + JobStatus& GetStatus(); + + void Format(Json::Value& target) const; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp new file mode 100644 index 0000000..264a1cc --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp @@ -0,0 +1,78 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobStatus.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + JobStatus::JobStatus() : + errorCode_(ErrorCode_InternalError), + progress_(0), + jobType_("Invalid"), + publicContent_(Json::objectValue), + hasSerialized_(false) + { + } + + + JobStatus::JobStatus(ErrorCode code, + const std::string& details, + const IJob& job) : + errorCode_(code), + progress_(job.GetProgress()), + publicContent_(Json::objectValue), + details_(details) + { + if (progress_ < 0) + { + progress_ = 0; + } + + if (progress_ > 1) + { + progress_ = 1; + } + + job.GetJobType(jobType_); + job.GetPublicContent(publicContent_); + + hasSerialized_ = job.Serialize(serialized_); + } + + + const Json::Value& JobStatus::GetSerialized() const + { + if (!hasSerialized_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return serialized_; + } + } +} diff --git a/OrthancFramework/Sources/JobsEngine/JobStatus.h b/OrthancFramework/Sources/JobsEngine/JobStatus.h new file mode 100644 index 0000000..0170539 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h @@ -0,0 +1,91 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJob.h" + +namespace Orthanc +{ + class JobStatus + { + private: + ErrorCode errorCode_; + float progress_; + std::string jobType_; + Json::Value publicContent_; + Json::Value serialized_; + bool hasSerialized_; + std::string details_; + + public: + JobStatus(); + + JobStatus(ErrorCode code, + const std::string& details, + const IJob& job); + + ErrorCode GetErrorCode() const + { + return errorCode_; + } + + void SetErrorCode(ErrorCode error) + { + errorCode_ = error; + } + + float GetProgress() const + { + return progress_; + } + + void UpdateProgress(const IJob& job) + { + progress_ = job.GetProgress(); + } + + const std::string& GetJobType() const + { + return jobType_; + } + + const Json::Value& GetPublicContent() const + { + return publicContent_; + } + + const Json::Value& GetSerialized() const; + + bool HasSerialized() const + { + return hasSerialized_; + } + + const std::string& GetDetails() const + { + return details_; + } + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp b/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp new file mode 100644 index 0000000..ad9f4b8 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp @@ -0,0 +1,121 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobStepResult.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + JobStepResult::JobStepResult() : + code_(JobStepCode_Failure), + timeout_(0), + error_(ErrorCode_InternalError) + { + } + + JobStepResult JobStepResult::Success() + { + return JobStepResult(JobStepCode_Success); + } + + JobStepResult JobStepResult::Continue() + { + return JobStepResult(JobStepCode_Continue); + } + + JobStepResult JobStepResult::Retry(unsigned int timeout) + { + JobStepResult result(JobStepCode_Retry); + result.timeout_ = timeout; + return result; + } + + + JobStepResult JobStepResult::Failure(const ErrorCode& error, + const char* details) + { + JobStepResult result(JobStepCode_Failure); + result.error_ = error; + + if (details != NULL) + { + result.failureDetails_ = details; + } + + return result; + } + + + JobStepResult JobStepResult::Failure(const OrthancException& exception) + { + return Failure(exception.GetErrorCode(), + exception.HasDetails() ? exception.GetDetails() : NULL); + } + + JobStepCode JobStepResult::GetCode() const + { + return code_; + } + + + unsigned int JobStepResult::GetRetryTimeout() const + { + if (code_ == JobStepCode_Retry) + { + return timeout_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ErrorCode JobStepResult::GetFailureCode() const + { + if (code_ == JobStepCode_Failure) + { + return error_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const std::string& JobStepResult::GetFailureDetails() const + { + if (code_ == JobStepCode_Failure) + { + return failureDetails_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } +} diff --git a/OrthancFramework/Sources/JobsEngine/JobStepResult.h b/OrthancFramework/Sources/JobsEngine/JobStepResult.h new file mode 100644 index 0000000..c93ecd4 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.h @@ -0,0 +1,70 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Enumerations.h" + +namespace Orthanc +{ + class OrthancException; + + class ORTHANC_PUBLIC JobStepResult + { + private: + JobStepCode code_; + unsigned int timeout_; + ErrorCode error_; + std::string failureDetails_; + + explicit JobStepResult(JobStepCode code) : + code_(code), + timeout_(0), + error_(ErrorCode_Success) + { + } + + public: + explicit JobStepResult(); + + static JobStepResult Success(); + + static JobStepResult Continue(); + + static JobStepResult Retry(unsigned int timeout); + + static JobStepResult Failure(const ErrorCode& error, + const char* details); + + static JobStepResult Failure(const OrthancException& exception); + + JobStepCode GetCode() const; + + unsigned int GetRetryTimeout() const; + + ErrorCode GetFailureCode() const; + + const std::string& GetFailureDetails() const; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp new file mode 100644 index 0000000..8349a05 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp @@ -0,0 +1,319 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobsEngine.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + + +namespace Orthanc +{ + bool JobsEngine::IsRunning() + { + boost::mutex::scoped_lock lock(stateMutex_); + return (state_ == State_Running); + } + + + bool JobsEngine::ExecuteStep(JobsRegistry::RunningJob& running, + size_t workerIndex) + { + assert(running.IsValid()); + + if (running.IsPauseScheduled()) + { + running.GetJob().Stop(JobStopReason_Paused); + running.MarkPause(); + return false; + } + + if (running.IsCancelScheduled()) + { + running.GetJob().Stop(JobStopReason_Canceled); + running.MarkCanceled(); + return false; + } + + JobStepResult result; + + try + { + result = running.GetJob().Step(running.GetId()); + } + catch (OrthancException& e) + { + result = JobStepResult::Failure(e); + } + catch (boost::bad_lexical_cast&) + { + result = JobStepResult::Failure(ErrorCode_BadFileFormat, NULL); + } + catch (...) + { + result = JobStepResult::Failure(ErrorCode_InternalError, NULL); + } + + switch (result.GetCode()) + { + case JobStepCode_Success: + running.GetJob().Stop(JobStopReason_Success); + running.UpdateStatus(ErrorCode_Success, ""); + running.MarkSuccess(); + return false; + + case JobStepCode_Failure: + running.GetJob().Stop(JobStopReason_Failure); + running.UpdateStatus(result.GetFailureCode(), result.GetFailureDetails()); + running.MarkFailure(); + return false; + + case JobStepCode_Retry: + running.GetJob().Stop(JobStopReason_Retry); + running.UpdateStatus(ErrorCode_Success, ""); + running.MarkRetry(result.GetRetryTimeout()); + return false; + + case JobStepCode_Continue: + running.UpdateStatus(ErrorCode_Success, ""); + return true; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void JobsEngine::RetryHandler(JobsEngine* engine) + { + assert(engine != NULL); + + while (engine->IsRunning()) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(engine->threadSleep_)); + engine->GetRegistry().ScheduleRetries(); + } + } + + + void JobsEngine::Worker(JobsEngine* engine, + size_t workerIndex) + { + assert(engine != NULL); + Logging::SetCurrentThreadName(std::string("JOBS-WORKER-") + boost::lexical_cast(workerIndex)); + CLOG(INFO, JOBS) << "Worker thread " << workerIndex << " has started"; + + while (engine->IsRunning()) + { + JobsRegistry::RunningJob running(engine->GetRegistry(), engine->threadSleep_); + + if (running.IsValid()) + { + std::string jobType; + running.GetJob().GetJobType(jobType); + + CLOG(INFO, JOBS) << "Executing " << jobType << " job with priority " << running.GetPriority() + << " in worker thread " << workerIndex << ": " << running.GetId(); + + while (engine->IsRunning()) + { + if (!engine->ExecuteStep(running, workerIndex)) + { + break; + } + } + } + } + } + + + JobsEngine::JobsEngine(size_t maxCompletedJobs) : + state_(State_Setup), + registry_(new JobsRegistry(maxCompletedJobs)), + threadSleep_(200), + workers_(1) + { + } + + + JobsEngine::~JobsEngine() + { + if (state_ != State_Setup && + state_ != State_Done) + { + CLOG(ERROR, JOBS) << "INTERNAL ERROR: JobsEngine::Stop() should be invoked manually to avoid mess in the destruction order!"; + Stop(); + } + } + + + JobsRegistry& JobsEngine::GetRegistry() + { + if (registry_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + return *registry_; + } + + + void JobsEngine::LoadRegistryFromJson(IJobUnserializer& unserializer, + const Json::Value& serialized) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + assert(registry_.get() != NULL); + const size_t maxCompletedJobs = registry_->GetMaxCompletedJobs(); + registry_.reset(new JobsRegistry(unserializer, serialized, maxCompletedJobs)); + } + + + void JobsEngine::LoadRegistryFromString(IJobUnserializer& unserializer, + const std::string& serialized) + { + Json::Value value; + if (Toolbox::ReadJson(value, serialized)) + { + LoadRegistryFromJson(unserializer, value); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void JobsEngine::SetWorkersCount(size_t count) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + workers_.resize(count); + } + + + void JobsEngine::SetThreadSleep(unsigned int sleep) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + threadSleep_ = sleep; + } + + + void JobsEngine::Start() + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + retryHandler_ = boost::thread(RetryHandler, this); + + if (workers_.size() == 0) + { + // Use all the available CPUs + size_t n = boost::thread::hardware_concurrency(); + + if (n == 0) + { + n = 1; + } + + workers_.resize(n); + } + + for (size_t i = 0; i < workers_.size(); i++) + { + assert(workers_[i] == NULL); + workers_[i] = new boost::thread(Worker, this, i); + } + + state_ = State_Running; + + CLOG(WARNING, JOBS) << "The jobs engine has started with " << workers_.size() << " threads"; + } + + + void JobsEngine::Stop() + { + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Running) + { + return; + } + + state_ = State_Stopping; + } + + CLOG(INFO, JOBS) << "Stopping the jobs engine"; + + if (retryHandler_.joinable()) + { + retryHandler_.join(); + } + + for (size_t i = 0; i < workers_.size(); i++) + { + assert(workers_[i] != NULL); + + if (workers_[i]->joinable()) + { + workers_[i]->join(); + } + + delete workers_[i]; + } + + { + boost::mutex::scoped_lock lock(stateMutex_); + state_ = State_Done; + } + + CLOG(WARNING, JOBS) << "The jobs engine has stopped"; + } +} diff --git a/OrthancFramework/Sources/JobsEngine/JobsEngine.h b/OrthancFramework/Sources/JobsEngine/JobsEngine.h new file mode 100644 index 0000000..e05cda2 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.h @@ -0,0 +1,84 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "JobsRegistry.h" + +#include "../Compatibility.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC JobsEngine : public boost::noncopyable + { + private: + enum State + { + State_Setup, + State_Running, + State_Stopping, + State_Done + }; + + boost::mutex stateMutex_; + State state_; + std::unique_ptr registry_; + boost::thread retryHandler_; + unsigned int threadSleep_; + std::vector workers_; + + bool IsRunning(); + + bool ExecuteStep(JobsRegistry::RunningJob& running, + size_t workerIndex); + + static void RetryHandler(JobsEngine* engine); + + static void Worker(JobsEngine* engine, + size_t workerIndex); + + public: + explicit JobsEngine(size_t maxCompletedJobs); + + ~JobsEngine(); + + JobsRegistry& GetRegistry(); + + void LoadRegistryFromJson(IJobUnserializer& unserializer, + const Json::Value& serialized); + + void LoadRegistryFromString(IJobUnserializer& unserializer, + const std::string& serialized); + + void SetWorkersCount(size_t count); + + void SetThreadSleep(unsigned int sleep); + + void Start(); + + void Stop(); + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp new file mode 100644 index 0000000..3471475 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp @@ -0,0 +1,1575 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobsRegistry.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" +#include "../SerializationToolbox.h" + +namespace Orthanc +{ + static const char* STATE = "State"; + static const char* TYPE = "Type"; + static const char* PRIORITY = "Priority"; + static const char* JOB = "Job"; + static const char* JOBS = "Jobs"; + static const char* JOBS_REGISTRY = "JobsRegistry"; + static const char* CREATION_TIME = "CreationTime"; + static const char* LAST_CHANGE_TIME = "LastChangeTime"; + static const char* RUNTIME = "Runtime"; + static const char* ERROR_CODE = "ErrorCode"; + static const char* ERROR_DETAILS = "ErrorDetails"; + + + class JobsRegistry::JobHandler : public boost::noncopyable + { + private: + std::string id_; + JobState state_; + std::string jobType_; + std::unique_ptr job_; + int priority_; // "+inf()" means highest priority + boost::posix_time::ptime creationTime_; + boost::posix_time::ptime lastStateChangeTime_; + boost::posix_time::time_duration runtime_; + boost::posix_time::ptime retryTime_; + bool pauseScheduled_; + bool cancelScheduled_; + JobStatus lastStatus_; + + void Touch() + { + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + + if (state_ == JobState_Running) + { + runtime_ += (now - lastStateChangeTime_); + } + + lastStateChangeTime_ = now; + } + + void SetStateInternal(JobState state) + { + state_ = state; + pauseScheduled_ = false; + cancelScheduled_ = false; + Touch(); + } + + public: + JobHandler(IJob* job, + int priority) : + id_(Toolbox::GenerateUuid()), + state_(JobState_Pending), + job_(job), + priority_(priority), + creationTime_(boost::posix_time::microsec_clock::universal_time()), + lastStateChangeTime_(creationTime_), + runtime_(boost::posix_time::milliseconds(0)), + retryTime_(creationTime_), + pauseScheduled_(false), + cancelScheduled_(false) + { + if (job == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + job->GetJobType(jobType_); + job->Start(); + + lastStatus_ = JobStatus(ErrorCode_Success, "", *job_); + } + + const std::string& GetId() const + { + return id_; + } + + IJob& GetJob() const + { + assert(job_.get() != NULL); + return *job_; + } + + void SetPriority(int priority) + { + priority_ = priority; + } + + int GetPriority() const + { + return priority_; + } + + JobState GetState() const + { + return state_; + } + + void SetState(JobState state) + { + if (state == JobState_Retry) + { + // Use "SetRetryState()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + SetStateInternal(state); + } + } + + void SetRetryState(unsigned int timeout) + { + if (state_ == JobState_Running) + { + SetStateInternal(JobState_Retry); + retryTime_ = (boost::posix_time::microsec_clock::universal_time() + + boost::posix_time::milliseconds(timeout)); + } + else + { + // Only valid for running jobs + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + void SchedulePause() + { + if (state_ == JobState_Running) + { + pauseScheduled_ = true; + } + else + { + // Only valid for running jobs + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + void ScheduleCancel() + { + if (state_ == JobState_Running) + { + cancelScheduled_ = true; + } + else + { + // Only valid for running jobs + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + bool IsPauseScheduled() + { + return pauseScheduled_; + } + + bool IsCancelScheduled() + { + return cancelScheduled_; + } + + bool IsRetryReady(const boost::posix_time::ptime& now) const + { + if (state_ != JobState_Retry) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return retryTime_ <= now; + } + } + + const boost::posix_time::ptime& GetCreationTime() const + { + return creationTime_; + } + + const boost::posix_time::ptime& GetLastStateChangeTime() const + { + return lastStateChangeTime_; + } + + void SetLastStateChangeTime(const boost::posix_time::ptime& time) + { + lastStateChangeTime_ = time; + } + + boost::posix_time::time_duration GetRuntime() const + { + if (state_ == JobState_Running) + { + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + return now - lastStateChangeTime_; + } + + return runtime_; + } + + void ResetRuntime() + { + runtime_ = boost::posix_time::milliseconds(0); + } + + const JobStatus& GetLastStatus() const + { + return lastStatus_; + } + + void SetLastStatus(const JobStatus& status) + { + lastStatus_ = status; + Touch(); + } + + void SetLastErrorCode(ErrorCode code) + { + lastStatus_.SetErrorCode(code); + } + + bool Serialize(Json::Value& target) const + { + target = Json::objectValue; + + bool ok; + + if (state_ == JobState_Running) + { + // WARNING: Cannot directly access the "job_" member, as long + // as a "RunningJob" instance is running. We do not use a + // mutex at the "JobHandler" level, as serialization would be + // blocked while a step in the job is running. Instead, we + // save a snapshot of the serialized job. (*) + + if (lastStatus_.HasSerialized()) + { + target[JOB] = lastStatus_.GetSerialized(); + ok = true; + } + else + { + ok = false; + } + } + else + { + ok = job_->Serialize(target[JOB]); + } + + if (ok) + { + target[STATE] = EnumerationToString(state_); + target[PRIORITY] = priority_; + target[CREATION_TIME] = boost::posix_time::to_iso_string(creationTime_); + target[LAST_CHANGE_TIME] = boost::posix_time::to_iso_string(lastStateChangeTime_); + target[RUNTIME] = static_cast(runtime_.total_milliseconds()); + + // New in Orthanc 1.9.5 + target[ERROR_CODE] = static_cast(lastStatus_.GetErrorCode()); + target[ERROR_DETAILS] = lastStatus_.GetDetails(); + + return true; + } + else + { + LOG(TRACE) << "Job backup is not supported for job of type: " << jobType_; + return false; + } + } + + JobHandler(IJobUnserializer& unserializer, + const Json::Value& serialized, + const std::string& id) : + id_(id), + pauseScheduled_(false), + cancelScheduled_(false) + { + state_ = StringToJobState(SerializationToolbox::ReadString(serialized, STATE)); + priority_ = SerializationToolbox::ReadInteger(serialized, PRIORITY); + creationTime_ = boost::posix_time::from_iso_string + (SerializationToolbox::ReadString(serialized, CREATION_TIME)); + lastStateChangeTime_ = boost::posix_time::from_iso_string + (SerializationToolbox::ReadString(serialized, LAST_CHANGE_TIME)); + runtime_ = boost::posix_time::milliseconds + (SerializationToolbox::ReadInteger(serialized, RUNTIME)); + + retryTime_ = creationTime_; + + job_.reset(unserializer.UnserializeJob(serialized[JOB])); + job_->GetJobType(jobType_); + job_->Start(); + + ErrorCode errorCode; + if (serialized.isMember(ERROR_CODE)) + { + errorCode = static_cast(SerializationToolbox::ReadInteger(serialized, ERROR_CODE)); + } + else + { + errorCode = ErrorCode_Success; // Backward compatibility with Orthanc <= 1.9.4 + } + + std::string details; + if (serialized.isMember(ERROR_DETAILS)) // Backward compatibility with Orthanc <= 1.9.4 + { + details = SerializationToolbox::ReadString(serialized, ERROR_DETAILS); + } + + lastStatus_ = JobStatus(errorCode, details, *job_); + } + }; + + + bool JobsRegistry::PriorityComparator::operator() (JobHandler* const& a, + JobHandler* const& b) const + { + return a->GetPriority() < b->GetPriority(); + } + + +#if defined(NDEBUG) + void JobsRegistry::CheckInvariants() const + { + } + +#else + bool JobsRegistry::IsPendingJob(const JobHandler& job) const + { + PendingJobs copy = pendingJobs_; + while (!copy.empty()) + { + if (copy.top() == &job) + { + return true; + } + + copy.pop(); + } + + return false; + } + + bool JobsRegistry::IsCompletedJob(JobHandler& job) const + { + for (CompletedJobs::const_iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + if (*it == &job) + { + return true; + } + } + + return false; + } + + bool JobsRegistry::IsRetryJob(JobHandler& job) const + { + return retryJobs_.find(&job) != retryJobs_.end(); + } + + void JobsRegistry::CheckInvariants() const + { + { + PendingJobs copy = pendingJobs_; + while (!copy.empty()) + { + assert(copy.top()->GetState() == JobState_Pending); + copy.pop(); + } + } + + assert(completedJobs_.size() <= maxCompletedJobs_); + + for (CompletedJobs::const_iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + assert((*it)->GetState() == JobState_Success || + (*it)->GetState() == JobState_Failure); + } + + for (RetryJobs::const_iterator it = retryJobs_.begin(); + it != retryJobs_.end(); ++it) + { + assert((*it)->GetState() == JobState_Retry); + } + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + JobHandler& job = *it->second; + + assert(job.GetId() == it->first); + + switch (job.GetState()) + { + case JobState_Pending: + assert(!IsRetryJob(job) && IsPendingJob(job) && !IsCompletedJob(job)); + break; + + case JobState_Success: + case JobState_Failure: + assert(!IsRetryJob(job) && !IsPendingJob(job) && IsCompletedJob(job)); + break; + + case JobState_Retry: + assert(IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job)); + break; + + case JobState_Running: + case JobState_Paused: + assert(!IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } +#endif + + + void JobsRegistry::ForgetOldCompletedJobs() + { + while (completedJobs_.size() > maxCompletedJobs_) + { + assert(completedJobs_.front() != NULL); + + std::string id = completedJobs_.front()->GetId(); + assert(jobsIndex_.find(id) != jobsIndex_.end()); + + jobsIndex_.erase(id); + delete(completedJobs_.front()); + completedJobs_.pop_front(); + } + + CheckInvariants(); + } + + + void JobsRegistry::SetCompletedJob(JobHandler& job, + bool success) + { + job.SetState(success ? JobState_Success : JobState_Failure); + + completedJobs_.push_back(&job); + someJobComplete_.notify_all(); + } + + + void JobsRegistry::MarkRunningAsCompleted(JobHandler& job, + CompletedReason reason) + { + const char* tmp; + + switch (reason) + { + case CompletedReason_Success: + tmp = "success"; + break; + + case CompletedReason_Failure: + tmp = "failure"; + break; + + case CompletedReason_Canceled: + tmp = "cancel"; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + LOG(INFO) << "Job has completed with " << tmp << ": " << job.GetId(); + + CheckInvariants(); + + assert(job.GetState() == JobState_Running); + SetCompletedJob(job, reason == CompletedReason_Success); + + if (reason == CompletedReason_Canceled) + { + job.SetLastErrorCode(ErrorCode_CanceledJob); + } + + if (observer_ != NULL) + { + if (reason == CompletedReason_Success) + { + observer_->SignalJobSuccess(job.GetId()); + } + else + { + observer_->SignalJobFailure(job.GetId()); + } + } + + // WARNING: The following call might make "job" invalid if the job + // history size is empty + ForgetOldCompletedJobs(); + } + + + void JobsRegistry::MarkRunningAsRetry(JobHandler& job, + unsigned int timeout) + { + LOG(INFO) << "Job scheduled for retry in " << timeout << "ms: " << job.GetId(); + + CheckInvariants(); + + assert(job.GetState() == JobState_Running && + retryJobs_.find(&job) == retryJobs_.end()); + + retryJobs_.insert(&job); + job.SetRetryState(timeout); + + CheckInvariants(); + } + + + void JobsRegistry::MarkRunningAsPaused(JobHandler& job) + { + LOG(INFO) << "Job paused: " << job.GetId(); + + CheckInvariants(); + assert(job.GetState() == JobState_Running); + + job.SetState(JobState_Paused); + + CheckInvariants(); + } + + + bool JobsRegistry::GetStateInternal(JobState& state, + const std::string& id) + { + CheckInvariants(); + + JobsIndex::const_iterator it = jobsIndex_.find(id); + if (it == jobsIndex_.end()) + { + return false; + } + else + { + state = it->second->GetState(); + return true; + } + } + + + JobsRegistry::~JobsRegistry() + { + for (JobsIndex::iterator it = jobsIndex_.begin(); it != jobsIndex_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void JobsRegistry::SetMaxCompletedJobs(size_t n) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + LOG(INFO) << "The size of the history of the jobs engine is set to: " << n << " job(s)"; + + maxCompletedJobs_ = n; + ForgetOldCompletedJobs(); + } + + + size_t JobsRegistry::GetMaxCompletedJobs() + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + return maxCompletedJobs_; + } + + + void JobsRegistry::ListJobs(std::set& target) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + target.insert(it->first); + } + } + + + bool JobsRegistry::GetJobInfo(JobInfo& target, + const std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::const_iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + return false; + } + else + { + const JobHandler& handler = *found->second; + target = JobInfo(handler.GetId(), + handler.GetPriority(), + handler.GetState(), + handler.GetLastStatus(), + handler.GetCreationTime(), + handler.GetLastStateChangeTime(), + handler.GetRuntime(), + handler.GetJob()); + return true; + } + } + + + bool JobsRegistry::DeleteJobInfo(const std::string& id) + { + LOG(INFO) << "Deleting job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job to delete: " << id; + return false; + } + else + { + for (CompletedJobs::iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + if (*it == found->second) + { + found->second->GetJob().DeleteAllOutputs(); + delete found->second; + + completedJobs_.erase(it); + jobsIndex_.erase(id); + return true; + } + } + + LOG(WARNING) << "Can not delete a job that is not complete: " << id; + return false; + } + } + + + bool JobsRegistry::GetJobOutput(std::string& output, + MimeType& mime, + std::string& filename, + const std::string& job, + const std::string& key) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::const_iterator found = jobsIndex_.find(job); + + if (found == jobsIndex_.end()) + { + return false; + } + else + { + const JobHandler& handler = *found->second; + + if (handler.GetState() == JobState_Success) + { + return handler.GetJob().GetOutput(output, mime, filename, key); + } + else + { + return false; + } + } + } + + bool JobsRegistry::DeleteJobOutput(const std::string& job, + const std::string& key) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::const_iterator found = jobsIndex_.find(job); + + if (found == jobsIndex_.end()) + { + return false; + } + else + { + const JobHandler& handler = *found->second; + + if (handler.GetState() == JobState_Success) + { + return handler.GetJob().DeleteOutput(key); + } + else + { + return false; + } + } + } + + + void JobsRegistry::SubmitInternal(std::string& id, + JobHandler* handler) + { + if (handler == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + std::unique_ptr protection(handler); + + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + id = handler->GetId(); + int priority = handler->GetPriority(); + + jobsIndex_.insert(std::make_pair(id, protection.release())); + + switch (handler->GetState()) + { + case JobState_Pending: + case JobState_Retry: + case JobState_Running: + handler->SetState(JobState_Pending); + pendingJobs_.push(handler); + pendingJobAvailable_.notify_one(); + break; + + case JobState_Success: + SetCompletedJob(*handler, true); + break; + + case JobState_Failure: + SetCompletedJob(*handler, false); + break; + + case JobState_Paused: + break; + + default: + { + std::string details = ("A job should not be loaded from state: " + + std::string(EnumerationToString(handler->GetState()))); + throw OrthancException(ErrorCode_InternalError, details); + } + } + + std::string jobType; + handler->GetJob().GetJobType(jobType); + + LOG(INFO) << "New " << jobType << " job submitted with priority " << priority << ": " << id; + + if (observer_ != NULL) + { + observer_->SignalJobSubmitted(id); + } + + // WARNING: The following call might make "handler" invalid if + // the job history size is empty + ForgetOldCompletedJobs(); + } + } + + JobsRegistry::JobsRegistry(size_t maxCompletedJobs) : + maxCompletedJobs_(maxCompletedJobs), + observer_(NULL) + { + } + + + void JobsRegistry::Submit(std::string& id, + IJob* job, // Takes ownership + int priority) + { + SubmitInternal(id, new JobHandler(job, priority)); + } + + + void JobsRegistry::Submit(IJob* job, // Takes ownership + int priority) + { + std::string id; + SubmitInternal(id, new JobHandler(job, priority)); + } + + + void JobsRegistry::SubmitAndWait(Json::Value& successContent, + IJob* job, // Takes ownership + int priority) + { + std::string id; + Submit(id, job, priority); + + JobState state = JobState_Pending; // Dummy initialization + + { + boost::mutex::scoped_lock lock(mutex_); + + for (;;) + { + if (!GetStateInternal(state, id)) + { + // Job has finished and has been lost (typically happens if + // "JobsHistorySize" is 0) + throw OrthancException(ErrorCode_InexistentItem, + "Cannot retrieve the status of the job, " + "make sure that \"JobsHistorySize\" is not 0"); + } + else if (state == JobState_Failure) + { + // Failure + JobsIndex::const_iterator it = jobsIndex_.find(id); + if (it != jobsIndex_.end()) // Should always be true, already tested in GetStateInternal() + { + ErrorCode code = it->second->GetLastStatus().GetErrorCode(); + const std::string& details = it->second->GetLastStatus().GetDetails(); + + if (details.empty()) + { + throw OrthancException(code); + } + else + { + throw OrthancException(code, details); + } + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + else if (state == JobState_Success) + { + // Success, try and retrieve the status of the job + JobsIndex::const_iterator it = jobsIndex_.find(id); + if (it == jobsIndex_.end()) + { + // Should not happen + state = JobState_Failure; + } + else + { + const JobStatus& status = it->second->GetLastStatus(); + successContent = status.GetPublicContent(); + } + + return; + } + else + { + // This job has not finished yet, wait for new completion + someJobComplete_.wait(lock); + } + } + } + } + + + bool JobsRegistry::SetPriority(const std::string& id, + int priority) + { + LOG(INFO) << "Changing priority to " << priority << " for job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + found->second->SetPriority(priority); + + if (found->second->GetState() == JobState_Pending) + { + // If the job is pending, we need to reconstruct the + // priority queue, as the heap condition has changed + + PendingJobs copy; + std::swap(copy, pendingJobs_); + + assert(pendingJobs_.empty()); + while (!copy.empty()) + { + pendingJobs_.push(copy.top()); + copy.pop(); + } + } + + CheckInvariants(); + return true; + } + } + + + void JobsRegistry::RemovePendingJob(const std::string& id) + { + // If the job is pending, we need to reconstruct the priority + // queue to remove it + PendingJobs copy; + std::swap(copy, pendingJobs_); + + assert(pendingJobs_.empty()); + while (!copy.empty()) + { + if (copy.top()->GetId() != id) + { + pendingJobs_.push(copy.top()); + } + + copy.pop(); + } + } + + + void JobsRegistry::RemoveRetryJob(JobHandler* handler) + { + RetryJobs::iterator item = retryJobs_.find(handler); + assert(item != retryJobs_.end()); + retryJobs_.erase(item); + } + + + bool JobsRegistry::Pause(const std::string& id) + { + LOG(INFO) << "Pausing job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + switch (found->second->GetState()) + { + case JobState_Pending: + RemovePendingJob(id); + found->second->SetState(JobState_Paused); + break; + + case JobState_Retry: + RemoveRetryJob(found->second); + found->second->SetState(JobState_Paused); + break; + + case JobState_Paused: + case JobState_Success: + case JobState_Failure: + // Nothing to be done + break; + + case JobState_Running: + found->second->SchedulePause(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Cancel(const std::string& id) + { + LOG(INFO) << "Canceling job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + switch (found->second->GetState()) + { + case JobState_Pending: + RemovePendingJob(id); + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Retry: + RemoveRetryJob(found->second); + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Paused: + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Success: + case JobState_Failure: + // Nothing to be done + break; + + case JobState_Running: + found->second->ScheduleCancel(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + // WARNING: The following call might make "handler" invalid if + // the job history size is empty + ForgetOldCompletedJobs(); + + return true; + } + } + + + bool JobsRegistry::Resume(const std::string& id) + { + LOG(INFO) << "Resuming job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else if (found->second->GetState() != JobState_Paused) + { + LOG(WARNING) << "Cannot resume a job that is not paused: " << id; + return false; + } + else + { + found->second->SetState(JobState_Pending); + pendingJobs_.push(found->second); + pendingJobAvailable_.notify_one(); + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Resubmit(const std::string& id) + { + LOG(INFO) << "Resubmitting failed job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else if (found->second->GetState() != JobState_Failure) + { + LOG(WARNING) << "Cannot resubmit a job that has not failed: " << id; + return false; + } + else + { + found->second->GetJob().Reset(); + + bool ok = false; + for (CompletedJobs::iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + if (*it == found->second) + { + ok = true; + completedJobs_.erase(it); + break; + } + } + + (void) ok; // Remove warning about unused variable in release builds + assert(ok); + + found->second->ResetRuntime(); + found->second->SetState(JobState_Pending); + pendingJobs_.push(found->second); + pendingJobAvailable_.notify_one(); + + CheckInvariants(); + return true; + } + } + + + void JobsRegistry::ScheduleRetries() + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + RetryJobs copy; + std::swap(copy, retryJobs_); + + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + + assert(retryJobs_.empty()); + for (RetryJobs::iterator it = copy.begin(); it != copy.end(); ++it) + { + if ((*it)->IsRetryReady(now)) + { + LOG(INFO) << "Retrying job: " << (*it)->GetId(); + (*it)->SetState(JobState_Pending); + pendingJobs_.push(*it); + pendingJobAvailable_.notify_one(); + } + else + { + retryJobs_.insert(*it); + } + } + + CheckInvariants(); + } + + + bool JobsRegistry::GetState(JobState& state, + const std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + return GetStateInternal(state, id); + } + + + void JobsRegistry::SetObserver(JobsRegistry::IObserver& observer) + { + boost::mutex::scoped_lock lock(mutex_); + observer_ = &observer; + } + + + void JobsRegistry::ResetObserver() + { + boost::mutex::scoped_lock lock(mutex_); + observer_ = NULL; + } + + + JobsRegistry::RunningJob::RunningJob(JobsRegistry& registry, + unsigned int timeout) : + registry_(registry), + handler_(NULL), + targetState_(JobState_Failure), + targetRetryTimeout_(0), + canceled_(false) + { + { + boost::mutex::scoped_lock lock(registry_.mutex_); + + while (registry_.pendingJobs_.empty()) + { + if (timeout == 0) + { + registry_.pendingJobAvailable_.wait(lock); + } + else + { + bool success = registry_.pendingJobAvailable_.timed_wait + (lock, boost::posix_time::milliseconds(timeout)); + if (!success) + { + // No pending job + return; + } + } + } + + handler_ = registry_.pendingJobs_.top(); + registry_.pendingJobs_.pop(); + + assert(handler_->GetState() == JobState_Pending); + handler_->SetState(JobState_Running); + handler_->SetLastErrorCode(ErrorCode_Success); + + job_ = &handler_->GetJob(); + id_ = handler_->GetId(); + priority_ = handler_->GetPriority(); + } + } + + + JobsRegistry::RunningJob::~RunningJob() + { + if (IsValid()) + { + boost::mutex::scoped_lock lock(registry_.mutex_); + + switch (targetState_) + { + case JobState_Failure: + registry_.MarkRunningAsCompleted + (*handler_, canceled_ ? CompletedReason_Canceled : CompletedReason_Failure); + break; + + case JobState_Success: + registry_.MarkRunningAsCompleted(*handler_, CompletedReason_Success); + break; + + case JobState_Paused: + registry_.MarkRunningAsPaused(*handler_); + break; + + case JobState_Retry: + registry_.MarkRunningAsRetry(*handler_, targetRetryTimeout_); + break; + + default: + assert(0); + } + } + } + + + bool JobsRegistry::RunningJob::IsValid() const + { + return (handler_ != NULL && + job_ != NULL); + } + + + const std::string& JobsRegistry::RunningJob::GetId() const + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return id_; + } + } + + + int JobsRegistry::RunningJob::GetPriority() const + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return priority_; + } + } + + + IJob& JobsRegistry::RunningJob::GetJob() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *job_; + } + } + + + bool JobsRegistry::RunningJob::IsPauseScheduled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + return handler_->IsPauseScheduled(); + } + } + + + bool JobsRegistry::RunningJob::IsCancelScheduled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + return handler_->IsCancelScheduled(); + } + } + + + void JobsRegistry::RunningJob::MarkSuccess() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Success; + } + } + + + void JobsRegistry::RunningJob::MarkFailure() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Failure; + } + } + + + void JobsRegistry::RunningJob::MarkCanceled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Failure; + canceled_ = true; + } + } + + + void JobsRegistry::RunningJob::MarkPause() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Paused; + } + } + + + void JobsRegistry::RunningJob::MarkRetry(unsigned int timeout) + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Retry; + targetRetryTimeout_ = timeout; + } + } + + + void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code, + const std::string& details) + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + JobStatus status(code, details, *job_); + + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + handler_->SetLastStatus(status); + } + } + + + + void JobsRegistry::Serialize(Json::Value& target) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + target = Json::objectValue; + target[TYPE] = JOBS_REGISTRY; + target[JOBS] = Json::objectValue; + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + Json::Value v; + if (it->second->Serialize(v)) + { + target[JOBS][it->first] = v; + } + } + } + + + JobsRegistry::JobsRegistry(IJobUnserializer& unserializer, + const Json::Value& s, + size_t maxCompletedJobs) : + maxCompletedJobs_(maxCompletedJobs), + observer_(NULL) + { + if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY || + !s.isMember(JOBS) || + s[JOBS].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value::Members members = s[JOBS].getMemberNames(); + + for (Json::Value::Members::const_iterator it = members.begin(); + it != members.end(); ++it) + { + std::unique_ptr job; + + try + { + job.reset(new JobHandler(unserializer, s[JOBS][*it], *it)); + } + catch (OrthancException& e) + { + LOG(WARNING) << "Cannot unserialize one job from previous execution, " + << "skipping it: " << e.What(); + continue; + } + + const boost::posix_time::ptime lastChangeTime = job->GetLastStateChangeTime(); + + std::string id; + SubmitInternal(id, job.release()); + + // Check whether the job has not been removed (which could be + // the case if the "maxCompletedJobs_" value gets smaller) + JobsIndex::iterator found = jobsIndex_.find(id); + if (found != jobsIndex_.end()) + { + // The job still lies in the history: Update the time of its + // last change to the time that was serialized + assert(found->second != NULL); + found->second->SetLastStateChangeTime(lastChangeTime); + } + } + } + + + void JobsRegistry::GetStatistics(unsigned int& pending, + unsigned int& running, + unsigned int& success, + unsigned int& failed) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + pending = 0; + running = 0; + success = 0; + failed = 0; + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + JobHandler& job = *it->second; + + switch (job.GetState()) + { + case JobState_Retry: + case JobState_Pending: + pending ++; + break; + + case JobState_Paused: + case JobState_Running: + running ++; + break; + + case JobState_Success: + success ++; + break; + + case JobState_Failure: + failed ++; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } +} diff --git a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h new file mode 100644 index 0000000..b4229c6 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h @@ -0,0 +1,248 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The job engine cannot be used in sandboxed environments +#endif + +#include "JobInfo.h" +#include "IJobUnserializer.h" + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + // This class handles the state machine of the jobs engine + class ORTHANC_PUBLIC JobsRegistry : public boost::noncopyable + { + public: + class ORTHANC_PUBLIC IObserver : public boost::noncopyable + { + public: + virtual ~IObserver() + { + } + + virtual void SignalJobSubmitted(const std::string& jobId) = 0; + + virtual void SignalJobSuccess(const std::string& jobId) = 0; + + virtual void SignalJobFailure(const std::string& jobId) = 0; + }; + + private: + enum CompletedReason + { + CompletedReason_Success, + CompletedReason_Failure, + CompletedReason_Canceled + }; + + class JobHandler; + + struct PriorityComparator + { + bool operator() (JobHandler* const& a, + JobHandler* const& b) const; + }; + + typedef std::map JobsIndex; + typedef std::list CompletedJobs; + typedef std::set RetryJobs; + typedef std::priority_queue, // Could be a "std::deque" + PriorityComparator> PendingJobs; + + boost::mutex mutex_; + JobsIndex jobsIndex_; + PendingJobs pendingJobs_; + CompletedJobs completedJobs_; + RetryJobs retryJobs_; + + boost::condition_variable pendingJobAvailable_; + boost::condition_variable someJobComplete_; + size_t maxCompletedJobs_; + + IObserver* observer_; + + +#ifndef NDEBUG + bool IsPendingJob(const JobHandler& job) const; + + bool IsCompletedJob(JobHandler& job) const; + + bool IsRetryJob(JobHandler& job) const; +#endif + + void CheckInvariants() const; + + void ForgetOldCompletedJobs(); + + void SetCompletedJob(JobHandler& job, + bool success); + + void MarkRunningAsCompleted(JobHandler& job, + CompletedReason reason); + + void MarkRunningAsRetry(JobHandler& job, + unsigned int timeout); + + void MarkRunningAsPaused(JobHandler& job); + + bool GetStateInternal(JobState& state, + const std::string& id); + + void RemovePendingJob(const std::string& id); + + void RemoveRetryJob(JobHandler* handler); + + void SubmitInternal(std::string& id, + JobHandler* handler); + + public: + explicit JobsRegistry(size_t maxCompletedJobs); + + JobsRegistry(IJobUnserializer& unserializer, + const Json::Value& s, + size_t maxCompletedJobs); + + ~JobsRegistry(); + + void SetMaxCompletedJobs(size_t i); + + size_t GetMaxCompletedJobs(); + + void ListJobs(std::set& target); + + bool GetJobInfo(JobInfo& target, + const std::string& id); + + bool DeleteJobInfo(const std::string& id); + + bool GetJobOutput(std::string& output, + MimeType& mime, + std::string& filename, + const std::string& job, + const std::string& key); + + bool DeleteJobOutput(const std::string& job, + const std::string& key); + + void Serialize(Json::Value& target); + + void Submit(std::string& id, + IJob* job, // Takes ownership + int priority); + + void Submit(IJob* job, // Takes ownership + int priority); + + void SubmitAndWait(Json::Value& successContent, + IJob* job, // Takes ownership + int priority); + + bool SetPriority(const std::string& id, + int priority); + + bool Pause(const std::string& id); + + bool Resume(const std::string& id); + + bool Resubmit(const std::string& id); + + bool Cancel(const std::string& id); + + void ScheduleRetries(); + + bool GetState(JobState& state, + const std::string& id); + + void SetObserver(IObserver& observer); + + void ResetObserver(); + + void GetStatistics(unsigned int& pending, + unsigned int& running, + unsigned int& success, + unsigned int& errors); + + class ORTHANC_PUBLIC RunningJob : public boost::noncopyable + { + private: + JobsRegistry& registry_; + JobHandler* handler_; // Can only be accessed if the + // registry mutex is locked! + IJob* job_; // Will by design be in mutual exclusion, + // because only one RunningJob can be + // executed at a time on a JobHandler + + std::string id_; + int priority_; + JobState targetState_; + unsigned int targetRetryTimeout_; + bool canceled_; + + public: + RunningJob(JobsRegistry& registry, + unsigned int timeout); + + ~RunningJob(); + + bool IsValid() const; + + const std::string& GetId() const; + + int GetPriority() const; + + IJob& GetJob(); + + bool IsPauseScheduled(); + + bool IsCancelScheduled(); + + void MarkSuccess(); + + void MarkFailure(); + + void MarkPause(); + + void MarkCanceled(); + + void MarkRetry(unsigned int timeout); + + void UpdateStatus(ErrorCode code, + const std::string& details); + }; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h new file mode 100644 index 0000000..4601546 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h @@ -0,0 +1,43 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "JobOperationValues.h" + +namespace Orthanc +{ + class ORTHANC_PUBLIC IJobOperation : public boost::noncopyable + { + public: + virtual ~IJobOperation() + { + } + + virtual void Apply(JobOperationValues& outputs, + const IJobOperationValue& input) = 0; + + virtual void Serialize(Json::Value& result) const = 0; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h new file mode 100644 index 0000000..b665d5c --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperationValue.h @@ -0,0 +1,54 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../../OrthancFramework.h" + +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC IJobOperationValue : public boost::noncopyable + { + public: + enum Type + { + Type_DicomInstance, + Type_Null, + Type_String + }; + + virtual ~IJobOperationValue() + { + } + + virtual Type GetType() const = 0; + + virtual IJobOperationValue* Clone() const = 0; + + virtual void Serialize(Json::Value& target) const = 0; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp new file mode 100644 index 0000000..ba5cc19 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp @@ -0,0 +1,167 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "JobOperationValues.h" + +#include "../IJobUnserializer.h" +#include "../../OrthancException.h" + +#include +#include + +namespace Orthanc +{ +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 + void JobOperationValues::Append(JobOperationValue* value) + { + throw OrthancException(ErrorCode_DiscontinuedAbi, "Removed in 1.8.1"); + } +#endif + + + void JobOperationValues::Append(JobOperationValues& target, + bool clear) + { + target.Reserve(target.GetSize() + GetSize()); + + for (size_t i = 0; i < values_.size(); i++) + { + if (clear) + { + target.Append(values_[i]); + values_[i] = NULL; + } + else + { + target.Append(GetValue(i).Clone()); + } + } + + if (clear) + { + Clear(); + } + } + + JobOperationValues::~JobOperationValues() + { + Clear(); + } + + void JobOperationValues::Move(JobOperationValues &target) + { + return Append(target, true); + } + + void JobOperationValues::Copy(JobOperationValues &target) + { + return Append(target, false); + } + + + void JobOperationValues::Clear() + { + for (size_t i = 0; i < values_.size(); i++) + { + if (values_[i] != NULL) + { + delete values_[i]; + } + } + + values_.clear(); + } + + void JobOperationValues::Reserve(size_t count) + { + values_.reserve(count); + } + + + void JobOperationValues::Append(IJobOperationValue* value) // Takes ownership + { + if (value == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + values_.push_back(value); + } + } + + size_t JobOperationValues::GetSize() const + { + return values_.size(); + } + + + IJobOperationValue& JobOperationValues::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(values_[index] != NULL); + return *values_[index]; + } + } + + + void JobOperationValues::Serialize(Json::Value& target) const + { + target = Json::arrayValue; + + for (size_t i = 0; i < values_.size(); i++) + { + Json::Value tmp; + values_[i]->Serialize(tmp); + target.append(tmp); + } + } + + + JobOperationValues* JobOperationValues::Unserialize(IJobUnserializer& unserializer, + const Json::Value& source) + { + if (source.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + std::unique_ptr result(new JobOperationValues); + + result->Reserve(source.size()); + + for (Json::Value::ArrayIndex i = 0; i < source.size(); i++) + { + result->Append(unserializer.UnserializeValue(source[i])); + } + + return result.release(); + } +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h new file mode 100644 index 0000000..49cd090 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h @@ -0,0 +1,83 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJobOperationValue.h" +#include "../../Compatibility.h" + +#include + +namespace Orthanc +{ + class IJobUnserializer; + +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 + class JobOperationValue + { + /** + * This is for ABI compatibility with Orthanc framework <= 1.8.0, + * only to be able to run unit tests from Orthanc 1.7.2 to + * 1.8.0. The class was moved to "IJobOperationValue" in 1.8.1, + * and its memory layout has changed. Don't use this anymore. + **/ + }; +#endif + + class ORTHANC_PUBLIC JobOperationValues : public boost::noncopyable + { + private: + std::vector values_; + +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 + // For binary compatibility with Orthanc <= 1.8.0 + ORTHANC_DEPRECATED(void Append(JobOperationValue* value)); +#endif + + void Append(JobOperationValues& target, + bool clear); + + public: + ~JobOperationValues(); + + void Move(JobOperationValues& target); + + void Copy(JobOperationValues& target); + + void Clear(); + + void Reserve(size_t count); + + void Append(IJobOperationValue* value); // Takes ownership + + size_t GetSize() const; + + IJobOperationValue& GetValue(size_t index) const; + + void Serialize(Json::Value& target) const; + + static JobOperationValues* Unserialize(IJobUnserializer& unserializer, + const Json::Value& source); + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp new file mode 100644 index 0000000..1c725f6 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp @@ -0,0 +1,60 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "LogJobOperation.h" + +#include "../../Logging.h" +#include "StringOperationValue.h" + +namespace Orthanc +{ + void LogJobOperation::Apply(JobOperationValues& outputs, + const IJobOperationValue& input) + { + switch (input.GetType()) + { + case IJobOperationValue::Type_String: + LOG(INFO) << "Job value: " + << dynamic_cast(input).GetContent(); + break; + + case IJobOperationValue::Type_Null: + LOG(INFO) << "Job value: (null)"; + break; + + default: + LOG(INFO) << "Job value: (unsupport)"; + break; + } + + outputs.Append(input.Clone()); + } + + void LogJobOperation::Serialize(Json::Value &result) const + { + result = Json::objectValue; + result["Type"] = "Log"; + } +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h new file mode 100644 index 0000000..157798a --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h @@ -0,0 +1,41 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJobOperation.h" + +#include "../../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + class ORTHANC_PUBLIC LogJobOperation : public IJobOperation + { + public: + virtual void Apply(JobOperationValues& outputs, + const IJobOperationValue& input) ORTHANC_OVERRIDE; + + virtual void Serialize(Json::Value& result) const ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.cpp b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.cpp new file mode 100644 index 0000000..6b7d414 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.cpp @@ -0,0 +1,46 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "NullOperationValue.h" + + +namespace Orthanc +{ + IJobOperationValue::Type NullOperationValue::GetType() const + { + return Type_Null; + } + + IJobOperationValue* NullOperationValue::Clone() const + { + return new NullOperationValue; + } + + void NullOperationValue::Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["Type"] = "Null"; + } +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h new file mode 100644 index 0000000..8bb1a0d --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h @@ -0,0 +1,42 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJobOperationValue.h" + +#include "../../Compatibility.h" // For ORTHANC_OVERRIDE + +namespace Orthanc +{ + class ORTHANC_PUBLIC NullOperationValue : public IJobOperationValue + { + public: + virtual Type GetType() const ORTHANC_OVERRIDE; + + virtual IJobOperationValue* Clone() const ORTHANC_OVERRIDE; + + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp new file mode 100644 index 0000000..462d19e --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp @@ -0,0 +1,511 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "SequenceOfOperationsJob.h" + +#include "../../Logging.h" +#include "../../OrthancException.h" +#include "../../SerializationToolbox.h" +#include "../IJobUnserializer.h" + +namespace Orthanc +{ + static const char* CURRENT = "Current"; + static const char* DESCRIPTION = "Description"; + static const char* NEXT_OPERATIONS = "Next"; + static const char* OPERATION = "Operation"; + static const char* OPERATIONS = "Operations"; + static const char* ORIGINAL_INPUTS = "OriginalInputs"; + static const char* TRAILING_TIMEOUT = "TrailingTimeout"; + static const char* TYPE = "Type"; + static const char* WORK_INPUTS = "WorkInputs"; + + + class SequenceOfOperationsJob::Operation : public boost::noncopyable + { + private: + size_t index_; + std::unique_ptr operation_; + std::unique_ptr originalInputs_; + std::unique_ptr workInputs_; + std::list nextOperations_; + size_t currentInput_; + + public: + Operation(size_t index, + IJobOperation* operation) : + index_(index), + operation_(operation), + originalInputs_(new JobOperationValues), + workInputs_(new JobOperationValues), + currentInput_(0) + { + if (operation == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + void AddOriginalInput(const IJobOperationValue& value) + { + if (currentInput_ != 0) + { + // Cannot add input after processing has started + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + originalInputs_->Append(value.Clone()); + } + } + + const JobOperationValues& GetOriginalInputs() const + { + return *originalInputs_; + } + + void Reset() + { + workInputs_->Clear(); + currentInput_ = 0; + } + + void AddNextOperation(Operation& other, + bool unserializing) + { + if (other.index_ <= index_) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (!unserializing && + currentInput_ != 0) + { + // Cannot add input after processing has started + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + nextOperations_.push_back(&other); + } + } + + bool IsDone() const + { + return currentInput_ >= originalInputs_->GetSize() + workInputs_->GetSize(); + } + + void Step() + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const IJobOperationValue* input; + + if (currentInput_ < originalInputs_->GetSize()) + { + input = &originalInputs_->GetValue(currentInput_); + } + else + { + input = &workInputs_->GetValue(currentInput_ - originalInputs_->GetSize()); + } + + JobOperationValues outputs; + operation_->Apply(outputs, *input); + + if (!nextOperations_.empty()) + { + std::list::iterator first = nextOperations_.begin(); + outputs.Move(*(*first)->workInputs_); + + std::list::iterator current = first; + ++current; + + while (current != nextOperations_.end()) + { + (*first)->workInputs_->Copy(*(*current)->workInputs_); + ++current; + } + } + + currentInput_ += 1; + } + + void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target[CURRENT] = static_cast(currentInput_); + operation_->Serialize(target[OPERATION]); + originalInputs_->Serialize(target[ORIGINAL_INPUTS]); + workInputs_->Serialize(target[WORK_INPUTS]); + + Json::Value tmp = Json::arrayValue; + for (std::list::const_iterator it = nextOperations_.begin(); + it != nextOperations_.end(); ++it) + { + tmp.append(static_cast((*it)->index_)); + } + + target[NEXT_OPERATIONS] = tmp; + } + + Operation(IJobUnserializer& unserializer, + Json::Value::ArrayIndex index, + const Json::Value& serialized) : + index_(index) + { + if (serialized.type() != Json::objectValue || + !serialized.isMember(OPERATION) || + !serialized.isMember(ORIGINAL_INPUTS) || + !serialized.isMember(WORK_INPUTS)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + currentInput_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT); + operation_.reset(unserializer.UnserializeOperation(serialized[OPERATION])); + originalInputs_.reset(JobOperationValues::Unserialize + (unserializer, serialized[ORIGINAL_INPUTS])); + workInputs_.reset(JobOperationValues::Unserialize + (unserializer, serialized[WORK_INPUTS])); + } + }; + + + SequenceOfOperationsJob::SequenceOfOperationsJob() : + done_(false), + current_(0), + trailingTimeout_(boost::posix_time::milliseconds(1000)) + { + } + + + SequenceOfOperationsJob::~SequenceOfOperationsJob() + { + for (size_t i = 0; i < operations_.size(); i++) + { + if (operations_[i] != NULL) + { + delete operations_[i]; + } + } + } + + + void SequenceOfOperationsJob::SetDescription(const std::string& description) + { + boost::mutex::scoped_lock lock(mutex_); + description_ = description; + } + + + void SequenceOfOperationsJob::GetDescription(std::string& description) + { + boost::mutex::scoped_lock lock(mutex_); + description = description_; + } + + + void SequenceOfOperationsJob::Register(IObserver& observer) + { + boost::mutex::scoped_lock lock(mutex_); + observers_.push_back(&observer); + } + + +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 + void SequenceOfOperationsJob::Lock::AddInput(size_t index, + const JobOperationValue& value) + { + throw OrthancException(ErrorCode_DiscontinuedAbi, "Removed in 1.8.1"); + } +#endif + + + SequenceOfOperationsJob::Lock::Lock(SequenceOfOperationsJob& that) : + that_(that), + lock_(that.mutex_) + { + } + + bool SequenceOfOperationsJob::Lock::IsDone() const + { + return that_.done_; + } + + void SequenceOfOperationsJob::Lock::SetTrailingOperationTimeout(unsigned int timeout) + { + that_.trailingTimeout_ = boost::posix_time::milliseconds(timeout); + } + + + size_t SequenceOfOperationsJob::Lock::AddOperation(IJobOperation* operation) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + size_t index = that_.operations_.size(); + + that_.operations_.push_back(new Operation(index, operation)); + that_.operationAdded_.notify_one(); + + return index; + } + + size_t SequenceOfOperationsJob::Lock::GetOperationsCount() const + { + return that_.operations_.size(); + } + + + void SequenceOfOperationsJob::Lock::AddInput(size_t index, + const IJobOperationValue& value) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (index >= that_.operations_.size() || + index < that_.current_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + that_.operations_[index]->AddOriginalInput(value); + } + } + + + void SequenceOfOperationsJob::Lock::Connect(size_t input, + size_t output) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (input >= output || + input >= that_.operations_.size() || + output >= that_.operations_.size() || + input < that_.current_ || + output < that_.current_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + Operation& a = *that_.operations_[input]; + Operation& b = *that_.operations_[output]; + a.AddNextOperation(b, false /* not unserializing */); + } + } + + + void SequenceOfOperationsJob::Start() + { + } + + + JobStepResult SequenceOfOperationsJob::Step(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + if (current_ == operations_.size()) + { + LOG(INFO) << "Executing the trailing timeout in the sequence of operations"; + operationAdded_.timed_wait(lock, trailingTimeout_); + + if (current_ == operations_.size()) + { + // No operation was added during the trailing timeout: The + // job is over + LOG(INFO) << "The sequence of operations is over"; + done_ = true; + + for (std::list::iterator it = observers_.begin(); + it != observers_.end(); ++it) + { + (*it)->SignalDone(*this); + } + + return JobStepResult::Success(); + } + else + { + LOG(INFO) << "New operation were added to the sequence of operations"; + } + } + + assert(current_ < operations_.size()); + + while (current_ < operations_.size() && + operations_[current_]->IsDone()) + { + current_++; + } + + if (current_ < operations_.size()) + { + operations_[current_]->Step(); + } + + return JobStepResult::Continue(); + } + + + void SequenceOfOperationsJob::Reset() + { + boost::mutex::scoped_lock lock(mutex_); + + current_ = 0; + done_ = false; + + for (size_t i = 0; i < operations_.size(); i++) + { + operations_[i]->Reset(); + } + } + + void SequenceOfOperationsJob::Stop(JobStopReason reason) + { + } + + + float SequenceOfOperationsJob::GetProgress() const + { + boost::mutex::scoped_lock lock(mutex_); + + return (static_cast(current_) / + static_cast(operations_.size() + 1)); + } + + void SequenceOfOperationsJob::GetJobType(std::string& target) const + { + target = "SequenceOfOperations"; + } + + + void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) const + { + boost::mutex::scoped_lock lock(mutex_); + + value["CountOperations"] = static_cast(operations_.size()); + value["Description"] = description_; + } + + + bool SequenceOfOperationsJob::Serialize(Json::Value& value) const + { + boost::mutex::scoped_lock lock(mutex_); + + value = Json::objectValue; + + std::string jobType; + GetJobType(jobType); + value[TYPE] = jobType; + + value[DESCRIPTION] = description_; + value[TRAILING_TIMEOUT] = static_cast(trailingTimeout_.total_milliseconds()); + value[CURRENT] = static_cast(current_); + + Json::Value tmp = Json::arrayValue; + for (size_t i = 0; i < operations_.size(); i++) + { + Json::Value operation = Json::objectValue; + operations_[i]->Serialize(operation); + tmp.append(operation); + } + + value[OPERATIONS] = tmp; + + return true; + } + + + void SequenceOfOperationsJob::AwakeTrailingSleep() + { + operationAdded_.notify_one(); + } + + + SequenceOfOperationsJob::SequenceOfOperationsJob(IJobUnserializer& unserializer, + const Json::Value& serialized) : + done_(false) + { + std::string jobType; + GetJobType(jobType); + + if (SerializationToolbox::ReadString(serialized, TYPE) != jobType || + !serialized.isMember(OPERATIONS) || + serialized[OPERATIONS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + description_ = SerializationToolbox::ReadString(serialized, DESCRIPTION); + trailingTimeout_ = boost::posix_time::milliseconds + (SerializationToolbox::ReadUnsignedInteger(serialized, TRAILING_TIMEOUT)); + current_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT); + + const Json::Value& ops = serialized[OPERATIONS]; + + // Unserialize the individual operations + operations_.reserve(ops.size()); + for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++) + { + operations_.push_back(new Operation(unserializer, i, ops[i])); + } + + // Connect the next operations + for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++) + { + if (!ops[i].isMember(NEXT_OPERATIONS) || + ops[i][NEXT_OPERATIONS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& next = ops[i][NEXT_OPERATIONS]; + for (Json::Value::ArrayIndex j = 0; j < next.size(); j++) + { + if (next[j].type() != Json::intValue || + next[j].asInt() < 0 || + next[j].asUInt() >= operations_.size()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + operations_[i]->AddNextOperation(*operations_[next[j].asUInt()], true); + } + } + } + } +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h new file mode 100644 index 0000000..56b7978 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h @@ -0,0 +1,143 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../IJob.h" +#include "IJobOperation.h" + +#include "../../Compatibility.h" // For ORTHANC_OVERRIDE + +#include +#include + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC SequenceOfOperationsJob : public IJob + { + public: + class ORTHANC_PUBLIC IObserver : public boost::noncopyable + { + public: + virtual ~IObserver() + { + } + + virtual void SignalDone(const SequenceOfOperationsJob& job) = 0; + }; + + private: + class Operation; + + std::string description_; + bool done_; + mutable boost::mutex mutex_; + std::vector operations_; + size_t current_; + boost::condition_variable operationAdded_; + boost::posix_time::time_duration trailingTimeout_; + std::list observers_; + + void NotifyDone() const; + + public: + SequenceOfOperationsJob(); + + SequenceOfOperationsJob(IJobUnserializer& unserializer, + const Json::Value& serialized); + + virtual ~SequenceOfOperationsJob(); + + void SetDescription(const std::string& description); + + void GetDescription(std::string& description); + + void Register(IObserver& observer); + + // This lock allows adding new operations to the end of the job, + // from another thread than the worker thread, after the job has + // been submitted for processing + class ORTHANC_PUBLIC Lock : public boost::noncopyable + { + private: + SequenceOfOperationsJob& that_; + boost::mutex::scoped_lock lock_; + +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 + ORTHANC_DEPRECATED(void AddInput(size_t index, + const JobOperationValue& value)); +#endif + + public: + explicit Lock(SequenceOfOperationsJob& that); + + bool IsDone() const; + + void SetTrailingOperationTimeout(unsigned int timeout); + + size_t AddOperation(IJobOperation* operation); + + size_t GetOperationsCount() const; + + void AddInput(size_t index, + const IJobOperationValue& value); + + void Connect(size_t input, + size_t output); + }; + + virtual void Start() ORTHANC_OVERRIDE; + + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; + + virtual void Reset() ORTHANC_OVERRIDE; + + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; + + virtual float GetProgress() const ORTHANC_OVERRIDE; + + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE; + + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE; + + virtual bool GetOutput(std::string& output, + MimeType& mime, + std::string& filename, + const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + + void AwakeTrailingSleep(); + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.cpp b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.cpp new file mode 100644 index 0000000..f3f9489 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.cpp @@ -0,0 +1,57 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "StringOperationValue.h" + + +namespace Orthanc +{ + StringOperationValue::StringOperationValue(const std::string& content) : + content_(content) + { + } + + IJobOperationValue::Type StringOperationValue::GetType() const + { + return Type_String; + } + + IJobOperationValue* StringOperationValue::Clone() const + { + return new StringOperationValue(content_); + } + + const std::string& StringOperationValue::GetContent() const + { + return content_; + } + + void StringOperationValue::Serialize(Json::Value& target) const + { + target = Json::objectValue; + target["Type"] = "String"; + target["Content"] = content_; + } +} diff --git a/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h new file mode 100644 index 0000000..e143349 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h @@ -0,0 +1,51 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJobOperationValue.h" + +#include "../../Compatibility.h" // For ORTHANC_OVERRIDE + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC StringOperationValue : public IJobOperationValue + { + private: + std::string content_; + + public: + explicit StringOperationValue(const std::string& content); + + virtual Type GetType() const ORTHANC_OVERRIDE; + + virtual IJobOperationValue* Clone() const ORTHANC_OVERRIDE; + + const std::string& GetContent() const; + + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp new file mode 100644 index 0000000..9bc7647 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp @@ -0,0 +1,328 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "SetOfCommandsJob.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include +#include + +namespace Orthanc +{ + SetOfCommandsJob::SetOfCommandsJob() : + started_(false), + permissive_(false), + position_(0) + { + } + + + SetOfCommandsJob::~SetOfCommandsJob() + { + for (size_t i = 0; i < commands_.size(); i++) + { + assert(commands_[i] != NULL); + delete commands_[i]; + } + } + + size_t SetOfCommandsJob::GetPosition() const + { + return position_; + } + + void SetOfCommandsJob::SetDescription(const std::string &description) + { + description_ = description; + } + + const std::string& SetOfCommandsJob::GetDescription() const + { + return description_; + } + + void SetOfCommandsJob::Reserve(size_t size) + { + if (started_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + commands_.reserve(size); + } + } + + size_t SetOfCommandsJob::GetCommandsCount() const + { + return commands_.size(); + } + + + void SetOfCommandsJob::AddCommand(ICommand* command) + { + if (command == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (started_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + commands_.push_back(command); + } + } + + bool SetOfCommandsJob::IsPermissive() const + { + return permissive_; + } + + + void SetOfCommandsJob::SetPermissive(bool permissive) + { + if (started_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + permissive_ = permissive; + } + } + + + void SetOfCommandsJob::Reset() + { + if (started_) + { + position_ = 0; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + void SetOfCommandsJob::Start() + { + started_ = true; + } + + + float SetOfCommandsJob::GetProgress() const + { + if (commands_.empty()) + { + return 1; + } + else + { + return (static_cast(position_) / + static_cast(commands_.size())); + } + } + + bool SetOfCommandsJob::IsStarted() const + { + return started_; + } + + + const SetOfCommandsJob::ICommand& SetOfCommandsJob::GetCommand(size_t index) const + { + if (index >= commands_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(commands_[index] != NULL); + return *commands_[index]; + } + } + + + JobStepResult SetOfCommandsJob::Step(const std::string& jobId) + { + if (!started_) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (commands_.empty() && + position_ == 0) + { + // No command to handle: We're done + position_ = 1; + return JobStepResult::Success(); + } + + if (position_ >= commands_.size()) + { + // Already done + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + try + { + // Not at the trailing step: Handle the current command + if (!commands_[position_]->Execute(jobId)) + { + // Error + if (!permissive_) + { + return JobStepResult::Failure(ErrorCode_InternalError, NULL); + } + } + } + catch (OrthancException& e) + { + if (permissive_) + { + LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What(); + } + else + { + return JobStepResult::Failure(e); + } + } + + position_ += 1; + + if (position_ == commands_.size()) + { + // We're done + return JobStepResult::Success(); + } + else + { + return JobStepResult::Continue(); + } + } + + + + static const char* KEY_DESCRIPTION = "Description"; + static const char* KEY_PERMISSIVE = "Permissive"; + static const char* KEY_POSITION = "Position"; + static const char* KEY_TYPE = "Type"; + static const char* KEY_COMMANDS = "Commands"; + + + void SetOfCommandsJob::GetPublicContent(Json::Value& value) const + { + value[KEY_DESCRIPTION] = GetDescription(); + } + + + bool SetOfCommandsJob::Serialize(Json::Value& target) const + { + target = Json::objectValue; + + std::string type; + GetJobType(type); + target[KEY_TYPE] = type; + + target[KEY_PERMISSIVE] = permissive_; + target[KEY_POSITION] = static_cast(position_); + target[KEY_DESCRIPTION] = description_; + + target[KEY_COMMANDS] = Json::arrayValue; + Json::Value& tmp = target[KEY_COMMANDS]; + + for (size_t i = 0; i < commands_.size(); i++) + { + assert(commands_[i] != NULL); + + Json::Value command; + commands_[i]->Serialize(command); + tmp.append(command); + } + + return true; + } + + + SetOfCommandsJob::SetOfCommandsJob(ICommandUnserializer* unserializer, + const Json::Value& source) : + started_(false) + { + std::unique_ptr raii(unserializer); + + permissive_ = SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE); + position_ = SerializationToolbox::ReadUnsignedInteger(source, KEY_POSITION); + description_ = SerializationToolbox::ReadString(source, KEY_DESCRIPTION); + + if (!source.isMember(KEY_COMMANDS) || + source[KEY_COMMANDS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + const Json::Value& tmp = source[KEY_COMMANDS]; + commands_.resize(tmp.size()); + + for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++) + { + try + { + commands_[i] = unserializer->Unserialize(tmp[i]); + } + catch (OrthancException&) + { + } + + if (commands_[i] == NULL) + { + for (size_t j = 0; j < i; j++) + { + delete commands_[j]; + } + + throw OrthancException(ErrorCode_BadFileFormat); + } + } + } + + if (commands_.empty()) + { + if (position_ > 1) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + else if (position_ > commands_.size()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } +} diff --git a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h new file mode 100644 index 0000000..02a344b --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h @@ -0,0 +1,120 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJob.h" + +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC SetOfCommandsJob : public IJob + { + public: + class ICommand : public boost::noncopyable + { + public: + virtual ~ICommand() + { + } + + virtual bool Execute(const std::string& jobId) = 0; + + virtual void Serialize(Json::Value& target) const = 0; + }; + + class ICommandUnserializer : public boost::noncopyable + { + public: + virtual ~ICommandUnserializer() + { + } + + virtual ICommand* Unserialize(const Json::Value& source) const = 0; + }; + + private: + bool started_; + std::vector commands_; + bool permissive_; + size_t position_; + std::string description_; + + public: + SetOfCommandsJob(); + + SetOfCommandsJob(ICommandUnserializer* unserializer /* takes ownership */, + const Json::Value& source); + + virtual ~SetOfCommandsJob(); + + size_t GetPosition() const; + + void SetDescription(const std::string& description); + + const std::string& GetDescription() const; + + void Reserve(size_t size); + + size_t GetCommandsCount() const; + + void AddCommand(ICommand* command); // Takes ownership + + bool IsPermissive() const; + + void SetPermissive(bool permissive); + + virtual void Reset() ORTHANC_OVERRIDE; + + virtual void Start() ORTHANC_OVERRIDE; + + virtual float GetProgress() const ORTHANC_OVERRIDE; + + bool IsStarted() const; + + const ICommand& GetCommand(size_t index) const; + + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; + + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; + + virtual bool GetOutput(std::string& output, + MimeType& mime, + std::string& filename, + const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + }; +} diff --git a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp new file mode 100644 index 0000000..eaa7ea0 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp @@ -0,0 +1,262 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "SetOfInstancesJob.h" + +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include + +namespace Orthanc +{ + class SetOfInstancesJob::InstanceCommand : public SetOfInstancesJob::ICommand + { + private: + SetOfInstancesJob& that_; + std::string instance_; + + public: + InstanceCommand(SetOfInstancesJob& that, + const std::string& instance) : + that_(that), + instance_(instance) + { + } + + const std::string& GetInstance() const + { + return instance_; + } + + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE + { + if (!that_.HandleInstance(instance_)) + { + that_.failedInstances_.insert(instance_); + return false; + } + else + { + return true; + } + } + + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE + { + target = instance_; + } + }; + + + class SetOfInstancesJob::TrailingStepCommand : public SetOfInstancesJob::ICommand + { + private: + SetOfInstancesJob& that_; + + public: + explicit TrailingStepCommand(SetOfInstancesJob& that) : + that_(that) + { + } + + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE + { + return that_.HandleTrailingStep(); + } + + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE + { + target = Json::nullValue; + } + }; + + + class SetOfInstancesJob::InstanceUnserializer : + public SetOfInstancesJob::ICommandUnserializer + { + private: + SetOfInstancesJob& that_; + + public: + explicit InstanceUnserializer(SetOfInstancesJob& that) : + that_(that) + { + } + + virtual ICommand* Unserialize(const Json::Value& source) const + { + if (source.type() == Json::nullValue) + { + return new TrailingStepCommand(that_); + } + else if (source.type() == Json::stringValue) + { + return new InstanceCommand(that_, source.asString()); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + }; + + + SetOfInstancesJob::SetOfInstancesJob() : + hasTrailingStep_(false) + { + } + + + void SetOfInstancesJob::AddParentResource(const std::string &resource) + { + parentResources_.insert(resource); + } + + + void SetOfInstancesJob::AddInstance(const std::string& instance) + { + AddCommand(new InstanceCommand(*this, instance)); + } + + + void SetOfInstancesJob::AddTrailingStep() + { + AddCommand(new TrailingStepCommand(*this)); + hasTrailingStep_ = true; + } + + + size_t SetOfInstancesJob::GetInstancesCount() const + { + if (hasTrailingStep_) + { + assert(GetCommandsCount() > 0); + return GetCommandsCount() - 1; + } + else + { + return GetCommandsCount(); + } + } + + + const std::string& SetOfInstancesJob::GetInstance(size_t index) const + { + if (index >= GetInstancesCount()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return dynamic_cast(GetCommand(index)).GetInstance(); + } + } + + bool SetOfInstancesJob::HasTrailingStep() const + { + return hasTrailingStep_; + } + + const std::set &SetOfInstancesJob::GetFailedInstances() const + { + return failedInstances_; + } + + bool SetOfInstancesJob::IsFailedInstance(const std::string &instance) const + { + return failedInstances_.find(instance) != failedInstances_.end(); + } + + + void SetOfInstancesJob::Start() + { + SetOfCommandsJob::Start(); + } + + + void SetOfInstancesJob::Reset() + { + SetOfCommandsJob::Reset(); + + failedInstances_.clear(); + } + + + static const char* KEY_TRAILING_STEP = "TrailingStep"; + static const char* KEY_FAILED_INSTANCES = "FailedInstances"; + static const char* KEY_PARENT_RESOURCES = "ParentResources"; + + void SetOfInstancesJob::GetPublicContent(Json::Value& target) const + { + SetOfCommandsJob::GetPublicContent(target); + target["InstancesCount"] = static_cast(GetInstancesCount()); + target["FailedInstancesCount"] = static_cast(failedInstances_.size()); + + if (!parentResources_.empty()) + { + SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES); + } + } + + + bool SetOfInstancesJob::Serialize(Json::Value& target) const + { + if (SetOfCommandsJob::Serialize(target)) + { + target[KEY_TRAILING_STEP] = hasTrailingStep_; + SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES); + SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES); + return true; + } + else + { + return false; + } + } + + + SetOfInstancesJob::SetOfInstancesJob(const Json::Value& source) : + SetOfCommandsJob(new InstanceUnserializer(*this), source) + { + SerializationToolbox::ReadSetOfStrings(failedInstances_, source, KEY_FAILED_INSTANCES); + + if (source.isMember(KEY_PARENT_RESOURCES)) + { + // Backward compatibility with Orthanc <= 1.5.6 + SerializationToolbox::ReadSetOfStrings(parentResources_, source, KEY_PARENT_RESOURCES); + } + + if (source.isMember(KEY_TRAILING_STEP)) + { + hasTrailingStep_ = SerializationToolbox::ReadBoolean(source, KEY_TRAILING_STEP); + } + else + { + // Backward compatibility with Orthanc <= 1.4.2 + hasTrailingStep_ = false; + } + } +} diff --git a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h new file mode 100644 index 0000000..0085bd9 --- /dev/null +++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h @@ -0,0 +1,84 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IJob.h" +#include "SetOfCommandsJob.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC SetOfInstancesJob : public SetOfCommandsJob + { + private: + class InstanceCommand; + class TrailingStepCommand; + class InstanceUnserializer; + + bool hasTrailingStep_; + std::set failedInstances_; + std::set parentResources_; + + protected: + virtual bool HandleInstance(const std::string& instance) = 0; + + virtual bool HandleTrailingStep() = 0; + + // Hiding this method, use AddInstance() instead + using SetOfCommandsJob::AddCommand; + + public: + SetOfInstancesJob(); + + explicit SetOfInstancesJob(const Json::Value& source); // Unserialization + + // Only used for reporting in the public content + // https://groups.google.com/d/msg/orthanc-users/9GCV88GLEzw/6wAgP_PRAgAJ + void AddParentResource(const std::string& resource); + + void AddInstance(const std::string& instance); + + void AddTrailingStep(); + + size_t GetInstancesCount() const; + + const std::string& GetInstance(size_t index) const; + + bool HasTrailingStep() const; + + const std::set& GetFailedInstances() const; + + bool IsFailedInstance(const std::string& instance) const; + + virtual void Start() ORTHANC_OVERRIDE; + + virtual void Reset() ORTHANC_OVERRIDE; + + virtual void GetPublicContent(Json::Value& target) const ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/Logging.cpp b/OrthancFramework/Sources/Logging.cpp new file mode 100644 index 0000000..efffc71 --- /dev/null +++ b/OrthancFramework/Sources/Logging.cpp @@ -0,0 +1,1070 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "Logging.h" + +#include "OrthancException.h" + +#include +#include + + +/********************************************************* + * Common section + *********************************************************/ + +namespace Orthanc +{ + namespace Logging + { + static const uint32_t ALL_CATEGORIES_MASK = 0xffffffff; + + static uint32_t infoCategoriesMask_ = 0; + static uint32_t traceCategoriesMask_ = 0; + static std::string logTargetFolder_; // keep a track of the log folder in case of reset of the context + static std::string logTargetFile_; // keep a track of the log file in case of reset of the context + + const char* EnumerationToString(LogLevel level) + { + switch (level) + { + case LogLevel_ERROR: + return "ERROR"; + + case LogLevel_WARNING: + return "WARNING"; + + case LogLevel_INFO: + return "INFO"; + + case LogLevel_TRACE: + return "TRACE"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + LogLevel StringToLogLevel(const char *level) + { + if (strcmp(level, "ERROR") == 0) + { + return LogLevel_ERROR; + } + else if (strcmp(level, "WARNING") == 0) + { + return LogLevel_WARNING; + } + else if (strcmp(level, "INFO") == 0) + { + return LogLevel_INFO; + } + else if (strcmp(level, "TRACE") == 0) + { + return LogLevel_TRACE; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + + void EnableInfoLevel(bool enabled) + { + if (enabled) + { + infoCategoriesMask_ = ALL_CATEGORIES_MASK; + } + else + { + // Also disable the "TRACE" level when info-level debugging is disabled + infoCategoriesMask_ = 0; + traceCategoriesMask_ = 0; + } + } + + + bool IsInfoLevelEnabled() + { + return (infoCategoriesMask_ != 0); + } + + + void EnableTraceLevel(bool enabled) + { + if (enabled) + { + // Also enable the "INFO" level when trace-level debugging is enabled + infoCategoriesMask_ = ALL_CATEGORIES_MASK; + traceCategoriesMask_ = ALL_CATEGORIES_MASK; + } + else + { + traceCategoriesMask_ = 0; + } + } + + + bool IsTraceLevelEnabled() + { + return (traceCategoriesMask_ != 0); + } + + + void SetCategoryEnabled(LogLevel level, + LogCategory category, + bool enabled) + { + // Invariant: If a bit is set for "trace", it must also be set + // for "verbose" (in other words, trace level implies verbose level) + assert((traceCategoriesMask_ & infoCategoriesMask_) == traceCategoriesMask_); + + if (level == LogLevel_INFO) + { + if (enabled) + { + infoCategoriesMask_ |= static_cast(category); + } + else + { + infoCategoriesMask_ &= ~static_cast(category); + traceCategoriesMask_ &= ~static_cast(category); + } + } + else if (level == LogLevel_TRACE) + { + if (enabled) + { + traceCategoriesMask_ |= static_cast(category); + infoCategoriesMask_ |= static_cast(category); + } + else + { + traceCategoriesMask_ &= ~static_cast(category); + } + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Can only modify the parameters of the INFO and TRACE levels"); + } + + assert((traceCategoriesMask_ & infoCategoriesMask_) == traceCategoriesMask_); + } + + + bool IsCategoryEnabled(LogLevel level, + LogCategory category) + { + if (level == LogLevel_ERROR || + level == LogLevel_WARNING) + { + return true; + } + else if (level == LogLevel_INFO) + { + return (infoCategoriesMask_ & category) != 0; + } + else if (level == LogLevel_TRACE) + { + return (traceCategoriesMask_ & category) != 0; + } + else + { + return false; + } + } + + + bool LookupCategory(LogCategory& target, + const std::string& category) + { + if (category == "generic") + { + target = LogCategory_GENERIC; + return true; + } + else if (category == "plugins") + { + target = LogCategory_PLUGINS; + return true; + } + else if (category == "http") + { + target = LogCategory_HTTP; + return true; + } + else if (category == "dicom") + { + target = LogCategory_DICOM; + return true; + } + else if (category == "sqlite") + { + target = LogCategory_SQLITE; + return true; + } + else if (category == "jobs") + { + target = LogCategory_JOBS; + return true; + } + else if (category == "lua") + { + target = LogCategory_LUA; + return true; + } + else + { + return false; + } + } + + + unsigned int GetCategoriesCount() + { + return 7; + } + + + const char* GetCategoryName(unsigned int i) + { + if (i < GetCategoriesCount()) + { + return GetCategoryName(static_cast(1 << i)); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* GetCategoryName(LogCategory category) + { + switch (category) + { + case LogCategory_GENERIC: + return "generic"; + + case LogCategory_PLUGINS: + return "plugins"; + + case LogCategory_HTTP: + return "http"; + + case LogCategory_DICOM: + return "dicom"; + + case LogCategory_SQLITE: + return "sqlite"; + + case LogCategory_JOBS: + return "jobs"; + + case LogCategory_LUA: + return "lua"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } +} + + + +#if ORTHANC_ENABLE_LOGGING != 1 + +/********************************************************* + * Section if logging is disabled + *********************************************************/ + +namespace Orthanc +{ + namespace Logging + { + void InitializePluginContext(void* pluginContext) + { + } + + void InitializePluginContext(void* pluginContext, const char* pluginName) + { + } + + void Initialize() + { + } + + void Finalize() + { + } + + void Reset() + { + } + + void Flush() + { + } + + void SetTargetFile(const std::string& path) + { + } + + void SetTargetFolder(const std::string& path) + { + } + } +} + + +#elif ORTHANC_ENABLE_LOGGING_STDIO == 1 + +/********************************************************* + * Logger compatible with OR logger that sends its + * output to the emscripten html5 api (depending on the + * definition of __EMSCRIPTEN__) + *********************************************************/ + +#include + +#ifdef __EMSCRIPTEN__ +# include +#endif + +namespace Orthanc +{ + namespace Logging + { +#ifdef __EMSCRIPTEN__ + static void ErrorLogFunc(const char* msg) + { + emscripten_console_error(msg); + } + + static void WarningLogFunc(const char* msg) + { + emscripten_console_warn(msg); + } + + static void InfoLogFunc(const char* msg) + { + emscripten_console_log(msg); + } + + static void TraceLogFunc(const char* msg) + { + emscripten_console_log(msg); + } +#else /* __EMSCRIPTEN__ not #defined */ + static void ErrorLogFunc(const char* msg) + { + fprintf(stderr, "E: %s\n", msg); + } + + static void WarningLogFunc(const char*) + { + fprintf(stdout, "W: %s\n", msg); + } + + static void InfoLogFunc(const char*) + { + fprintf(stdout, "I: %s\n", msg); + } + + static void TraceLogFunc(const char*) + { + fprintf(stdout, "T: %s\n", msg); + } +#endif /* __EMSCRIPTEN__ */ + + + InternalLogger::~InternalLogger() + { + std::string message = messageStream_.str(); + + if (IsCategoryEnabled(level_, category_)) + { + switch (level_) + { + case LogLevel_ERROR: + ErrorLogFunc(message.c_str()); + break; + + case LogLevel_WARNING: + WarningLogFunc(message.c_str()); + break; + + case LogLevel_INFO: + InfoLogFunc(message.c_str()); + // TODO: stone_console_info(message_.c_str()); + break; + + case LogLevel_TRACE: + TraceLogFunc(message.c_str()); + break; + + default: + { + std::stringstream ss; + ss << "Unknown log level (" << level_ << ") for message: " << message; + std::string s = ss.str(); + ErrorLogFunc(s.c_str()); + } + } + } + } + + void InitializePluginContext(void* pluginContext) + { + } + + void InitializePluginContext(void* pluginContext, const char* pluginName) + { + } + + void Initialize() + { + } + + void Finalize() + { + } + + void Reset() + { + } + + void Flush() + { + } + + void SetTargetFile(const std::string& path) + { + } + + void SetTargetFolder(const std::string& path) + { + } + } +} + + +#else + +/********************************************************* + * Logger compatible with the Orthanc plugin SDK, or that + * mimics behavior from Google Log. + *********************************************************/ + +#include +#include + +namespace +{ + /** + * This is minimal implementation of the context for an Orthanc + * plugin, limited to the logging facilities, and that is binary + * compatible with the definitions of "OrthancCPlugin.h" + **/ + typedef enum + { + _OrthancPluginService_LogInfo = 1, + _OrthancPluginService_LogWarning = 2, + _OrthancPluginService_LogError = 3, + _OrthancPluginService_LogMessage = 45, + _OrthancPluginService_INTERNAL = 0x7fffffff + } _OrthancPluginService; + + typedef struct _OrthancPluginContext_t + { + void* pluginsManager; + const char* orthancVersion; + void (*Free) (void* buffer); + int32_t (*InvokeService) (struct _OrthancPluginContext_t* context, + _OrthancPluginService service, + const void* params); + } OrthancPluginContext; + + typedef struct + { + const char* message; + const char* plugin; + const char* file; + uint32_t line; + uint32_t category; // can be a LogCategory or a OrthancPluginLogCategory + uint32_t level; // can be a LogLevel or a OrthancPluginLogLevel + } _OrthancPluginLogMessage; + +} + + +#include "Enumerations.h" +#include "SystemToolbox.h" +#include "Toolbox.h" + +#include +#include +#include +#include + + +namespace +{ + struct LoggingStreamsContext + { + std::string targetFile_; + std::string targetFolder_; + + std::ostream* error_; + std::ostream* warning_; + std::ostream* info_; + + std::unique_ptr file_; + + LoggingStreamsContext() : + error_(&std::cerr), + warning_(&std::cerr), + info_(&std::cerr) + { + } + }; +} + + + +static std::unique_ptr loggingStreamsContext_; +static boost::mutex loggingStreamsMutex_; +static Orthanc::Logging::NullStream nullStream_; +static OrthancPluginContext* pluginContext_ = NULL; // this is != NULL only when running from a plugin +static std::string pluginName_; // this string can only be non-empty if running from a plugin +static bool hasOrthancAdvancedLogging_ = false; // Whether the Orthanc runtime is >= 1.12.4 +static boost::recursive_mutex threadNamesMutex_; +static std::map threadNames_; +static bool enableThreadNames_ = true; + + + +namespace Orthanc +{ + namespace Logging + { + void EnableThreadNames(bool enabled) + { + enableThreadNames_ = enabled; + } + + static void GetLogPath(boost::filesystem::path& log, + boost::filesystem::path& link, + const std::string& suffix, + const std::string& directory) + { + /** + From Google Log documentation: + + Unless otherwise specified, logs will be written to the filename + "...log.", + followed by the date, time, and pid (you can't prevent the date, + time, and pid from being in the filename). + + In this implementation : "hostname" and "username" are not used + **/ + + boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); + boost::filesystem::path root(directory); + boost::filesystem::path exe(SystemToolbox::GetPathToExecutable()); + + if (!boost::filesystem::exists(root) || + !boost::filesystem::is_directory(root)) + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + + char date[64]; + sprintf(date, "%04d%02d%02d-%02d%02d%02d.%d", + static_cast(now.date().year()), + now.date().month().as_number(), + now.date().day().as_number(), + static_cast(now.time_of_day().hours()), + static_cast(now.time_of_day().minutes()), + static_cast(now.time_of_day().seconds()), + SystemToolbox::GetProcessId()); + + std::string programName = exe.filename().replace_extension("").string(); + + log = (root / (programName + ".log" + suffix + "." + std::string(date))); + link = (root / (programName + ".log" + suffix)); + } + + + static void PrepareLogFolder(std::unique_ptr& file, + const std::string& suffix, + const std::string& directory) + { + boost::filesystem::path log, link; + GetLogPath(log, link, suffix, directory); + +#if !defined(_WIN32) && (defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__))) + boost::filesystem::remove(link); + boost::filesystem::create_symlink(log.filename(), link); +#endif + + file.reset(new std::ofstream(log.string().c_str())); + } + + + // "loggingStreamsMutex_" must be locked + static void CheckFile(std::unique_ptr& f) + { + if (loggingStreamsContext_->file_.get() == NULL || + !loggingStreamsContext_->file_->is_open()) + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + } + + void SetCurrentThreadNameInternal(const boost::thread::id& id, const std::string& name) + { + boost::recursive_mutex::scoped_lock lock(threadNamesMutex_); + + if (name.size() > 16) + { + throw OrthancException(ErrorCode_InternalError, std::string("Thread name can not exceed 16 characters: ") + name); + } + + threadNames_[id] = name; + } + + void SetCurrentThreadName(const std::string& name) + { + boost::recursive_mutex::scoped_lock lock(threadNamesMutex_); + SetCurrentThreadNameInternal(boost::this_thread::get_id(), name); + } + + bool HasCurrentThreadName() + { + boost::thread::id threadId = boost::this_thread::get_id(); + + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + return threadNames_.find(threadId) != threadNames_.end(); + } + + static std::string GetCurrentThreadName() + { + boost::thread::id threadId = boost::this_thread::get_id(); + + boost::recursive_mutex::scoped_lock lock(threadNamesMutex_); + + if (threadNames_.find(threadId) == threadNames_.end()) + { + // set the threadId as the thread name + SetCurrentThreadNameInternal(threadId, boost::lexical_cast(threadId)); + } + + return threadNames_[threadId]; + } + + static void GetLinePrefix(std::string& prefix, + LogLevel level, + const char* pluginName, // when logging in the core but coming from a plugin, pluginName_ is NULL but this argument is != NULL + const char* file, + int line, + LogCategory category) + { + boost::filesystem::path path(file); + boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time(); + boost::posix_time::time_duration duration = now.time_of_day(); + + /** + From Google Log documentation: + + "Log lines have this form: + + Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg... + + where the fields are defined as follows: + + L A single character, representing the log level (eg 'I' for INFO) + mm The month (zero padded; ie May is '05') + dd The day (zero padded) + hh:mm:ss.uuuuuu Time in hours, minutes and fractional seconds + threadid The space-padded thread ID as returned by GetTID() (this matches the PID on Linux) + file The file name + line The line number + msg The user-supplied message" + + In this implementation, "threadid" is not printed. + **/ + + char c; + switch (level) + { + case LogLevel_ERROR: + c = 'E'; + break; + + case LogLevel_WARNING: + c = 'W'; + break; + + case LogLevel_INFO: + c = 'I'; + break; + + case LogLevel_TRACE: + c = 'T'; + break; + + default: + c = '?'; + break; + } + + char date[64]; + sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ", + c, + now.date().month().as_number(), + now.date().day().as_number(), + static_cast(duration.hours()), + static_cast(duration.minutes()), + static_cast(duration.seconds()), + static_cast(duration.fractional_seconds())); + + char threadName[20]; // thread names are limited to 16 char + a space + if (enableThreadNames_) + { + sprintf(threadName, "%16s ", GetCurrentThreadName().c_str()); + } + else + { + threadName[0] = '\0'; + } + + std::string internalPluginName = ""; + if (pluginName != NULL) + { + internalPluginName = std::string(pluginName) + ":/"; + } + + prefix = (std::string(date) + threadName + internalPluginName + path.filename().string() + ":" + + boost::lexical_cast(line) + "] "); + + if (level != LogLevel_ERROR && + level != LogLevel_WARNING && + category != LogCategory_GENERIC) + { + prefix += "(" + std::string(GetCategoryName(category)) + ") "; + } + } + + + void InitializePluginContext(void* pluginContext) + { + assert(sizeof(_OrthancPluginService) == sizeof(int32_t)); + + if (pluginContext == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + loggingStreamsContext_.reset(NULL); + pluginContext_ = reinterpret_cast(pluginContext); + + // The value "hasOrthancAdvancedLogging_" is cached to avoid computing it on every logged message + hasOrthancAdvancedLogging_ = Toolbox::IsVersionAbove(pluginContext_->orthancVersion, 1, 12, 4); + + EnableInfoLevel(true); // allow the plugin to log at info level (but the Orthanc Core still decides of the level) + } + + + void InitializePluginContext(void* pluginContext, const std::string& pluginName) + { + InitializePluginContext(pluginContext); + pluginName_ = pluginName; + } + + + void Initialize() + { + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + + if (loggingStreamsContext_.get() == NULL) + { + loggingStreamsContext_.reset(new LoggingStreamsContext); + } + } + + void Finalize() + { + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + loggingStreamsContext_.reset(NULL); + } + + void Reset() + { + { + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + loggingStreamsContext_.reset(new LoggingStreamsContext); + } + + // Recover the old logging context if any + if (!logTargetFile_.empty()) + { + SetTargetFile(logTargetFile_); + } + else if (!logTargetFolder_.empty()) + { + SetTargetFolder(logTargetFolder_); + } + } + + + void SetTargetFolder(const std::string& path) + { + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + if (loggingStreamsContext_.get() != NULL) + { + PrepareLogFolder(loggingStreamsContext_->file_, "" /* no suffix */, path); + CheckFile(loggingStreamsContext_->file_); + + loggingStreamsContext_->targetFile_.clear(); + loggingStreamsContext_->targetFolder_ = path; + loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get(); + loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get(); + loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get(); + logTargetFolder_ = path; + } + } + + + void SetTargetFile(const std::string& path) + { + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + + if (loggingStreamsContext_.get() != NULL) + { + loggingStreamsContext_->file_.reset(new std::ofstream(path.c_str(), std::fstream::app)); + CheckFile(loggingStreamsContext_->file_); + + loggingStreamsContext_->targetFile_ = path; + loggingStreamsContext_->targetFolder_.clear(); + loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get(); + loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get(); + loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get(); + logTargetFile_ = path; + } + } + + + InternalLogger::InternalLogger(LogLevel level, + LogCategory category, + const char* pluginName, + const char* file, + int line) : + lock_(loggingStreamsMutex_, boost::defer_lock_t()), + level_(level), + stream_(&nullStream_), // By default, logging to "/dev/null" is simulated + category_(category), + file_(file), + line_(line) + { + if (pluginContext_ != NULL) + { + // We are logging using the Orthanc plugin SDK + + if (level_ == LogLevel_TRACE || + !IsCategoryEnabled(level_, category)) + { + // No trace level in plugins, directly exit as the stream is + // set to "/dev/null" + return; + } + else + { + pluginStream_.reset(new std::stringstream); + stream_ = pluginStream_.get(); + } + } + else + { + // We are logging in a standalone application, not inside an Orthanc plugin + + if (!IsCategoryEnabled(level_, category)) + { + // This logging level is disabled, directly exit as the + // stream is set to "/dev/null" + return; + } + + std::string prefix; + GetLinePrefix(prefix, level_, pluginName, file, line, category); + + { + // We lock the global mutex. The mutex is locked until the + // destructor is called: No change in the output can be done. + lock_.lock(); + + if (loggingStreamsContext_.get() == NULL) + { + // Have you called Orthanc::Logging::InitializePluginContext()? + fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine " + "(or did you forgot to initialize it?)\n"); + lock_.unlock(); + return; + } + + switch (level_) + { + case LogLevel_ERROR: + stream_ = loggingStreamsContext_->error_; + break; + + case LogLevel_WARNING: + stream_ = loggingStreamsContext_->warning_; + break; + + case LogLevel_INFO: + case LogLevel_TRACE: + stream_ = loggingStreamsContext_->info_; + break; + + default: // Should not occur + stream_ = loggingStreamsContext_->error_; + break; + } + + if (stream_ == &nullStream_) + { + // The logging is disabled for this level, we can release + // the global mutex. + lock_.unlock(); + } + else + { + try + { + (*stream_) << prefix; + } + catch (...) + { + // Something is going really wrong, probably running out of + // memory. Fallback to a degraded mode. + stream_ = loggingStreamsContext_->error_; + (*stream_) << "E???? ??:??:??.?????? ] "; + } + } + } + } + } + + + + InternalLogger::~InternalLogger() + { + if (pluginStream_.get() != NULL) + { + // We are logging through the Orthanc SDK + + std::string message = pluginStream_->str(); + + if (pluginContext_ != NULL) + { + if (!pluginName_.empty() && + hasOrthancAdvancedLogging_) + { + _OrthancPluginLogMessage m; + m.category = category_; + m.level = level_; + m.file = file_; + m.line = line_; + m.plugin = pluginName_.c_str(); + m.message = message.c_str(); + pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogMessage, &m); + } + else + { + switch (level_) + { + case LogLevel_ERROR: + pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogError, message.c_str()); + break; + + case LogLevel_WARNING: + pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogWarning, message.c_str()); + break; + + case LogLevel_INFO: + pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str()); + break; + + default: + break; + } + } + } + } + else if (stream_ != &nullStream_) + { + *stream_ << "\n"; + stream_->flush(); + } + } + + + void Flush() + { + if (pluginContext_ != NULL) + { + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + + if (loggingStreamsContext_.get() != NULL && + loggingStreamsContext_->file_.get() != NULL) + { + loggingStreamsContext_->file_->flush(); + } + } + } + + + void SetErrorWarnInfoLoggingStreams(std::ostream& errorStream, + std::ostream& warningStream, + std::ostream& infoStream) + { + boost::mutex::scoped_lock lock(loggingStreamsMutex_); + + loggingStreamsContext_.reset(new LoggingStreamsContext); + loggingStreamsContext_->error_ = &errorStream; + loggingStreamsContext_->warning_ = &warningStream; + loggingStreamsContext_->info_ = &infoStream; + } + } +} + + +#endif // ORTHANC_ENABLE_LOGGING diff --git a/OrthancFramework/Sources/Logging.h b/OrthancFramework/Sources/Logging.h new file mode 100644 index 0000000..5412e15 --- /dev/null +++ b/OrthancFramework/Sources/Logging.h @@ -0,0 +1,310 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +// To have ORTHANC_ENABLE_LOGGING defined if using the shared library +#include "OrthancFramework.h" +#include "Compatibility.h" + +#include +#include + +#if !defined(ORTHANC_ENABLE_LOGGING) +# error The macro ORTHANC_ENABLE_LOGGING must be defined +#endif + +#if !defined(ORTHANC_ENABLE_LOGGING_STDIO) +# if ORTHANC_ENABLE_LOGGING == 1 +# error The macro ORTHANC_ENABLE_LOGGING_STDIO must be defined +# else +# define ORTHANC_ENABLE_LOGGING_STDIO 0 +# endif +#endif + + +namespace Orthanc +{ + namespace Logging + { + // Note: these values must match the ones in OrthancCPlugin.h + enum LogLevel + { + LogLevel_ERROR = 0, + LogLevel_WARNING = 1, + LogLevel_INFO = 2, + LogLevel_TRACE = 3 + }; + + /** + * NB: The log level for each category is encoded as a bit + * mask. As a consequence, there can be up to 31 log categories + * (not 32, as the value GENERIC is reserved for the log commands + * that don't fall in a specific category). + * Note: these values must match the ones in OrthancCPlugin.h + **/ + enum LogCategory + { + LogCategory_GENERIC = (1 << 0), + LogCategory_PLUGINS = (1 << 1), + LogCategory_HTTP = (1 << 2), + LogCategory_SQLITE = (1 << 3), + LogCategory_DICOM = (1 << 4), + LogCategory_JOBS = (1 << 5), + LogCategory_LUA = (1 << 6) + }; + + ORTHANC_PUBLIC const char* EnumerationToString(LogLevel level); + + ORTHANC_PUBLIC LogLevel StringToLogLevel(const char* level); + + // "pluginContext" must be of type "OrthancPluginContext" + ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext); + + ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext, + const std::string& pluginName); + + ORTHANC_PUBLIC void Initialize(); + + ORTHANC_PUBLIC void Finalize(); + + ORTHANC_PUBLIC void Reset(); + + ORTHANC_PUBLIC void Flush(); + + ORTHANC_PUBLIC void SetCurrentThreadName(const std::string& name); + + ORTHANC_PUBLIC bool HasCurrentThreadName(); + + ORTHANC_PUBLIC void EnableThreadNames(bool enabled); + + ORTHANC_PUBLIC void EnableInfoLevel(bool enabled); + + ORTHANC_PUBLIC void EnableTraceLevel(bool enabled); + + ORTHANC_PUBLIC bool IsTraceLevelEnabled(); + + ORTHANC_PUBLIC bool IsInfoLevelEnabled(); + + ORTHANC_PUBLIC void SetCategoryEnabled(LogLevel level, + LogCategory category, + bool enabled); + + ORTHANC_PUBLIC bool IsCategoryEnabled(LogLevel level, + LogCategory category); + + ORTHANC_PUBLIC bool LookupCategory(LogCategory& target, + const std::string& category); + + ORTHANC_PUBLIC unsigned int GetCategoriesCount(); + + ORTHANC_PUBLIC const char* GetCategoryName(unsigned int i); + + ORTHANC_PUBLIC const char* GetCategoryName(LogCategory category); + + ORTHANC_PUBLIC void SetTargetFile(const std::string& path); + + ORTHANC_PUBLIC void SetTargetFolder(const std::string& path); + + struct ORTHANC_LOCAL NullStream : public std::ostream + { + NullStream() : + std::ios(0), + std::ostream(0) + { + } + + template + std::ostream& operator<< (const T& message) + { + return *this; + } + }; + } +} + + + +/** + * NB: + * - The "VLOG(unused)" macro is for backward compatibility with + * Orthanc <= 1.8.0. + * - The "CLOG()" macro stands for "category log" (new in Orthanc 1.8.1) + **/ + +#if defined(LOG) +# error The macro LOG cannot be defined beforehand +#endif + +#if defined(VLOG) +# error The macro VLOG cannot be defined beforehand +#endif + +#if defined(CLOG) +# error The macro CLOG cannot be defined beforehand +#endif + +#if ORTHANC_ENABLE_LOGGING != 1 +# define LOG(level) ::Orthanc::Logging::NullStream() +# define VLOG(unused) ::Orthanc::Logging::NullStream() +# define CLOG(level, category) ::Orthanc::Logging::NullStream() +# define LOG_FROM_PLUGIN(level, category, pluginName, file, line) ::Orthanc::Logging::NullStream() +#else /* ORTHANC_ENABLE_LOGGING == 1 */ + +#if !defined(__ORTHANC_FILE__) +# if defined(_MSC_VER) +# pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries") +# else +# warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries +# endif +# define __ORTHANC_FILE__ __FILE__ +#endif + +# define LOG(level) ::Orthanc::Logging::InternalLogger \ + (::Orthanc::Logging::LogLevel_ ## level, \ + ::Orthanc::Logging::LogCategory_GENERIC, NULL /* no plugin */, \ + __ORTHANC_FILE__, __LINE__) + +# define VLOG(unused) ::Orthanc::Logging::InternalLogger \ + (::Orthanc::Logging::LogLevel_TRACE, \ + ::Orthanc::Logging::LogCategory_GENERIC, NULL /* no plugin */, \ + __ORTHANC_FILE__, __LINE__) + +# define CLOG(level, category) ::Orthanc::Logging::InternalLogger \ + (::Orthanc::Logging::LogLevel_ ## level, \ + ::Orthanc::Logging::LogCategory_ ## category, NULL /* no plugin */, \ + __ORTHANC_FILE__, __LINE__) + +# define LOG_FROM_PLUGIN(level, category, pluginName, file, line) \ + ::Orthanc::Logging::InternalLogger(level, category, pluginName, file, line) + +#endif + + + +#if (ORTHANC_ENABLE_LOGGING == 1 && \ + ORTHANC_ENABLE_LOGGING_STDIO == 1) +// This is notably for WebAssembly + +#include +#include +#include + +namespace Orthanc +{ + namespace Logging + { + class ORTHANC_PUBLIC InternalLogger : public boost::noncopyable + { + private: + LogLevel level_; + LogCategory category_; + std::stringstream messageStream_; + + public: + InternalLogger(LogLevel level, + LogCategory category, + const char* pluginName /* ignored */, + const char* file /* ignored */, + int line /* ignored */) : + level_(level), + category_(category) + { + } + + ~InternalLogger(); + + template + std::ostream& operator<< (const T& message) + { + return messageStream_ << boost::lexical_cast(message); + } + }; + } +} + +#endif + + + +#if (ORTHANC_ENABLE_LOGGING == 1 && \ + ORTHANC_ENABLE_LOGGING_STDIO == 0) + +#include +#include +#include +#include + +namespace Orthanc +{ + namespace Logging + { + class ORTHANC_PUBLIC InternalLogger : public boost::noncopyable + { + private: + boost::mutex::scoped_lock lock_; + LogLevel level_; + std::unique_ptr pluginStream_; + std::ostream* stream_; + LogCategory category_; + const char* file_; + uint32_t line_; + + public: + InternalLogger(LogLevel level, + LogCategory category, + const char* pluginName, + const char* file, + int line); + + ~InternalLogger(); + + template + std::ostream& operator<< (const T& message) + { + return (*stream_) << boost::lexical_cast(message); + } + }; + + /** + * Set custom logging streams for the error, warning and info + * logs. This function may not be called if a log file or folder + * has been set beforehand. All three references must be valid. + * + * Please ensure the supplied streams remain alive and valid as + * long as logging calls are performed. In order to prevent + * dangling pointer usage, it is mandatory to call + * Orthanc::Logging::Reset() before the stream objects are + * destroyed and the references become invalid. + * + * This function must only be used by unit tests. It is ignored if + * InitializePluginContext() was called. + **/ + ORTHANC_PUBLIC void SetErrorWarnInfoLoggingStreams(std::ostream& errorStream, + std::ostream& warningStream, + std::ostream& infoStream); + } +} + +#endif diff --git a/OrthancFramework/Sources/Lua/LuaContext.cpp b/OrthancFramework/Sources/Lua/LuaContext.cpp new file mode 100644 index 0000000..01aa9ac --- /dev/null +++ b/OrthancFramework/Sources/Lua/LuaContext.cpp @@ -0,0 +1,717 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "LuaContext.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include +#include +#include + +extern "C" +{ +#include +#include +} + +namespace Orthanc +{ + static bool OnlyContainsDigits(const std::string& s) + { + for (size_t i = 0; i < s.size(); i++) + { + if (!isdigit(s[i])) + { + return false; + } + } + + return true; + } + + LuaContext& LuaContext::GetLuaContext(lua_State *state) + { + const void* value = GetGlobalVariable(state, "_LuaContext"); + assert(value != NULL); + + return *const_cast(reinterpret_cast(value)); + } + + int LuaContext::PrintToLog(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // http://medek.wordpress.com/2009/02/03/wrapping-lua-errors-and-print-function/ + int nArgs = lua_gettop(state); + lua_getglobal(state, "tostring"); + + // Make sure you start at 1 *NOT* 0 for arrays in Lua. + std::string result; + + for (int i = 1; i <= nArgs; i++) + { + const char *s; + lua_pushvalue(state, -1); + lua_pushvalue(state, i); + lua_call(state, 1, 1); + s = lua_tostring(state, -1); + + if (result.size() > 0) + result.append(", "); + + if (s == NULL) + result.append(""); + else + result.append(s); + + lua_pop(state, 1); + } + + LOG(WARNING) << "Lua says: " << result; + that.log_.append(result); + that.log_.append("\n"); + + return 0; + } + + + int LuaContext::ParseJson(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + int nArgs = lua_gettop(state); + if (nArgs != 1 || + !lua_isstring(state, 1)) // Password + { + lua_pushnil(state); + return 1; + } + + const char* str = lua_tostring(state, 1); + + Json::Value value; + if (Toolbox::ReadJson(value, str, strlen(str))) + { + that.PushJson(value); + } + else + { + lua_pushnil(state); + } + + return 1; + } + + + int LuaContext::DumpJson(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + int nArgs = lua_gettop(state); + if ((nArgs != 1 && nArgs != 2) || + (nArgs == 2 && !lua_isboolean(state, 2))) + { + lua_pushnil(state); + return 1; + } + + bool keepStrings = false; + if (nArgs == 2) + { + keepStrings = lua_toboolean(state, 2) ? true : false; + } + + Json::Value json; + that.GetJson(json, state, 1, keepStrings); + + std::string s; + Toolbox::WriteFastJson(s, json); + lua_pushlstring(state, s.c_str(), s.size()); + + return 1; + } + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::SetHttpCredentials(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs != 2 || + !lua_isstring(state, 1) || // Username + !lua_isstring(state, 2)) // Password + { + LOG(ERROR) << "Lua: Bad parameters to SetHttpCredentials()"; + } + else + { + // Configure the HTTP client + const char* username = lua_tostring(state, 1); + const char* password = lua_tostring(state, 2); + that.httpClient_.SetCredentials(username, password); + } + + return 0; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::SetHttpTimeout(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs != 1 || + !lua_isnumber(state, 1)) // Timeout + { + LOG(ERROR) << "Lua: Bad parameters to SetHttpTimeout()"; + } + else + { + // Configure the HTTP client + // Convert to "int" if truncation does not loose precision + long timeout = static_cast(lua_tonumber(state, 1)); + + that.httpClient_.SetTimeout(timeout); + } + + return 0; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + bool LuaContext::AnswerHttpQuery(lua_State* state) + { + std::string str; + + try + { + httpClient_.Apply(str); + } + catch (OrthancException&) + { + return false; + } + + // Return the result of the HTTP request + lua_pushlstring(state, str.c_str(), str.size()); + + return true; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + void LuaContext::SetHttpHeaders(int top) + { + std::map headers; + GetDictionaryArgument(headers, lua_, top, false /* keep key case as provided by Lua script */); + + httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request + + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + httpClient_.AddHeader(it->first, it->second); + } + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpGet(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs < 1 || nArgs > 2 || // check args count + !lua_isstring(state, 1)) // URL is a string + { + LOG(ERROR) << "Lua: Bad parameters to HttpGet()"; + lua_pushnil(state); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(HttpMethod_Get); + that.httpClient_.SetUrl(url); + that.httpClient_.ClearBody(); + that.SetHttpHeaders(2); + + // Do the HTTP GET request + if (!that.AnswerHttpQuery(state)) + { + LOG(ERROR) << "Lua: Error in HttpGet() for URL " << url; + lua_pushnil(state); + } + + return 1; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpPostOrPut(lua_State *state, + HttpMethod method) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if ((nArgs < 1 || nArgs > 3) || // check arg count + !lua_isstring(state, 1) || // URL is a string + (nArgs >= 2 && (!lua_isstring(state, 2) && !lua_isnil(state, 2)))) // Body data is null or is a string + { + LOG(ERROR) << "Lua: Bad parameters to HttpPost() or HttpPut()"; + lua_pushnil(state); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(method); + that.httpClient_.SetUrl(url); + that.SetHttpHeaders(3); + + if (nArgs >= 2 && !lua_isnil(state, 2)) + { + size_t bodySize = 0; + const char* bodyData = lua_tolstring(state, 2, &bodySize); + + if (bodySize == 0) + { + that.httpClient_.ClearBody(); + } + else + { + that.httpClient_.AssignBody(bodyData, bodySize); + } + } + else + { + that.httpClient_.ClearBody(); + } + + // Do the HTTP POST/PUT request + if (!that.AnswerHttpQuery(state)) + { + LOG(ERROR) << "Lua: Error in HttpPost() or HttpPut() for URL " << url; + lua_pushnil(state); + } + + return 1; + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpPost(lua_State *state) + { + return CallHttpPostOrPut(state, HttpMethod_Post); + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpPut(lua_State *state) + { + return CallHttpPostOrPut(state, HttpMethod_Put); + } +#endif + + +#if ORTHANC_ENABLE_CURL == 1 + int LuaContext::CallHttpDelete(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs < 1 || nArgs > 2 || !lua_isstring(state, 1)) // URL + { + LOG(ERROR) << "Lua: Bad parameters to HttpDelete()"; + lua_pushnil(state); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(HttpMethod_Delete); + that.httpClient_.SetUrl(url); + that.httpClient_.ClearBody(); + that.SetHttpHeaders(2); + + // Do the HTTP DELETE request + std::string s; + if (!that.httpClient_.Apply(s)) + { + LOG(ERROR) << "Lua: Error in HttpDelete() for URL " << url; + lua_pushnil(state); + } + else + { + lua_pushstring(state, "SUCCESS"); + } + + return 1; + } +#endif + + + void LuaContext::PushJson(const Json::Value& value) + { + if (value.isString()) + { + const std::string s = value.asString(); + lua_pushlstring(lua_, s.c_str(), s.size()); + } + else if (value.isInt()) + { + lua_pushinteger(lua_, value.asInt()); + } + else if (value.isUInt()) + { + lua_pushinteger(lua_, value.asUInt()); + } + else if (value.isDouble()) + { + lua_pushnumber(lua_, value.asDouble()); + } + else if (value.isBool()) + { + lua_pushboolean(lua_, value.asBool()); + } + else if (value.isNull()) + { + lua_pushnil(lua_); + } + else if (value.isArray()) + { + lua_newtable(lua_); + + // http://lua-users.org/wiki/SimpleLuaApiExample + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + // Push the table index (note the "+1" because of Lua conventions) + lua_pushnumber(lua_, i + 1); + + // Push the value of the cell + PushJson(value[i]); + + // Stores the pair in the table + lua_rawset(lua_, -3); + } + } + else if (value.isObject()) + { + lua_newtable(lua_); + + Json::Value::Members members = value.getMemberNames(); + + for (Json::Value::Members::const_iterator + it = members.begin(); it != members.end(); ++it) + { + // Push the index of the cell + lua_pushlstring(lua_, it->c_str(), it->size()); + + // Push the value of the cell + PushJson(value[*it]); + + // Stores the pair in the table + lua_rawset(lua_, -3); + } + } + else + { + throw OrthancException(ErrorCode_JsonToLuaTable); + } + } + + + void LuaContext::GetJson(Json::Value& result, + lua_State* state, + int top, + bool keepStrings) + { + if (lua_istable(state, top)) + { + Json::Value tmp = Json::objectValue; + bool isArray = true; + size_t size = 0; + + // Code adapted from: http://stackoverflow.com/a/6142700/881731 + + // Push another reference to the table on top of the stack (so we know + // where it is, and this function can work for negative, positive and + // pseudo indices + lua_pushvalue(state, top); + // stack now contains: -1 => table + lua_pushnil(state); + // stack now contains: -1 => nil; -2 => table + while (lua_next(state, -2)) + { + // stack now contains: -1 => value; -2 => key; -3 => table + // copy the key so that lua_tostring does not modify the original + lua_pushvalue(state, -2); + // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table + std::string key(lua_tostring(state, -1)); + Json::Value v; + GetJson(v, state, -2, keepStrings); + + tmp[key] = v; + + size += 1; + try + { + if (!OnlyContainsDigits(key) || + boost::lexical_cast(key) != size) + { + isArray = false; + } + } + catch (boost::bad_lexical_cast&) + { + isArray = false; + } + + // pop value + copy of key, leaving original key + lua_pop(state, 2); + // stack now contains: -1 => key; -2 => table + } + // stack now contains: -1 => table (when lua_next returns 0 it pops the key + // but does not push anything.) + // Pop table + lua_pop(state, 1); + + // Stack is now the same as it was on entry to this function + + if (isArray) + { + result = Json::arrayValue; + for (size_t i = 0; i < size; i++) + { + result.append(tmp[boost::lexical_cast(i + 1)]); + } + } + else + { + result = tmp; + } + } + else if (lua_isnil(state, top)) + { + result = Json::nullValue; + } + else if (!keepStrings && + lua_isboolean(state, top)) + { + result = lua_toboolean(state, top) ? true : false; + } + else if (!keepStrings && + lua_isnumber(state, top)) + { + // Convert to "int" if truncation does not loose precision + double value = static_cast(lua_tonumber(state, top)); + int truncated = static_cast(value); + + if (std::abs(value - static_cast(truncated)) <= + std::numeric_limits::epsilon()) + { + result = truncated; + } + else + { + result = value; + } + } + else if (lua_isstring(state, top)) + { + // Caution: The "lua_isstring()" case must be the last, since + // Lua can convert most types to strings by default. + result = std::string(lua_tostring(state, top)); + } + else if (lua_isboolean(state, top)) + { + result = lua_toboolean(state, top) ? true : false; + } + else + { + LOG(WARNING) << "Unsupported Lua type when returning Json"; + result = Json::nullValue; + } + } + + + LuaContext::LuaContext() + { + lua_ = luaL_newstate(); + if (!lua_) + { + throw OrthancException(ErrorCode_CannotCreateLua); + } + + luaL_openlibs(lua_); + lua_register(lua_, "print", PrintToLog); + lua_register(lua_, "ParseJson", ParseJson); + lua_register(lua_, "DumpJson", DumpJson); + +#if ORTHANC_ENABLE_CURL == 1 + lua_register(lua_, "HttpGet", CallHttpGet); + lua_register(lua_, "HttpPost", CallHttpPost); + lua_register(lua_, "HttpPut", CallHttpPut); + lua_register(lua_, "HttpDelete", CallHttpDelete); + lua_register(lua_, "SetHttpCredentials", SetHttpCredentials); + lua_register(lua_, "SetHttpTimeout", SetHttpTimeout); +#endif + + SetGlobalVariable("_LuaContext", this); + } + + + LuaContext::~LuaContext() + { + lua_close(lua_); + } + + + void LuaContext::Execute(const std::string &command) + { + ExecuteInternal(NULL, command); + } + + void LuaContext::Execute(std::string &output, const std::string &command) + { + ExecuteInternal(&output, command); + } + + + void LuaContext::ExecuteInternal(std::string* output, + const std::string& command) + { + log_.clear(); + int error = (luaL_loadbuffer(lua_, command.c_str(), command.size(), "line") || + lua_pcall(lua_, 0, 0, 0)); + + if (error) + { + assert(lua_gettop(lua_) >= 1); + + std::string description(lua_tostring(lua_, -1)); + lua_pop(lua_, 1); /* pop error message from the stack */ + throw OrthancException(ErrorCode_CannotExecuteLua, description); + } + + if (output != NULL) + { + *output = log_; + } + } + + + bool LuaContext::IsExistingFunction(const char* name) + { + lua_settop(lua_, 0); + lua_getglobal(lua_, name); + return lua_type(lua_, -1) == LUA_TFUNCTION; + } + + + void LuaContext::Execute(Json::Value& output, + const std::string& command) + { + std::string s; + ExecuteInternal(&s, command); + + if (!Toolbox::ReadJson(output, s)) + { + throw OrthancException(ErrorCode_BadJson); + } + } + + + void LuaContext::RegisterFunction(const char* name, + lua_CFunction func) + { + lua_register(lua_, name, func); + } + + + void LuaContext::SetGlobalVariable(const char* name, + void* value) + { + lua_pushlightuserdata(lua_, value); + lua_setglobal(lua_, name); + } + + + const void* LuaContext::GetGlobalVariable(lua_State* state, + const char* name) + { + lua_getglobal(state, name); + assert(lua_type(state, -1) == LUA_TLIGHTUSERDATA); + const void* value = lua_topointer(state, -1); + lua_pop(state, 1); + return value; + } + + + void LuaContext::GetDictionaryArgument(std::map& target, + lua_State* state, + int top, + bool keyToLowerCase) + { + target.clear(); + + if (lua_gettop(state) >= top) + { + Json::Value headers; + GetJson(headers, state, top, true); + + Json::Value::Members members = headers.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + std::string key = members[i]; + + if (keyToLowerCase) + { + Toolbox::ToLowerCase(key); + } + + target[key] = headers[members[i]].asString(); + } + } + } +} diff --git a/OrthancFramework/Sources/Lua/LuaContext.h b/OrthancFramework/Sources/Lua/LuaContext.h new file mode 100644 index 0000000..f05f864 --- /dev/null +++ b/OrthancFramework/Sources/Lua/LuaContext.h @@ -0,0 +1,140 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +// To have ORTHANC_ENABLE_LUA defined if using the shared library +#include "../OrthancFramework.h" + +#if !defined(ORTHANC_ENABLE_LUA) +# error The macro ORTHANC_ENABLE_LUA must be defined +#endif + +#if !defined(ORTHANC_ENABLE_CURL) +# error Macro ORTHANC_ENABLE_CURL must be defined +#endif + +#if ORTHANC_ENABLE_LUA == 0 +# error The Lua support is disabled, cannot include this file +#endif + +#if ORTHANC_ENABLE_CURL == 1 +# include "../HttpClient.h" +#endif + +extern "C" +{ +#include +} + +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC LuaContext : public boost::noncopyable + { + private: + friend class LuaFunctionCall; + + lua_State *lua_; + std::string log_; + +#if ORTHANC_ENABLE_CURL == 1 + HttpClient httpClient_; +#endif + + static int PrintToLog(lua_State *state); + static int ParseJson(lua_State *state); + static int DumpJson(lua_State *state); + +#if ORTHANC_ENABLE_CURL == 1 + static int SetHttpCredentials(lua_State *state); + static int SetHttpTimeout(lua_State *state); + static int CallHttpPostOrPut(lua_State *state, + HttpMethod method); + static int CallHttpGet(lua_State *state); + static int CallHttpPost(lua_State *state); + static int CallHttpPut(lua_State *state); + static int CallHttpDelete(lua_State *state); +#endif + + bool AnswerHttpQuery(lua_State* state); + + void ExecuteInternal(std::string* output, + const std::string& command); + + static void GetJson(Json::Value& result, + lua_State* state, + int top, + bool keepStrings); + + void SetHttpHeaders(int top); + + public: + LuaContext(); + + ~LuaContext(); + + void Execute(const std::string& command); + + void Execute(std::string& output, + const std::string& command); + + void Execute(Json::Value& output, + const std::string& command); + + bool IsExistingFunction(const char* name); + + void RegisterFunction(const char* name, + lua_CFunction func); + + void SetGlobalVariable(const char* name, + void* value); + + static LuaContext& GetLuaContext(lua_State *state); + + static const void* GetGlobalVariable(lua_State* state, + const char* name); + + void PushJson(const Json::Value& value); + + static void GetDictionaryArgument(std::map& target, + lua_State* state, + int top, + bool keyToLowerCase); + +#if ORTHANC_ENABLE_CURL == 1 + void SetHttpsVerifyPeers(bool verify) + { + httpClient_.SetHttpsVerifyPeers(verify); + } + + bool IsHttpsVerifyPeers() const + { + return httpClient_.IsHttpsVerifyPeers(); + } +#endif + }; +} diff --git a/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp new file mode 100644 index 0000000..88902b0 --- /dev/null +++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp @@ -0,0 +1,244 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "LuaFunctionCall.h" + +#include "../OrthancException.h" +#include "../Logging.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../DicomParsing/FromDcmtkBridge.h" +#endif + +#include +#include +#include + +namespace Orthanc +{ + void LuaFunctionCall::CheckAlreadyExecuted() + { + if (isExecuted_) + { + throw OrthancException(ErrorCode_LuaAlreadyExecuted); + } + } + + LuaFunctionCall::LuaFunctionCall(LuaContext& context, + const char* functionName) : + context_(context), + isExecuted_(false) + { + // Clear the stack to fulfill the invariant + lua_settop(context_.lua_, 0); + lua_getglobal(context_.lua_, functionName); + } + + void LuaFunctionCall::PushString(const std::string& value) + { + CheckAlreadyExecuted(); + lua_pushlstring(context_.lua_, value.c_str(), value.size()); + } + + void LuaFunctionCall::PushBoolean(bool value) + { + CheckAlreadyExecuted(); + lua_pushboolean(context_.lua_, value); + } + + void LuaFunctionCall::PushInteger(int value) + { + CheckAlreadyExecuted(); + lua_pushinteger(context_.lua_, value); + } + + void LuaFunctionCall::PushDouble(double value) + { + CheckAlreadyExecuted(); + lua_pushnumber(context_.lua_, value); + } + + void LuaFunctionCall::PushJson(const Json::Value& value) + { + CheckAlreadyExecuted(); + context_.PushJson(value); + } + + void LuaFunctionCall::ExecuteInternal(int numOutputs) + { + CheckAlreadyExecuted(); + + assert(lua_gettop(context_.lua_) >= 1); + int nargs = lua_gettop(context_.lua_) - 1; + int error = lua_pcall(context_.lua_, nargs, numOutputs, 0); + + if (error) + { + assert(lua_gettop(context_.lua_) >= 1); + + std::string description(lua_tostring(context_.lua_, -1)); + lua_pop(context_.lua_, 1); /* pop error message from the stack */ + + throw OrthancException(ErrorCode_CannotExecuteLua, description); + } + + if (lua_gettop(context_.lua_) < numOutputs) + { + throw OrthancException(ErrorCode_LuaBadOutput); + } + + isExecuted_ = true; + } + + bool LuaFunctionCall::ExecutePredicate() + { + ExecuteInternal(1); + + if (!lua_isboolean(context_.lua_, 1)) + { + throw OrthancException(ErrorCode_NotLuaPredicate); + } + + return lua_toboolean(context_.lua_, 1) != 0; + } + + + void LuaFunctionCall::ExecuteToJson(Json::Value& result, + bool keepStrings) + { + ExecuteInternal(1); + context_.GetJson(result, context_.lua_, lua_gettop(context_.lua_), keepStrings); + } + + + void LuaFunctionCall::ExecuteToString(std::string& result) + { + ExecuteInternal(1); + + int top = lua_gettop(context_.lua_); + if (lua_isstring(context_.lua_, top)) + { + result = lua_tostring(context_.lua_, top); + } + else + { + throw OrthancException(ErrorCode_LuaReturnsNoString); + } + } + + void LuaFunctionCall::ExecuteToInt(int& result) + { + ExecuteInternal(1); + + int top = lua_gettop(context_.lua_); + if (lua_isnumber(context_.lua_, top)) + { + result = static_cast(lua_tointeger(context_.lua_, top)); + } + else + { + throw OrthancException(ErrorCode_LuaReturnsNoString); + } + } + + void LuaFunctionCall::PushStringMap(const std::map& value) + { + Json::Value json = Json::objectValue; + + for (std::map::const_iterator + it = value.begin(); it != value.end(); ++it) + { + json[it->first] = it->second; + } + + PushJson(json); + } + + + void LuaFunctionCall::PushDicom(const DicomMap& dicom) + { + DicomArray a(dicom); + PushDicom(a); + } + + + void LuaFunctionCall::PushDicom(const DicomArray& dicom) + { + Json::Value value = Json::objectValue; + + for (size_t i = 0; i < dicom.GetSize(); i++) + { + const DicomValue& v = dicom.GetElement(i).GetValue(); + std::string s = (v.IsNull() || v.IsBinary()) ? "" : v.GetContent(); + value[dicom.GetElement(i).GetTag().Format()] = s; + } + + PushJson(value); + } + + void LuaFunctionCall::Execute() + { + ExecuteInternal(0); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void LuaFunctionCall::ExecuteToDicom(DicomMap& target) + { + Json::Value output; + ExecuteToJson(output, true /* keep strings */); + + target.Clear(); + + if (output.type() == Json::arrayValue && + output.size() == 0) + { + // This case happens for empty tables + return; + } + + if (output.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_LuaBadOutput, + "Lua: The script must return a table"); + } + + Json::Value::Members members = output.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + if (output[members[i]].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_LuaBadOutput, + "Lua: The script must return a table " + "mapping names of DICOM tags to strings"); + } + + DicomTag tag(FromDcmtkBridge::ParseTag(members[i])); + target.SetValue(tag, output[members[i]].asString(), false); + } + } +#endif +} diff --git a/OrthancFramework/Sources/Lua/LuaFunctionCall.h b/OrthancFramework/Sources/Lua/LuaFunctionCall.h new file mode 100644 index 0000000..bdf1783 --- /dev/null +++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.h @@ -0,0 +1,89 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "LuaContext.h" + +#include "../DicomFormat/DicomArray.h" +#include "../DicomFormat/DicomMap.h" + +namespace Orthanc +{ + class ORTHANC_PUBLIC LuaFunctionCall : public boost::noncopyable + { + private: + LuaContext& context_; + bool isExecuted_; + + void CheckAlreadyExecuted(); + + protected: + void ExecuteInternal(int numOutputs); + + lua_State* GetState() + { + return context_.lua_; + } + + public: + LuaFunctionCall(LuaContext& context, + const char* functionName); + + void PushString(const std::string& value); + + void PushBoolean(bool value); + + void PushInteger(int value); + + void PushDouble(double value); + + void PushJson(const Json::Value& value); + + void PushStringMap(const std::map& value); + + void PushDicom(const DicomMap& dicom); + + void PushDicom(const DicomArray& dicom); + + void Execute(); + + bool ExecutePredicate(); + + void ExecuteToJson(Json::Value& result, + bool keepStrings); + + void ExecuteToString(std::string& result); + + void ExecuteToInt(int& result); + +#if ORTHANC_ENABLE_DCMTK == 1 + void ExecuteToDicom(DicomMap& target); +#endif + }; +} diff --git a/OrthancFramework/Sources/MallocMemoryBuffer.cpp b/OrthancFramework/Sources/MallocMemoryBuffer.cpp new file mode 100644 index 0000000..4db6542 --- /dev/null +++ b/OrthancFramework/Sources/MallocMemoryBuffer.cpp @@ -0,0 +1,101 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "MallocMemoryBuffer.h" + +#include "OrthancException.h" + +#include + + +namespace Orthanc +{ + MallocMemoryBuffer::MallocMemoryBuffer() : + buffer_(NULL), + size_(0), + free_(NULL) + { + } + + + void MallocMemoryBuffer::Clear() + { + if (size_ != 0) + { + if (free_ == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + free_(buffer_); + buffer_ = NULL; + size_ = 0; + free_ = NULL; + } + } + + + void MallocMemoryBuffer::Assign(void* buffer, + uint64_t size, + FreeFunction freeFunction) + { + Clear(); + + if (size != 0 && + buffer == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + if (static_cast(static_cast(size)) != size) + { + freeFunction(buffer); + throw OrthancException(ErrorCode_InternalError, "Buffer larger than 4GB, which is too large for Orthanc running in 32bits"); + } + + buffer_ = buffer; + size_ = static_cast(size); + free_ = freeFunction; + + if (size_ != 0 && + free_ == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "No valid free() function provided"); + } + } + + + void MallocMemoryBuffer::MoveToString(std::string& target) + { + target.resize(size_); + + if (size_ != 0) + { + memcpy(&target[0], buffer_, size_); + } + + Clear(); + } +} diff --git a/OrthancFramework/Sources/MallocMemoryBuffer.h b/OrthancFramework/Sources/MallocMemoryBuffer.h new file mode 100644 index 0000000..fe769b1 --- /dev/null +++ b/OrthancFramework/Sources/MallocMemoryBuffer.h @@ -0,0 +1,71 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IMemoryBuffer.h" +#include "Compatibility.h" + +#include // For uint64_t + + +namespace Orthanc +{ + class MallocMemoryBuffer : public IMemoryBuffer + { + public: + typedef void (*FreeFunction) (void* buffer); + + private: + void* buffer_; + size_t size_; + FreeFunction free_; + + public: + MallocMemoryBuffer(); + + virtual ~MallocMemoryBuffer() + { + Clear(); + } + + void Clear(); + + void Assign(void* buffer, + uint64_t size, + FreeFunction freeFunction); + + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; + + virtual const void* GetData() const ORTHANC_OVERRIDE + { + return buffer_; + } + + virtual size_t GetSize() const ORTHANC_OVERRIDE + { + return size_; + } + }; +} diff --git a/OrthancFramework/Sources/MetricsRegistry.cpp b/OrthancFramework/Sources/MetricsRegistry.cpp new file mode 100644 index 0000000..12b8632 --- /dev/null +++ b/OrthancFramework/Sources/MetricsRegistry.cpp @@ -0,0 +1,581 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "MetricsRegistry.h" + +#include "ChunkedBuffer.h" +#include "Compatibility.h" +#include "OrthancException.h" + +#include + +namespace Orthanc +{ + static const boost::posix_time::ptime GetNow() + { + return boost::posix_time::microsec_clock::universal_time(); + } + + namespace + { + template + class TimestampedValue : public boost::noncopyable + { + private: + boost::posix_time::ptime time_; + bool hasValue_; + T value_; + + void SetValue(const T& value, + const boost::posix_time::ptime& now) + { + hasValue_ = true; + value_ = value; + time_ = now; + } + + bool IsLargerOverPeriod(const T& value, + int duration, + const boost::posix_time::ptime& now) const + { + if (hasValue_) + { + return (value > value_ || + (now - time_).total_seconds() > duration /* old value has expired */); + } + else + { + return true; // No value yet + } + } + + bool IsSmallerOverPeriod(const T& value, + int duration, + const boost::posix_time::ptime& now) const + { + if (hasValue_) + { + return (value < value_ || + (now - time_).total_seconds() > duration /* old value has expired */); + } + else + { + return true; // No value yet + } + } + + public: + explicit TimestampedValue() : + hasValue_(false), + value_(0) + { + } + + void Update(const T& value, + const MetricsUpdatePolicy& policy) + { + const boost::posix_time::ptime now = GetNow(); + + switch (policy) + { + case MetricsUpdatePolicy_Directly: + SetValue(value, now); + break; + + case MetricsUpdatePolicy_MaxOver10Seconds: + if (IsLargerOverPeriod(value, 10, now)) + { + SetValue(value, now); + } + break; + + case MetricsUpdatePolicy_MaxOver1Minute: + if (IsLargerOverPeriod(value, 60, now)) + { + SetValue(value, now); + } + break; + + case MetricsUpdatePolicy_MinOver10Seconds: + if (IsSmallerOverPeriod(value, 10, now)) + { + SetValue(value, now); + } + break; + + case MetricsUpdatePolicy_MinOver1Minute: + if (IsSmallerOverPeriod(value, 60, now)) + { + SetValue(value, now); + } + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void Increment(const T& delta) + { + time_ = GetNow(); + + if (hasValue_) + { + value_ += delta; + } + else + { + value_ = delta; + hasValue_ = true; + } + } + + bool HasValue() const + { + return hasValue_; + } + + const boost::posix_time::ptime& GetTime() const + { + if (hasValue_) + { + return time_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + const T& GetValue() const + { + if (hasValue_) + { + return value_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + }; + } + + + class MetricsRegistry::Item : public boost::noncopyable + { + private: + MetricsUpdatePolicy policy_; + + public: + explicit Item(MetricsUpdatePolicy policy) : + policy_(policy) + { + } + + virtual ~Item() + { + } + + MetricsUpdatePolicy GetPolicy() const + { + return policy_; + } + + virtual void UpdateFloat(float value) = 0; + + virtual void UpdateInteger(int64_t value) = 0; + + virtual void IncrementInteger(int64_t delta) = 0; + + virtual MetricsDataType GetDataType() const = 0; + + virtual bool HasValue() const = 0; + + virtual const boost::posix_time::ptime& GetTime() const = 0; + + virtual std::string FormatValue() const = 0; + }; + + + class MetricsRegistry::FloatItem : public Item + { + private: + TimestampedValue value_; + + public: + explicit FloatItem(MetricsUpdatePolicy policy) : + Item(policy) + { + } + + virtual void UpdateFloat(float value) ORTHANC_OVERRIDE + { + value_.Update(value, GetPolicy()); + } + + virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE + { + value_.Update(static_cast(value), GetPolicy()); + } + + virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE + { + value_.Increment(static_cast(delta)); + } + + virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE + { + return MetricsDataType_Float; + } + + virtual bool HasValue() const ORTHANC_OVERRIDE + { + return value_.HasValue(); + } + + virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE + { + return value_.GetTime(); + } + + virtual std::string FormatValue() const ORTHANC_OVERRIDE + { + return boost::lexical_cast(value_.GetValue()); + } + }; + + + class MetricsRegistry::IntegerItem : public Item + { + private: + TimestampedValue value_; + + public: + explicit IntegerItem(MetricsUpdatePolicy policy) : + Item(policy) + { + } + + virtual void UpdateFloat(float value) ORTHANC_OVERRIDE + { + value_.Update(boost::math::llround(value), GetPolicy()); + } + + virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE + { + value_.Update(value, GetPolicy()); + } + + virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE + { + value_.Increment(delta); + } + + virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE + { + return MetricsDataType_Integer; + } + + virtual bool HasValue() const ORTHANC_OVERRIDE + { + return value_.HasValue(); + } + + virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE + { + return value_.GetTime(); + } + + virtual std::string FormatValue() const ORTHANC_OVERRIDE + { + return boost::lexical_cast(value_.GetValue()); + } + }; + + + MetricsRegistry::~MetricsRegistry() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + bool MetricsRegistry::IsEnabled() const + { + return enabled_; + } + + + void MetricsRegistry::SetEnabled(bool enabled) + { + boost::mutex::scoped_lock lock(mutex_); + enabled_ = enabled; + } + + + void MetricsRegistry::Register(const std::string& name, + MetricsUpdatePolicy policy, + MetricsDataType type) + { + boost::mutex::scoped_lock lock(mutex_); + + if (content_.find(name) != content_.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same metrics: " + name); + } + else + { + GetItemInternal(name, policy, type); + } + } + + + MetricsRegistry::Item& MetricsRegistry::GetItemInternal(const std::string& name, + MetricsUpdatePolicy policy, + MetricsDataType type) + { + Content::iterator found = content_.find(name); + + if (found == content_.end()) + { + Item* item = NULL; + + switch (type) + { + case MetricsDataType_Float: + item = new FloatItem(policy); + break; + + case MetricsDataType_Integer: + item = new IntegerItem(policy); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + content_[name] = item; + return *item; + } + else + { + assert(found->second != NULL); + return *found->second; + } + } + + MetricsRegistry::MetricsRegistry() : + enabled_(true) + { + } + + + void MetricsRegistry::SetFloatValue(const std::string& name, + float value, + MetricsUpdatePolicy policy) + { + // Inlining to avoid loosing time if metrics are disabled + if (enabled_) + { + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, policy, MetricsDataType_Float).UpdateFloat(value); + } + } + + + void MetricsRegistry::SetIntegerValue(const std::string &name, + int64_t value, + MetricsUpdatePolicy policy) + { + // Inlining to avoid loosing time if metrics are disabled + if (enabled_) + { + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, policy, MetricsDataType_Integer).UpdateInteger(value); + } + } + + + void MetricsRegistry::IncrementIntegerValue(const std::string &name, + int64_t delta) + { + // Inlining to avoid loosing time if metrics are disabled + if (enabled_) + { + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, MetricsUpdatePolicy_Directly, MetricsDataType_Integer).IncrementInteger(delta); + } + } + + + MetricsUpdatePolicy MetricsRegistry::GetUpdatePolicy(const std::string& metrics) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::const_iterator found = content_.find(metrics); + + if (found == content_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return found->second->GetPolicy(); + } + } + + + MetricsDataType MetricsRegistry::GetDataType(const std::string& metrics) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::const_iterator found = content_.find(metrics); + + if (found == content_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return found->second->GetDataType(); + } + } + + + void MetricsRegistry::ExportPrometheusText(std::string& s) + { + // https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch + static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1)); + + boost::mutex::scoped_lock lock(mutex_); + + s.clear(); + + if (!enabled_) + { + return; + } + + ChunkedBuffer buffer; + + for (Content::const_iterator it = content_.begin(); + it != content_.end(); ++it) + { + assert(it->second != NULL); + + if (it->second->HasValue()) + { + boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH; + + std::string line = (it->first + " " + + it->second->FormatValue() + " " + + boost::lexical_cast(diff.total_milliseconds()) + "\n"); + + buffer.AddChunk(line); + } + } + + buffer.Flatten(s); + } + + + MetricsRegistry::SharedMetrics::SharedMetrics(MetricsRegistry ®istry, + const std::string &name, + MetricsUpdatePolicy policy) : + registry_(registry), + name_(name), + value_(0) + { + } + + void MetricsRegistry::SharedMetrics::Add(int64_t delta) + { + boost::mutex::scoped_lock lock(mutex_); + value_ += delta; + registry_.SetIntegerValue(name_, value_); + } + + + MetricsRegistry::ActiveCounter::ActiveCounter(MetricsRegistry::SharedMetrics &metrics) : + metrics_(metrics) + { + metrics_.Add(1); + } + + MetricsRegistry::ActiveCounter::~ActiveCounter() + { + metrics_.Add(-1); + } + + + void MetricsRegistry::Timer::Start() + { + if (registry_.IsEnabled()) + { + active_ = true; + start_ = GetNow(); + } + else + { + active_ = false; + } + } + + + MetricsRegistry::Timer::Timer(MetricsRegistry ®istry, + const std::string &name) : + registry_(registry), + name_(name), + policy_(MetricsUpdatePolicy_MaxOver10Seconds) + { + Start(); + } + + + MetricsRegistry::Timer::Timer(MetricsRegistry ®istry, + const std::string &name, + MetricsUpdatePolicy policy) : + registry_(registry), + name_(name), + policy_(policy) + { + Start(); + } + + + MetricsRegistry::Timer::~Timer() + { + if (active_) + { + boost::posix_time::time_duration diff = GetNow() - start_; + registry_.SetIntegerValue(name_, static_cast(diff.total_milliseconds()), policy_); + } + } +} diff --git a/OrthancFramework/Sources/MetricsRegistry.h b/OrthancFramework/Sources/MetricsRegistry.h new file mode 100644 index 0000000..104a994 --- /dev/null +++ b/OrthancFramework/Sources/MetricsRegistry.h @@ -0,0 +1,171 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "OrthancFramework.h" + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class MetricsRegistry cannot be used in sandboxed environments +#endif + +#include +#include +#include + +namespace Orthanc +{ + enum MetricsUpdatePolicy + { + MetricsUpdatePolicy_Directly, + MetricsUpdatePolicy_MaxOver10Seconds, + MetricsUpdatePolicy_MaxOver1Minute, + MetricsUpdatePolicy_MinOver10Seconds, + MetricsUpdatePolicy_MinOver1Minute + }; + + enum MetricsDataType + { + MetricsDataType_Float, + MetricsDataType_Integer + }; + + class ORTHANC_PUBLIC MetricsRegistry : public boost::noncopyable + { + private: + class Item; + class FloatItem; + class IntegerItem; + + typedef std::map Content; + + bool enabled_; + boost::mutex mutex_; + Content content_; + + // The mutex must be locked + Item& GetItemInternal(const std::string& name, + MetricsUpdatePolicy policy, + MetricsDataType type); + + public: + MetricsRegistry(); + + ~MetricsRegistry(); + + bool IsEnabled() const; + + void SetEnabled(bool enabled); + + void Register(const std::string& name, + MetricsUpdatePolicy policy, + MetricsDataType type); + + void SetFloatValue(const std::string& name, + float value, + MetricsUpdatePolicy policy /* only used if this is a new metrics */); + + void SetFloatValue(const std::string& name, + float value) + { + SetFloatValue(name, value, MetricsUpdatePolicy_Directly); + } + + void SetIntegerValue(const std::string& name, + int64_t value, + MetricsUpdatePolicy policy /* only used if this is a new metrics */); + + void SetIntegerValue(const std::string& name, + int64_t value) + { + SetIntegerValue(name, value, MetricsUpdatePolicy_Directly); + } + + void IncrementIntegerValue(const std::string& name, + int64_t delta); + + MetricsUpdatePolicy GetUpdatePolicy(const std::string& metrics); + + MetricsDataType GetDataType(const std::string& metrics); + + // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format + void ExportPrometheusText(std::string& s); + + + class ORTHANC_PUBLIC SharedMetrics : public boost::noncopyable + { + private: + boost::mutex mutex_; + MetricsRegistry& registry_; + std::string name_; + int64_t value_; + + public: + SharedMetrics(MetricsRegistry& registry, + const std::string& name, + MetricsUpdatePolicy policy); + + void Add(int64_t delta); + }; + + + class ORTHANC_PUBLIC ActiveCounter : public boost::noncopyable + { + private: + SharedMetrics& metrics_; + + public: + explicit ActiveCounter(SharedMetrics& metrics); + + ~ActiveCounter(); + }; + + + class ORTHANC_PUBLIC Timer : public boost::noncopyable + { + private: + MetricsRegistry& registry_; + std::string name_; + MetricsUpdatePolicy policy_; + bool active_; + boost::posix_time::ptime start_; + + void Start(); + + public: + Timer(MetricsRegistry& registry, + const std::string& name); + + Timer(MetricsRegistry& registry, + const std::string& name, + MetricsUpdatePolicy policy); + + ~Timer(); + }; + }; +} diff --git a/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h b/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h new file mode 100644 index 0000000..37bb1e4 --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h @@ -0,0 +1,47 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../IDynamicObject.h" + +namespace Orthanc +{ + class IRunnableBySteps : public IDynamicObject + { + public: + virtual ~IRunnableBySteps() + { + } + + // Must return "true" if the runnable wishes to continue. Must + // return "false" if the runnable has not finished its job. + virtual bool Step() = 0; + + static void RunUntilDone(IRunnableBySteps& runnable) + { + while (runnable.Step()); + } + }; +} diff --git a/OrthancFramework/Sources/MultiThreading/Mutex.h b/OrthancFramework/Sources/MultiThreading/Mutex.h new file mode 100644 index 0000000..bc07b4d --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/Mutex.h @@ -0,0 +1,70 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(__EMSCRIPTEN__) +# include +#endif + +namespace Orthanc +{ + // Wrapper class for compatibility with Emscripten + +#if defined(__EMSCRIPTEN__) + + class ORTHANC_PUBLIC Mutex : public boost::noncopyable + { + public: + class ORTHANC_PUBLIC ScopedLock : public boost::noncopyable + { + public: + explicit ScopedLock(Mutex& mutex) + { + } + }; + }; + +#else + + class ORTHANC_PUBLIC Mutex : public boost::noncopyable + { + private: + boost::mutex mutex_; + + public: + class ORTHANC_PUBLIC ScopedLock : public boost::noncopyable + { + private: + boost::mutex::scoped_lock lock_; + + public: + explicit ScopedLock(Mutex& mutex) : + lock_(mutex.mutex_) + { + } + }; + }; +#endif +} diff --git a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp new file mode 100644 index 0000000..4dcd419 --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp @@ -0,0 +1,168 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RunnableWorkersPool.h" + +#include "SharedMessageQueue.h" +#include "../Compatibility.h" +#include "../OrthancException.h" +#include "../Logging.h" + +namespace Orthanc +{ + struct RunnableWorkersPool::PImpl + { + class Worker + { + private: + const bool& continue_; + SharedMessageQueue& queue_; + boost::thread thread_; + std::string name_; + + static void WorkerThread(Worker* that) + { + Logging::SetCurrentThreadName(that->name_); + + while (that->continue_) + { + try + { + std::unique_ptr obj(that->queue_.Dequeue(100)); + if (obj.get() != NULL) + { + IRunnableBySteps& runnable = *dynamic_cast(obj.get()); + + bool wishToContinue = runnable.Step(); + + if (wishToContinue) + { + // The runnable wishes to continue, reinsert it at the beginning of the queue + that->queue_.Enqueue(obj.release()); + } + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while handling some runnable object: " << e.What(); + } + catch (std::bad_alloc&) + { + LOG(ERROR) << "Not enough memory to handle some runnable object"; + } + catch (std::exception& e) + { + LOG(ERROR) << "std::exception while handling some runnable object: " << e.what(); + } + catch (...) + { + LOG(ERROR) << "Native exception while handling some runnable object"; + } + } + } + + public: + Worker(const bool& globalContinue, + SharedMessageQueue& queue, + const std::string& name) : + continue_(globalContinue), + queue_(queue), + name_(name) + { + thread_ = boost::thread(WorkerThread, this); + } + + void Join() + { + if (thread_.joinable()) + { + thread_.join(); + } + } + }; + + + bool continue_; + std::vector workers_; + SharedMessageQueue queue_; + }; + + + + RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers, const std::string& name) : pimpl_(new PImpl) + { + pimpl_->continue_ = true; + + if (countWorkers == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + pimpl_->workers_.resize(countWorkers); + + for (size_t i = 0; i < countWorkers; i++) + { + std::string workerName = name + boost::lexical_cast(i); + pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_, workerName); + } + } + + + void RunnableWorkersPool::Stop() + { + if (pimpl_->continue_) + { + pimpl_->continue_ = false; + + for (size_t i = 0; i < pimpl_->workers_.size(); i++) + { + PImpl::Worker* worker = pimpl_->workers_[i]; + + if (worker != NULL) + { + worker->Join(); + delete worker; + } + } + } + } + + + RunnableWorkersPool::~RunnableWorkersPool() + { + Stop(); + } + + + void RunnableWorkersPool::Add(IRunnableBySteps* runnable) + { + if (!pimpl_->continue_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + pimpl_->queue_.Enqueue(runnable); + } +} diff --git a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h new file mode 100644 index 0000000..b64c22f --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h @@ -0,0 +1,48 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IRunnableBySteps.h" + +#include + +namespace Orthanc +{ + class RunnableWorkersPool : public boost::noncopyable + { + private: + struct PImpl; + boost::shared_ptr pimpl_; + + void Stop(); + + public: + explicit RunnableWorkersPool(size_t countWorkers, const std::string& name); + + ~RunnableWorkersPool(); + + void Add(IRunnableBySteps* runnable); // Takes the ownership + }; +} diff --git a/OrthancFramework/Sources/MultiThreading/Semaphore.cpp b/OrthancFramework/Sources/MultiThreading/Semaphore.cpp new file mode 100644 index 0000000..2541ca9 --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/Semaphore.cpp @@ -0,0 +1,75 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "Semaphore.h" + +#include "../OrthancException.h" + + +namespace Orthanc +{ + Semaphore::Semaphore(unsigned int availableResources) : + availableResources_(availableResources) + { + } + + unsigned int Semaphore::GetAvailableResourcesCount() const + { + return availableResources_; + } + + void Semaphore::Release(unsigned int resourceCount) + { + boost::mutex::scoped_lock lock(mutex_); + + availableResources_ += resourceCount; + condition_.notify_one(); + } + + void Semaphore::Acquire(unsigned int resourceCount) + { + boost::mutex::scoped_lock lock(mutex_); + + while (availableResources_ < resourceCount) + { + condition_.wait(lock); + } + + availableResources_ -= resourceCount; + } + + bool Semaphore::TryAcquire(unsigned int resourceCount) + { + boost::mutex::scoped_lock lock(mutex_); + + if (availableResources_ < resourceCount) + { + return false; + } + + availableResources_ -= resourceCount; + return true; + } +} diff --git a/OrthancFramework/Sources/MultiThreading/Semaphore.h b/OrthancFramework/Sources/MultiThreading/Semaphore.h new file mode 100644 index 0000000..96ae801 --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/Semaphore.h @@ -0,0 +1,102 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../OrthancFramework.h" + +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC Semaphore : public boost::noncopyable + { + private: + unsigned int availableResources_; + boost::mutex mutex_; + boost::condition_variable condition_; + + public: + explicit Semaphore(unsigned int availableResources); + + unsigned int GetAvailableResourcesCount() const; + + void Release(unsigned int resourceCount = 1); + + void Acquire(unsigned int resourceCount = 1); + + bool TryAcquire(unsigned int resourceCount = 1); + + class Locker : public boost::noncopyable + { + private: + Semaphore& that_; + unsigned int resourceCount_; + + public: + explicit Locker(Semaphore& that, unsigned int resourceCount = 1) : + that_(that), + resourceCount_(resourceCount) + { + that_.Acquire(resourceCount_); + } + + ~Locker() + { + that_.Release(resourceCount_); + } + }; + + class TryLocker : public boost::noncopyable + { + private: + Semaphore& that_; + unsigned int resourceCount_; + bool isAcquired_; + + public: + explicit TryLocker(Semaphore& that, unsigned int resourceCount = 1) : + that_(that), + resourceCount_(resourceCount) + { + isAcquired_ = that_.TryAcquire(resourceCount_); + } + + ~TryLocker() + { + if (isAcquired_) + { + that_.Release(resourceCount_); + } + } + + bool IsAcquired() const + { + return isAcquired_; + } + }; + + }; +} diff --git a/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp new file mode 100644 index 0000000..1f65f4e --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp @@ -0,0 +1,218 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "SharedMessageQueue.h" + + +#include "../Compatibility.h" + + +/** + * FIFO (queue): + * + * back front + * +--+--+--+--+--+--+--+--+--+--+--+ + * Enqueue -> | | | | | | | | | | | | + * | | | | | | | | | | | | -> Dequeue + * +--+--+--+--+--+--+--+--+--+--+--+ + * ^ + * | + * Make room here + * + * + * LIFO (stack): + * + * back front + * +--+--+--+--+--+--+--+--+--+--+--+ + * | | | | | | | | | | | | <- Enqueue + * | | | | | | | | | | | | -> Dequeue + * +--+--+--+--+--+--+--+--+--+--+--+ + * ^ + * | + * Make room here + **/ + + +namespace Orthanc +{ + SharedMessageQueue::SharedMessageQueue(unsigned int maxSize) : + isFifo_(true), + maxSize_(maxSize) + { + } + + + SharedMessageQueue::~SharedMessageQueue() + { + for (Queue::iterator it = queue_.begin(); it != queue_.end(); ++it) + { + delete *it; + } + } + + + void SharedMessageQueue::Enqueue(IDynamicObject* message) + { + boost::mutex::scoped_lock lock(mutex_); + + if (maxSize_ != 0 && queue_.size() > maxSize_) + { + if (isFifo_) + { + // Too many elements in the queue: Make room + delete queue_.front(); + queue_.pop_front(); + } + else + { + // Too many elements in the stack: Make room + delete queue_.back(); + queue_.pop_back(); + } + } + + if (isFifo_) + { + // Queue policy (FIFO) + queue_.push_back(message); + } + else + { + // Stack policy (LIFO) + queue_.push_front(message); + } + + elementAvailable_.notify_one(); + } + + + IDynamicObject* SharedMessageQueue::Dequeue(int32_t millisecondsTimeout) + { + boost::mutex::scoped_lock lock(mutex_); + + // Wait for a message to arrive in the FIFO queue + while (queue_.empty()) + { + if (millisecondsTimeout == 0) + { + elementAvailable_.wait(lock); + } + else + { + bool success = elementAvailable_.timed_wait + (lock, boost::posix_time::milliseconds(millisecondsTimeout)); + if (!success) + { + return NULL; + } + } + } + + std::unique_ptr message(queue_.front()); + queue_.pop_front(); + + if (queue_.empty()) + { + emptied_.notify_all(); + } + + return message.release(); + } + + + + bool SharedMessageQueue::WaitEmpty(int32_t millisecondsTimeout) + { + boost::mutex::scoped_lock lock(mutex_); + + // Wait for the queue to become empty + while (!queue_.empty()) + { + if (millisecondsTimeout == 0) + { + emptied_.wait(lock); + } + else + { + if (!emptied_.timed_wait + (lock, boost::posix_time::milliseconds(millisecondsTimeout))) + { + return false; + } + } + } + + return true; + } + + bool SharedMessageQueue::IsFifoPolicy() const + { + return isFifo_; + } + + bool SharedMessageQueue::IsLifoPolicy() const + { + return !isFifo_; + } + + + void SharedMessageQueue::SetFifoPolicy() + { + boost::mutex::scoped_lock lock(mutex_); + isFifo_ = true; + } + + void SharedMessageQueue::SetLifoPolicy() + { + boost::mutex::scoped_lock lock(mutex_); + isFifo_ = false; + } + + void SharedMessageQueue::Clear() + { + boost::mutex::scoped_lock lock(mutex_); + + if (queue_.empty()) + { + return; + } + else + { + while (!queue_.empty()) + { + std::unique_ptr message(queue_.front()); + queue_.pop_front(); + } + + emptied_.notify_all(); + } + } + + size_t SharedMessageQueue::GetSize() + { + boost::mutex::scoped_lock lock(mutex_); + return queue_.size(); + } +} diff --git a/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h new file mode 100644 index 0000000..8a5f954 --- /dev/null +++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h @@ -0,0 +1,72 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../IDynamicObject.h" + +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC SharedMessageQueue : public boost::noncopyable + { + private: + typedef std::list Queue; + + bool isFifo_; + unsigned int maxSize_; + Queue queue_; + boost::mutex mutex_; + boost::condition_variable elementAvailable_; + boost::condition_variable emptied_; + + public: + explicit SharedMessageQueue(unsigned int maxSize = 0); + + ~SharedMessageQueue(); + + // This transfers the ownership of the message + void Enqueue(IDynamicObject* message); + + // The caller is responsible to delete the dequeud message! + IDynamicObject* Dequeue(int32_t millisecondsTimeout); + + bool WaitEmpty(int32_t millisecondsTimeout); + + bool IsFifoPolicy() const; + + bool IsLifoPolicy() const; + + void SetFifoPolicy(); + + void SetLifoPolicy(); + + void Clear(); + + size_t GetSize(); + }; +} diff --git a/OrthancFramework/Sources/OrthancException.cpp b/OrthancFramework/Sources/OrthancException.cpp new file mode 100644 index 0000000..b37ec76 --- /dev/null +++ b/OrthancFramework/Sources/OrthancException.cpp @@ -0,0 +1,129 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "OrthancException.h" + +#include "Logging.h" + + +namespace Orthanc +{ + OrthancException::OrthancException(const OrthancException& other) : + errorCode_(other.errorCode_), + httpStatus_(other.httpStatus_), + logged_(false) + { + if (other.details_.get() != NULL) + { + details_.reset(new std::string(*other.details_)); + } + } + + OrthancException::OrthancException(ErrorCode errorCode) : + errorCode_(errorCode), + httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)), + logged_(false) + { + } + + OrthancException::OrthancException(ErrorCode errorCode, + const std::string& details, + bool log) : + errorCode_(errorCode), + httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)), + logged_(log), + details_(new std::string(details)) + { +#if ORTHANC_ENABLE_LOGGING == 1 + if (log) + { + LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details; + } +#endif + } + + OrthancException::OrthancException(ErrorCode errorCode, + HttpStatus httpStatus) : + errorCode_(errorCode), + httpStatus_(httpStatus), + logged_(false) + { + } + + OrthancException::OrthancException(ErrorCode errorCode, + HttpStatus httpStatus, + const std::string& details, + bool log) : + errorCode_(errorCode), + httpStatus_(httpStatus), + logged_(log), + details_(new std::string(details)) + { +#if ORTHANC_ENABLE_LOGGING == 1 + if (log) + { + LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details; + } +#endif + } + + ErrorCode OrthancException::GetErrorCode() const + { + return errorCode_; + } + + HttpStatus OrthancException::GetHttpStatus() const + { + return httpStatus_; + } + + const char* OrthancException::What() const + { + return EnumerationToString(errorCode_); + } + + bool OrthancException::HasDetails() const + { + return details_.get() != NULL; + } + + const char* OrthancException::GetDetails() const + { + if (details_.get() == NULL) + { + return ""; + } + else + { + return details_->c_str(); + } + } + + bool OrthancException::HasBeenLogged() const + { + return logged_; + } + +} diff --git a/OrthancFramework/Sources/OrthancException.h b/OrthancFramework/Sources/OrthancException.h new file mode 100644 index 0000000..a7f7c01 --- /dev/null +++ b/OrthancFramework/Sources/OrthancException.h @@ -0,0 +1,76 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "Compatibility.h" // For std::unique_ptr<> +#include "Enumerations.h" +#include "OrthancFramework.h" + +namespace Orthanc +{ + class ORTHANC_PUBLIC OrthancException + { + private: + OrthancException(); // Forbidden + + OrthancException& operator= (const OrthancException&); // Forbidden + + ErrorCode errorCode_; + HttpStatus httpStatus_; + bool logged_; // has the exception already been logged ? (to avoid double logs) + + // New in Orthanc 1.5.0 + std::unique_ptr details_; + + public: + OrthancException(const OrthancException& other); + + explicit OrthancException(ErrorCode errorCode); + + OrthancException(ErrorCode errorCode, + const std::string& details, + bool log = true); + + OrthancException(ErrorCode errorCode, + HttpStatus httpStatus); + + OrthancException(ErrorCode errorCode, + HttpStatus httpStatus, + const std::string& details, + bool log = true); + + ErrorCode GetErrorCode() const; + + HttpStatus GetHttpStatus() const; + + const char* What() const; + + bool HasDetails() const; + + const char* GetDetails() const; + + bool HasBeenLogged() const; + }; +} diff --git a/OrthancFramework/Sources/OrthancFramework.cpp b/OrthancFramework/Sources/OrthancFramework.cpp new file mode 100644 index 0000000..4bba0d7 --- /dev/null +++ b/OrthancFramework/Sources/OrthancFramework.cpp @@ -0,0 +1,114 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "OrthancFramework.h" + +#if !defined(ORTHANC_ENABLE_CURL) +# error Macro ORTHANC_ENABLE_CURL must be defined +#endif + +#if !defined(ORTHANC_ENABLE_SSL) +# error Macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error Macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING) +# error Macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined +#endif + +#if ORTHANC_ENABLE_CURL == 1 +# include "HttpClient.h" +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "DicomParsing/FromDcmtkBridge.h" +# if ORTHANC_ENABLE_DCMTK_NETWORKING == 1 +# include +# endif +#endif + +#include "Logging.h" +#include "Toolbox.h" + + +namespace Orthanc +{ + void InitializeFramework(const std::string& locale, + bool loadPrivateDictionary) + { + Logging::Initialize(); + +#if (ORTHANC_ENABLE_LOCALE == 1) && !defined(__EMSCRIPTEN__) // No global locale in wasm/asm.js + if (locale.empty()) + { + Toolbox::InitializeGlobalLocale(NULL); + } + else + { + Toolbox::InitializeGlobalLocale(locale.c_str()); + } +#endif + + Toolbox::InitializeOpenSsl(); + +#if ORTHANC_ENABLE_CURL == 1 + HttpClient::GlobalInitialize(); +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 + FromDcmtkBridge::InitializeDictionary(loadPrivateDictionary); + FromDcmtkBridge::InitializeCodecs(); +#endif + +#if (ORTHANC_ENABLE_DCMTK == 1 && \ + ORTHANC_ENABLE_DCMTK_NETWORKING == 1) + /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */ + dcmDisableGethostbyaddr.set(OFTrue); +#endif + } + + + void FinalizeFramework() + { +#if ORTHANC_ENABLE_DCMTK == 1 + FromDcmtkBridge::FinalizeCodecs(); +#endif + +#if ORTHANC_ENABLE_CURL == 1 + HttpClient::GlobalFinalize(); +#endif + + Toolbox::FinalizeOpenSsl(); + +#if (ORTHANC_ENABLE_LOCALE == 1) && !defined(__EMSCRIPTEN__) + Toolbox::FinalizeGlobalLocale(); +#endif + + Logging::Finalize(); + } +} diff --git a/OrthancFramework/Sources/OrthancFramework.h b/OrthancFramework/Sources/OrthancFramework.h new file mode 100644 index 0000000..84c9a80 --- /dev/null +++ b/OrthancFramework/Sources/OrthancFramework.h @@ -0,0 +1,84 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +/** + * Besides the "pragma once" above that only protects this file, + * define a macro to prevent including different versions of + * "OrthancFramework.h" + **/ +#ifndef __ORTHANC_FRAMEWORK_H +#define __ORTHANC_FRAMEWORK_H + +#if !defined(ORTHANC_BUILDING_FRAMEWORK_LIBRARY) +# error The macro ORTHANC_BUILDING_FRAMEWORK_LIBRARY must be defined +#endif + +/** + * It is implied that if this file is used, we're building the Orthanc + * framework (not using it as a shared library): We don't use the + * common "BUILDING_DLL" + * construction. https://gcc.gnu.org/wiki/Visibility + **/ +#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 +# if defined(_WIN32) || defined (__CYGWIN__) +# define ORTHANC_PUBLIC __declspec(dllexport) +# define ORTHANC_LOCAL +# else +# if __GNUC__ >= 4 +# define ORTHANC_PUBLIC __attribute__((visibility ("default"))) +# define ORTHANC_LOCAL __attribute__((visibility ("hidden"))) +# else +# define ORTHANC_PUBLIC +# define ORTHANC_LOCAL +# pragma warning Unknown dynamic link import/export semantics +# endif +# endif +#else +# define ORTHANC_PUBLIC +# define ORTHANC_LOCAL +#endif + + +#define ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(major, minor, revision) \ + (ORTHANC_VERSION_MAJOR > major || \ + (ORTHANC_VERSION_MAJOR == major && \ + (ORTHANC_VERSION_MINOR > minor || \ + (ORTHANC_VERSION_MINOR == minor && \ + ORTHANC_VERSION_REVISION >= revision)))) + + +#include + +namespace Orthanc +{ + ORTHANC_PUBLIC void InitializeFramework(const std::string& locale, + bool loadPrivateDictionary); + + ORTHANC_PUBLIC void FinalizeFramework(); +} + + +#endif /* __ORTHANC_FRAMEWORK_H */ diff --git a/OrthancFramework/Sources/Pkcs11.cpp b/OrthancFramework/Sources/Pkcs11.cpp new file mode 100644 index 0000000..2211392 --- /dev/null +++ b/OrthancFramework/Sources/Pkcs11.cpp @@ -0,0 +1,310 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "Pkcs11.h" + + +#if defined(OPENSSL_NO_RSA) || defined(OPENSSL_NO_EC) || defined(OPENSSL_NO_ECDSA) || defined(OPENSSL_NO_ECDH) +# error OpenSSL was compiled without support for RSA, EC, ECDSA or ECDH +#endif + + +#include "Logging.h" +#include "OrthancException.h" +#include "SystemToolbox.h" + +extern "C" +{ +# include // This is P11's "engine.h" +# include +} + +#include + +#if OPENSSL_VERSION_NUMBER < 0x30000000L +# if defined(_MSC_VER) +# pragma message("You are linking Orthanc against OpenSSL 1.x, whose license is incompatible with the GPLv3+ used by Orthanc. Please update to OpenSSL 3.x, that uses the Apache 2 license.") +# else +# warning You are linking Orthanc against OpenSSL 1.x, whose license is incompatible with the GPLv3+ used by Orthanc. Please update to OpenSSL 3.x, that uses the Apache 2 license. +# endif +#endif + + +namespace Orthanc +{ + namespace Pkcs11 + { + static const char* PKCS11_ENGINE_ID = "pkcs11"; + static const char* PKCS11_ENGINE_NAME = "PKCS#11 for Orthanc"; + static const ENGINE_CMD_DEFN PKCS11_ENGINE_COMMANDS[] = + { + { + CMD_MODULE_PATH, + "MODULE_PATH", + "Specifies the path to the PKCS#11 module shared library", + ENGINE_CMD_FLAG_STRING + }, + { + CMD_PIN, + "PIN", + "Specifies the pin code", + ENGINE_CMD_FLAG_STRING + }, + { + CMD_VERBOSE, + "VERBOSE", + "Print additional details", + ENGINE_CMD_FLAG_NO_INPUT + }, + { + CMD_LOAD_CERT_CTRL, + "LOAD_CERT_CTRL", + "Get the certificate from card", + ENGINE_CMD_FLAG_INTERNAL + }, + { + 0, + NULL, + NULL, + 0 + } + }; + + + static bool pkcs11Initialized_ = false; + static ENGINE_CTX *context_ = NULL; + + static int EngineInitialize(ENGINE* engine) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_init(context_); + } + } + + + static int EngineFinalize(ENGINE* engine) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_finish(context_); + } + } + + + static int EngineDestroy(ENGINE* engine) + { + return (context_ == NULL ? 0 : 1); + } + + + static int EngineControl(ENGINE *engine, + int command, + long i, + void *p, + void (*f) ()) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_engine_ctrl(context_, command, i, p, f); + } + } + + + static EVP_PKEY *EngineLoadPublicKey(ENGINE *engine, + const char *s_key_id, + UI_METHOD *ui_method, + void *callback_data) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_load_public_key(context_, s_key_id, ui_method, callback_data); + } + } + + + static EVP_PKEY *EngineLoadPrivateKey(ENGINE *engine, + const char *s_key_id, + UI_METHOD *ui_method, + void *callback_data) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_load_private_key(context_, s_key_id, ui_method, callback_data); + } + } + + + static ENGINE* LoadEngine() + { + // This function creates an engine for PKCS#11 and inspired by + // the "ENGINE_load_dynamic" function from OpenSSL, in file + // "crypto/engine/eng_dyn.c" + + ENGINE* engine = ENGINE_new(); + if (!engine) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot create an OpenSSL engine for PKCS#11"); + } + + // Create a PKCS#11 context using libp11 + context_ = pkcs11_new(); + if (!context_) + { + ENGINE_free(engine); + throw OrthancException(ErrorCode_InternalError, + "Cannot create a libp11 context for PKCS#11"); + } + + if (!ENGINE_set_id(engine, PKCS11_ENGINE_ID) || + !ENGINE_set_name(engine, PKCS11_ENGINE_NAME) || + !ENGINE_set_cmd_defns(engine, PKCS11_ENGINE_COMMANDS) || + + // Register the callback functions + !ENGINE_set_init_function(engine, EngineInitialize) || + !ENGINE_set_finish_function(engine, EngineFinalize) || + !ENGINE_set_destroy_function(engine, EngineDestroy) || + !ENGINE_set_ctrl_function(engine, EngineControl) || + !ENGINE_set_load_pubkey_function(engine, EngineLoadPublicKey) || + !ENGINE_set_load_privkey_function(engine, EngineLoadPrivateKey) || + + !ENGINE_set_RSA(engine, PKCS11_get_rsa_method()) || + +#if OPENSSL_VERSION_NUMBER < 0x10100000L // OpenSSL 1.0.2 + !ENGINE_set_ECDSA(engine, PKCS11_get_ecdsa_method()) || + !ENGINE_set_ECDH(engine, PKCS11_get_ecdh_method()) || +#else + !ENGINE_set_EC(engine, PKCS11_get_ec_key_method()) || +#endif + + // Make OpenSSL know about our PKCS#11 engine + !ENGINE_add(engine)) + { + pkcs11_finish(context_); + ENGINE_free(engine); + throw OrthancException(ErrorCode_InternalError, + "Cannot initialize the OpenSSL engine for PKCS#11"); + } + + // If the "ENGINE_add" worked, it gets a structural + // reference. We release our just-created reference. + ENGINE_free(engine); + + return ENGINE_by_id(PKCS11_ENGINE_ID); + } + + + bool IsInitialized() + { + return pkcs11Initialized_; + } + + const char* GetEngineIdentifier() + { + return PKCS11_ENGINE_ID; + } + + void Initialize(const std::string& module, + const std::string& pin, + bool verbose) + { + if (pkcs11Initialized_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The PKCS#11 engine has already been initialized"); + } + + if (module.empty() || + !SystemToolbox::IsRegularFile(module)) + { + throw OrthancException( + ErrorCode_InexistentFile, + "The PKCS#11 module must be a path to one shared library (DLL or .so)"); + } + + ENGINE* engine = LoadEngine(); + if (!engine) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot create an OpenSSL engine for PKCS#11"); + } + + if (!ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", module.c_str(), 0)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot configure the OpenSSL dynamic engine for PKCS#11"); + } + + if (verbose) + { + ENGINE_ctrl_cmd_string(engine, "VERBOSE", NULL, 0); + } + + if (!pin.empty() && + !ENGINE_ctrl_cmd_string(engine, "PIN", pin.c_str(), 0)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot set the PIN code for PKCS#11"); + } + + if (!ENGINE_init(engine)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot initialize the OpenSSL dynamic engine for PKCS#11"); + } + + LOG(WARNING) << "The PKCS#11 engine has been successfully initialized"; + pkcs11Initialized_ = true; + } + + + void Finalize() + { + // Nothing to do, the unregistration of the engine is + // automatically done by OpenSSL + } + } +} diff --git a/OrthancFramework/Sources/Pkcs11.h b/OrthancFramework/Sources/Pkcs11.h new file mode 100644 index 0000000..98babdc --- /dev/null +++ b/OrthancFramework/Sources/Pkcs11.h @@ -0,0 +1,64 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if !defined(ORTHANC_ENABLE_PKCS11) +# error The macro ORTHANC_ENABLE_PKCS11 must be defined +#endif + +#if !defined(ORTHANC_ENABLE_SSL) +# error The macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error This file cannot be used in sandboxed environments +#endif + +#if ORTHANC_ENABLE_PKCS11 != 1 || ORTHANC_ENABLE_SSL != 1 +# error This file cannot be used if OpenSSL or PKCS#11 support is disabled +#endif + + +#include + +namespace Orthanc +{ + namespace Pkcs11 + { + const char* GetEngineIdentifier(); + + bool IsInitialized(); + + void Initialize(const std::string& module, + const std::string& pin, + bool verbose); + + void Finalize(); + } +} diff --git a/OrthancFramework/Sources/PrecompiledHeaders.cpp b/OrthancFramework/Sources/PrecompiledHeaders.cpp new file mode 100644 index 0000000..c5a975c --- /dev/null +++ b/OrthancFramework/Sources/PrecompiledHeaders.cpp @@ -0,0 +1,25 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" diff --git a/OrthancFramework/Sources/PrecompiledHeaders.h b/OrthancFramework/Sources/PrecompiledHeaders.h new file mode 100644 index 0000000..940e922 --- /dev/null +++ b/OrthancFramework/Sources/PrecompiledHeaders.h @@ -0,0 +1,67 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#if defined(_WIN32) && !defined(NOMINMAX) +#define NOMINMAX +#endif + +#if ORTHANC_USE_PRECOMPILED_HEADERS == 1 + +#include "OrthancFramework.h" // Must be the first one + +//#include +//#include +#include +//#include +//#include +#include +#include + +#include + +#if ORTHANC_ENABLE_PUGIXML == 1 +# include +#endif + +#include "Compatibility.h" +#include "Enumerations.h" +#include "Logging.h" +#include "OrthancException.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +// Headers from DCMTK used in Orthanc headers +# include +# include +# include +# include +#endif + +#if ORTHANC_ENABLE_DCMTK_NETWORKING == 1 +// Headers from DCMTK used in Orthanc headers +# include +#endif + +#endif diff --git a/OrthancFramework/Sources/RestApi/RestApi.cpp b/OrthancFramework/Sources/RestApi/RestApi.cpp new file mode 100644 index 0000000..560a3b6 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApi.cpp @@ -0,0 +1,940 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApi.h" + +#include "../HttpServer/StringHttpOutput.h" +#include "../Logging.h" +#include "../OrthancException.h" + +#include +#include +#include // To define "_exit()" under Windows +#include + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + class HttpHandlerVisitor : public RestApiHierarchy::IVisitor + { + private: + RestApi& api_; + RestApiOutput& output_; + RequestOrigin origin_; + const char* remoteIp_; + const char* username_; + HttpMethod method_; + const HttpToolbox::Arguments& headers_; + const HttpToolbox::Arguments& getArguments_; + const void* bodyData_; + size_t bodySize_; + + public: + HttpHandlerVisitor(RestApi& api, + RestApiOutput& output, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const HttpToolbox::Arguments& headers, + const HttpToolbox::Arguments& getArguments, + const void* bodyData, + size_t bodySize) : + api_(api), + output_(output), + origin_(origin), + remoteIp_(remoteIp), + username_(username), + method_(method), + headers_(headers), + getArguments_(getArguments), + bodyData_(bodyData), + bodySize_(bodySize) + { + } + + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + bool hasTrailing, + const HttpToolbox::Arguments& components, + const UriComponents& trailing) + { + if (resource.HasHandler(method_)) + { + switch (method_) + { + case HttpMethod_Get: + { + RestApiGetCall call(output_, api_, origin_, remoteIp_, username_, + headers_, components, trailing, uri, getArguments_); + resource.Handle(call); + return true; + } + + case HttpMethod_Post: + { + RestApiPostCall call(output_, api_, origin_, remoteIp_, username_, + headers_, components, trailing, uri, bodyData_, bodySize_); + resource.Handle(call); + return true; + } + + case HttpMethod_Delete: + { + RestApiDeleteCall call(output_, api_, origin_, remoteIp_, username_, + headers_, components, trailing, uri); + resource.Handle(call); + return true; + } + + case HttpMethod_Put: + { + RestApiPutCall call(output_, api_, origin_, remoteIp_, username_, + headers_, components, trailing, uri, bodyData_, bodySize_); + resource.Handle(call); + return true; + } + + default: + return false; + } + } + + return false; + } + }; + + + + class DocumentationVisitor : public RestApiHierarchy::IVisitor + { + private: + RestApi& restApi_; + size_t successPathsCount_; + size_t totalPathsCount_; + + protected: + virtual bool HandleCall(RestApiCall& call, + const std::string& path, + const std::set& uriArgumentsNames) = 0; + + public: + explicit DocumentationVisitor(RestApi& restApi) : + restApi_(restApi), + successPathsCount_(0), + totalPathsCount_(0) + { + } + + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + bool hasTrailing, + const HttpToolbox::Arguments& components, + const UriComponents& trailing) + { + std::string path = Toolbox::FlattenUri(uri); + if (hasTrailing) + { + path += "/{path}"; + } + + std::set uriArgumentsNames; + HttpToolbox::Arguments uriArguments; + + for (HttpToolbox::Arguments::const_iterator + it = components.begin(); it != components.end(); ++it) + { + assert(it->second.empty()); + uriArgumentsNames.insert(it->first.c_str()); + uriArguments[it->first] = ""; + } + + if (hasTrailing) + { + uriArgumentsNames.insert("path"); + uriArguments["path"] = ""; + } + + if (resource.HasHandler(HttpMethod_Get)) + { + totalPathsCount_ ++; + + StringHttpOutput o1; + HttpOutput o2(o1, false /* assume no keep-alive */, 0); + RestApiOutput o3(o2, HttpMethod_Get); + RestApiGetCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + uriArguments, UriComponents() /* trailing */, + uri, HttpToolbox::Arguments() /* GET arguments */); + + bool ok = false; + + try + { + ok = (resource.Handle(call) && + HandleCall(call, path, uriArgumentsNames)); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while documenting GET " << path << ": " << e.What(); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Bad lexical cast while documenting GET " << path; + } + + if (ok) + { + successPathsCount_ ++; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: GET " << path; + } + } + + if (resource.HasHandler(HttpMethod_Post)) + { + totalPathsCount_ ++; + + StringHttpOutput o1; + HttpOutput o2(o1, false /* assume no keep-alive */, 0); + RestApiOutput o3(o2, HttpMethod_Post); + RestApiPostCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + uriArguments, UriComponents() /* trailing */, + uri, NULL /* body */, 0 /* body size */); + + bool ok = false; + + try + { + ok = (resource.Handle(call) && + HandleCall(call, path, uriArgumentsNames)); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while documenting POST " << path << ": " << e.What(); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Bad lexical cast while documenting POST " << path; + } + + if (ok) + { + successPathsCount_ ++; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: POST " << path; + } + } + + if (resource.HasHandler(HttpMethod_Delete)) + { + totalPathsCount_ ++; + + StringHttpOutput o1; + HttpOutput o2(o1, false /* assume no keep-alive */, 0); + RestApiOutput o3(o2, HttpMethod_Delete); + RestApiDeleteCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + uriArguments, UriComponents() /* trailing */, uri); + + bool ok = false; + + try + { + ok = (resource.Handle(call) && + HandleCall(call, path, uriArgumentsNames)); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while documenting DELETE " << path << ": " << e.What(); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Bad lexical cast while documenting DELETE " << path; + } + + if (ok) + { + successPathsCount_ ++; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: DELETE " << path; + } + } + + if (resource.HasHandler(HttpMethod_Put)) + { + totalPathsCount_ ++; + + StringHttpOutput o1; + HttpOutput o2(o1, false /* assume no keep-alive */, 0); + RestApiOutput o3(o2, HttpMethod_Put); + RestApiPutCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + uriArguments, UriComponents() /* trailing */, uri, + NULL /* body */, 0 /* body size */); + + bool ok = false; + + try + { + ok = (resource.Handle(call) && + HandleCall(call, path, uriArgumentsNames)); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while documenting PUT " << path << ": " << e.What(); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Bad lexical cast while documenting PUT " << path; + } + + if (ok) + { + successPathsCount_ ++; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: PUT " << path; + } + } + + return true; + } + + size_t GetSuccessPathsCount() const + { + return successPathsCount_; + } + + size_t GetTotalPathsCount() const + { + return totalPathsCount_; + } + + void LogStatistics() const + { + assert(GetSuccessPathsCount() <= GetTotalPathsCount()); + size_t total = GetTotalPathsCount(); + if (total == 0) + { + total = 1; // Avoid division by zero + } + float coverage = (100.0f * static_cast(GetSuccessPathsCount()) / + static_cast(total)); + + LOG(WARNING) << "The documentation of the REST API contains " << GetSuccessPathsCount() + << " paths over a total of " << GetTotalPathsCount() << " paths " + << "(coverage: " << static_cast(boost::math::iround(coverage)) << "%)"; + } + }; + + + class OpenApiVisitor : public DocumentationVisitor + { + private: + Json::Value paths_; + + protected: + virtual bool HandleCall(RestApiCall& call, + const std::string& path, + const std::set& uriArgumentsNames) ORTHANC_OVERRIDE + { + Json::Value v; + if (call.GetDocumentation().FormatOpenApi(v, uriArgumentsNames, path)) + { + std::string method; + + switch (call.GetMethod()) + { + case HttpMethod_Get: + method = "get"; + break; + + case HttpMethod_Post: + method = "post"; + break; + + case HttpMethod_Delete: + method = "delete"; + break; + + case HttpMethod_Put: + method = "put"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if ((paths_.isMember(path) && + paths_[path].type() != Json::objectValue) || + paths_[path].isMember(method)) + { + throw OrthancException(ErrorCode_InternalError); + } + + paths_[path][method] = v; + + return true; + } + else + { + return false; + } + } + + public: + explicit OpenApiVisitor(RestApi& restApi) : + DocumentationVisitor(restApi), + paths_(Json::objectValue) + { + } + + const Json::Value& GetPaths() const + { + return paths_; + } + }; + + + class ReStructuredTextCheatSheet : public DocumentationVisitor + { + private: + class Path + { + private: + bool hasGet_; + bool hasPost_; + bool hasDelete_; + bool hasPut_; + std::string getTag_; + std::string postTag_; + std::string deleteTag_; + std::string putTag_; + std::string summary_; + bool getDeprecated_; + bool postDeprecated_; + bool deleteDeprecated_; + bool putDeprecated_; + HttpMethod summaryOrigin_; + + public: + Path() : + hasGet_(false), + hasPost_(false), + hasDelete_(false), + hasPut_(false), + getDeprecated_(false), + postDeprecated_(false), + deleteDeprecated_(false), + putDeprecated_(false), + summaryOrigin_(HttpMethod_Get) // Dummy initialization + { + } + + void AddMethod(HttpMethod method, + const std::string& tag, + bool deprecated) + { + switch (method) + { + case HttpMethod_Get: + if (hasGet_) + { + throw OrthancException(ErrorCode_InternalError); + } + + hasGet_ = true; + getTag_ = tag; + getDeprecated_ = deprecated; + break; + + case HttpMethod_Post: + if (hasPost_) + { + throw OrthancException(ErrorCode_InternalError); + } + + hasPost_ = true; + postTag_ = tag; + postDeprecated_ = deprecated; + break; + + case HttpMethod_Delete: + if (hasDelete_) + { + throw OrthancException(ErrorCode_InternalError); + } + + hasDelete_ = true; + deleteTag_ = tag; + deleteDeprecated_ = deprecated; + break; + + case HttpMethod_Put: + if (hasPut_) + { + throw OrthancException(ErrorCode_InternalError); + } + + hasPut_ = true; + putTag_ = tag; + putDeprecated_ = deprecated; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + void SetSummary(const std::string& summary, + HttpMethod newOrigin) + { + if (!summary.empty()) + { + bool replace; + + if (summary_.empty()) + { + // We don't have a summary so far + replace = true; + } + else + { + // We already have a summary. Replace it if the new + // summary is associated with a HTTP method of higher + // weight (GET > POST > DELETE > PUT) + switch (summaryOrigin_) + { + case HttpMethod_Get: + replace = false; + break; + + case HttpMethod_Post: + replace = (newOrigin == HttpMethod_Get); + break; + + case HttpMethod_Delete: + replace = (newOrigin == HttpMethod_Get || + newOrigin == HttpMethod_Post); + break; + + case HttpMethod_Put: + replace = (newOrigin == HttpMethod_Get || + newOrigin == HttpMethod_Post || + newOrigin == HttpMethod_Delete); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + if (replace) + { + summary_ = summary; + summaryOrigin_ = newOrigin; + } + } + } + + const std::string& GetSummary() const + { + return summary_; + } + + static std::string FormatTag(const std::string& tag) + { + if (tag.empty()) + { + return tag; + } + else + { + std::string s; + s.reserve(tag.size()); + s.push_back(tag[0]); + + for (size_t i = 1; i < tag.size(); i++) + { + if (tag[i] == ' ') + { + s.push_back('-'); + } + else if (isupper(tag[i]) && + tag[i - 1] == ' ') + { + s.push_back(tolower(tag[i])); + } + else + { + s.push_back(tag[i]); + } + } + + return s; + } + } + + std::string Format(const std::string& openApiUrl, + HttpMethod method, + const std::string& uri) const + { + std::string p = uri; + boost::replace_all(p, "/", "~1"); + + std::string verb; + std::string url; + + switch (method) + { + case HttpMethod_Get: + if (hasGet_) + { + verb = (getDeprecated_ ? "(get)" : "GET"); + url = openApiUrl + "#tag/" + FormatTag(getTag_) + "/paths/" + p + "/get"; + } + break; + + case HttpMethod_Post: + if (hasPost_) + { + verb = (postDeprecated_ ? "(post)" : "POST"); + url = openApiUrl + "#tag/" + FormatTag(postTag_) + "/paths/" + p + "/post"; + } + break; + + case HttpMethod_Delete: + if (hasDelete_) + { + verb = (deleteDeprecated_ ? "(delete)" : "DELETE"); + url = openApiUrl + "#tag/" + FormatTag(deleteTag_) + "/paths/" + p + "/delete"; + } + break; + + case HttpMethod_Put: + if (hasPut_) + { + verb = (putDeprecated_ ? "(put)" : "PUT"); + url = openApiUrl + "#tag/" + FormatTag(putTag_) + "/paths/" + p + "/put"; + } + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (verb.empty()) + { + return ""; + } + else if (openApiUrl.empty()) + { + return verb; + } + else + { + return "`" + verb + " <" + url + ">`__"; + } + } + + bool HasDeprecated() const + { + return ((hasGet_ && getDeprecated_) || + (hasPost_ && postDeprecated_) || + (hasDelete_ && deleteDeprecated_) || + (hasPut_ && putDeprecated_)); + } + }; + + typedef std::map Paths; + + Paths paths_; + + protected: + virtual bool HandleCall(RestApiCall& call, + const std::string& _path, + const std::set& uriArgumentsNames) ORTHANC_OVERRIDE + { + Path& path = paths_[ _path ]; + + path.AddMethod(call.GetMethod(), call.GetDocumentation().GetTag(), call.GetDocumentation().IsDeprecated()); + + if (call.GetDocumentation().HasSummary()) + { + path.SetSummary(call.GetDocumentation().GetSummary(), call.GetMethod()); + } + + return true; + } + + public: + explicit ReStructuredTextCheatSheet(RestApi& restApi) : + DocumentationVisitor(restApi) + { + } + + void Format(std::string& target, + const std::string& openApiUrl) const + { + target += "Path,GET,POST,DELETE,PUT,Summary\n"; + for (Paths::const_iterator it = paths_.begin(); it != paths_.end(); ++it) + { + target += "``" + it->first + "``,"; + target += it->second.Format(openApiUrl, HttpMethod_Get, it->first) + ","; + target += it->second.Format(openApiUrl, HttpMethod_Post, it->first) + ","; + target += it->second.Format(openApiUrl, HttpMethod_Delete, it->first) + ","; + target += it->second.Format(openApiUrl, HttpMethod_Put, it->first) + ","; + + if (it->second.HasDeprecated()) + { + target += "*(deprecated)* "; + } + + target += it->second.GetSummary() + "\n"; + } + } + }; + } + + + + static void AddMethod(std::string& target, + const std::string& method) + { + if (target.size() > 0) + target += "," + method; + else + target = method; + } + + static std::string MethodsToString(const std::set& methods) + { + std::string s; + + if (methods.find(HttpMethod_Get) != methods.end()) + { + AddMethod(s, "GET"); + } + + if (methods.find(HttpMethod_Post) != methods.end()) + { + AddMethod(s, "POST"); + } + + if (methods.find(HttpMethod_Put) != methods.end()) + { + AddMethod(s, "PUT"); + } + + if (methods.find(HttpMethod_Delete) != methods.end()) + { + AddMethod(s, "DELETE"); + } + + return s; + } + + + + bool RestApi::CreateChunkedRequestReader(std::unique_ptr& target, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers) + { + return false; + } + + + bool RestApi::Handle(HttpOutput& output, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers, + const HttpToolbox::GetArguments& getArguments, + const void* bodyData, + size_t bodySize) + { + RestApiOutput wrappedOutput(output, method); + +#if ORTHANC_ENABLE_PUGIXML == 1 + { + // Look if the client wishes XML answers instead of JSON + // http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html#z3 + HttpToolbox::Arguments::const_iterator it = headers.find("accept"); + if (it != headers.end()) + { + std::vector accepted; + Toolbox::TokenizeString(accepted, it->second, ';'); + for (size_t i = 0; i < accepted.size(); i++) + { + if (accepted[i] == MIME_XML) + { + wrappedOutput.SetConvertJsonToXml(true); + } + + if (accepted[i] == MIME_JSON) + { + wrappedOutput.SetConvertJsonToXml(false); + } + } + } + } +#endif + + HttpToolbox::Arguments compiled; + HttpToolbox::CompileGetArguments(compiled, getArguments); + + HttpHandlerVisitor visitor(*this, wrappedOutput, origin, remoteIp, username, + method, headers, compiled, bodyData, bodySize); + + if (root_.LookupResource(uri, visitor)) + { + wrappedOutput.Finalize(); + return true; + } + + std::set methods; + root_.GetAcceptedMethods(methods, uri); + + if (methods.empty()) + { + return false; // This URI is not served by this REST API + } + else + { + LOG(INFO) << "REST method " << EnumerationToString(method) + << " not allowed on: " << Toolbox::FlattenUri(uri); + + output.SendMethodNotAllowed(MethodsToString(methods)); + + return true; + } + } + + void RestApi::Register(const std::string& path, + RestApiGetCall::Handler handler) + { + root_.Register(path, handler); + } + + void RestApi::Register(const std::string& path, + RestApiPutCall::Handler handler) + { + root_.Register(path, handler); + } + + void RestApi::Register(const std::string& path, + RestApiPostCall::Handler handler) + { + root_.Register(path, handler); + } + + void RestApi::Register(const std::string& path, + RestApiDeleteCall::Handler handler) + { + root_.Register(path, handler); + } + + void RestApi::AutoListChildren(RestApiGetCall& call) + { + call.GetDocumentation() + .SetTag("Other") + .SetSummary("List operations") + .SetDescription("List the available operations under URI `" + call.FlattenUri() + "`") + .AddAnswerType(MimeType_Json, "List of the available operations"); + + RestApi& context = call.GetContext(); + + Json::Value directory; + if (context.root_.GetDirectory(directory, call.GetFullUri())) + { + if (call.IsDocumentation()) + { + call.GetDocumentation().SetSample(directory); + + std::set c; + call.GetUriComponentsNames(c); + for (std::set::const_iterator it = c.begin(); it != c.end(); ++it) + { + call.GetDocumentation().SetUriArgument(*it, RestApiCallDocumentation::Type_String, ""); + } + } + else + { + call.GetOutput().AnswerJson(directory); + } + } + } + + + void RestApi::GenerateOpenApiDocumentation(Json::Value& target) + { + OpenApiVisitor visitor(*this); + + UriComponents root; + std::set uriArgumentsNames; + root_.ExploreAllResources(visitor, root, uriArgumentsNames); + + target = Json::objectValue; + + target["info"] = Json::objectValue; + target["openapi"] = "3.0.0"; + target["servers"] = Json::arrayValue; + target["paths"] = visitor.GetPaths(); + + visitor.LogStatistics(); + } + + + void RestApi::GenerateReStructuredTextCheatSheet(std::string& target, + const std::string& openApiUrl) + { + ReStructuredTextCheatSheet visitor(*this); + + UriComponents root; + std::set uriArgumentsNames; + root_.ExploreAllResources(visitor, root, uriArgumentsNames); + + visitor.Format(target, openApiUrl); + + visitor.LogStatistics(); + } +} diff --git a/OrthancFramework/Sources/RestApi/RestApi.h b/OrthancFramework/Sources/RestApi/RestApi.h new file mode 100644 index 0000000..fa875a4 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApi.h @@ -0,0 +1,79 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "RestApiHierarchy.h" +#include "../Compatibility.h" +#include "../HttpServer/IHttpHandler.h" + +#include + +namespace Orthanc +{ + class RestApi : public IHttpHandler + { + private: + RestApiHierarchy root_; + + public: + static void AutoListChildren(RestApiGetCall& call); + + virtual bool CreateChunkedRequestReader(std::unique_ptr& target, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; + + virtual bool Handle(HttpOutput& output, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers, + const HttpToolbox::GetArguments& getArguments, + const void* bodyData, + size_t bodySize) ORTHANC_OVERRIDE; + + void Register(const std::string& path, + RestApiGetCall::Handler handler); + + void Register(const std::string& path, + RestApiPutCall::Handler handler); + + void Register(const std::string& path, + RestApiPostCall::Handler handler); + + void Register(const std::string& path, + RestApiDeleteCall::Handler handler); + + void GenerateOpenApiDocumentation(Json::Value& target); + + void GenerateReStructuredTextCheatSheet(std::string& target, + const std::string& openApiUrl); + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiCall.cpp b/OrthancFramework/Sources/RestApi/RestApiCall.cpp new file mode 100644 index 0000000..bfc5f6b --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiCall.cpp @@ -0,0 +1,88 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApiCall.h" + +#include "../OrthancException.h" + + +namespace Orthanc +{ + void RestApiCall::GetUriComponentsNames(std::set& components) const + { + components.clear(); + + for (HttpToolbox::Arguments::const_iterator it = uriComponents_.begin(); + it != uriComponents_.end(); ++it) + { + components.insert(it->first); + } + } + + + std::string RestApiCall::FlattenUri() const + { + std::string s = "/"; + + for (size_t i = 0; i < fullUri_.size(); i++) + { + s += fullUri_[i] + "/"; + } + + return s; + } + + + RestApiCallDocumentation& RestApiCall::GetDocumentation() + { + if (documentation_.get() == NULL) + { + documentation_.reset(new RestApiCallDocumentation(method_)); + } + + return *documentation_; + } + + + bool RestApiCall::ParseBoolean(const std::string& value) + { + std::string stripped = Toolbox::StripSpaces(value); + + if (stripped == "0" || + stripped == "false") + { + return false; + } + else if (stripped == "1" || + stripped == "true") + { + return true; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Boolean value expected"); + } + } +} diff --git a/OrthancFramework/Sources/RestApi/RestApiCall.h b/OrthancFramework/Sources/RestApi/RestApiCall.h new file mode 100644 index 0000000..45e1732 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiCall.h @@ -0,0 +1,160 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../HttpServer/HttpToolbox.h" +#include "RestApiCallDocumentation.h" +#include "RestApiPath.h" +#include "RestApiOutput.h" + +#include +#include + +namespace Orthanc +{ + class RestApi; + + class RestApiCall : public boost::noncopyable + { + private: + RestApiOutput& output_; + RestApi& context_; + RequestOrigin origin_; + const char* remoteIp_; + const char* username_; + const HttpToolbox::Arguments& httpHeaders_; + const HttpToolbox::Arguments& uriComponents_; + const UriComponents& trailing_; + const UriComponents& fullUri_; + HttpMethod method_; // To create RestApiCallDocumentation on demand + std::unique_ptr documentation_; // Lazy creation + + public: + RestApiCall(RestApiOutput& output, + RestApi& context, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri) : + output_(output), + context_(context), + origin_(origin), + remoteIp_(remoteIp), + username_(username), + httpHeaders_(httpHeaders), + uriComponents_(uriComponents), + trailing_(trailing), + fullUri_(fullUri), + method_(method) + { + } + + RestApiOutput& GetOutput() + { + return output_; + } + + RestApi& GetContext() + { + return context_; + } + + const UriComponents& GetFullUri() const + { + return fullUri_; + } + + const UriComponents& GetTrailingUri() const + { + return trailing_; + } + + void GetUriComponentsNames(std::set& components) const; + + bool HasUriComponent(const std::string& name) const + { + return (uriComponents_.find(name) != uriComponents_.end()); + } + + std::string GetUriComponent(const std::string& name, + const std::string& defaultValue) const + { + return HttpToolbox::GetArgument(uriComponents_, name, defaultValue); + } + + std::string GetHttpHeader(const std::string& name, + const std::string& defaultValue) const + { + return HttpToolbox::GetArgument(httpHeaders_, name, defaultValue); + } + + const HttpToolbox::Arguments& GetHttpHeaders() const + { + return httpHeaders_; + } + + void ParseCookies(HttpToolbox::Arguments& result) const + { + HttpToolbox::ParseCookies(result, httpHeaders_); + } + + std::string FlattenUri() const; + + RequestOrigin GetRequestOrigin() const + { + return origin_; + } + + const char* GetRemoteIp() const + { + return remoteIp_; + } + + const char* GetUsername() const + { + return username_; + } + + virtual bool ParseJsonRequest(Json::Value& result) const = 0; + + RestApiCallDocumentation& GetDocumentation(); + + HttpMethod GetMethod() const + { + return method_; + } + + bool IsDocumentation() const + { + return (origin_ == RequestOrigin_Documentation); + } + + static bool ParseBoolean(const std::string& value); + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp new file mode 100644 index 0000000..f0e6fd5 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp @@ -0,0 +1,521 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApiCallDocumentation.h" + +#if ORTHANC_ENABLE_CURL == 1 +# include "../HttpClient.h" +#endif + +#include "../Logging.h" +#include "../OrthancException.h" + + +namespace Orthanc +{ + RestApiCallDocumentation& RestApiCallDocumentation::AddRequestType(MimeType mime, + const std::string& description) + { + if (method_ != HttpMethod_Post && + method_ != HttpMethod_Put) + { + throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT"); + } + else if (requestTypes_.find(mime) != requestTypes_.end() && + mime != MimeType_Json) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of request: " + + std::string(EnumerationToString(mime))); + } + else + { + requestTypes_[mime] = description; + } + + return *this; + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetRequestField(const std::string& name, + Type type, + const std::string& description, + bool required) + { + if (method_ != HttpMethod_Post && + method_ != HttpMethod_Put) + { + throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT"); + } + + if (requestTypes_.find(MimeType_Json) == requestTypes_.end()) + { + requestTypes_[MimeType_Json] = ""; + } + + if (requestFields_.find(name) != requestFields_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON request is already documented"); + } + else + { + requestFields_[name] = Parameter(type, description, required); + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::AddAnswerType(MimeType mime, + const std::string& description) + { + if (answerTypes_.find(mime) != answerTypes_.end() && + mime != MimeType_Json) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of answer: " + + std::string(EnumerationToString(mime))); + } + else + { + answerTypes_[mime] = description; + } + + return *this; + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetUriArgument(const std::string& name, + Type type, + const std::string& description) + { + if (uriArguments_.find(name) != uriArguments_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "URI argument \"" + name + "\" is already documented"); + } + else + { + uriArguments_[name] = Parameter(type, description, true); + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetHttpHeader(const std::string& name, + const std::string& description) + { + if (httpHeaders_.find(name) != httpHeaders_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "HTTP header \"" + name + "\" is already documented"); + } + else + { + httpHeaders_[name] = Parameter(Type_String, description, false); + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetHttpGetArgument(const std::string& name, + Type type, + const std::string& description, + bool required) + { + if (method_ != HttpMethod_Get) + { + throw OrthancException(ErrorCode_InternalError, "Cannot set a HTTP GET argument on HTTP method: " + + std::string(EnumerationToString(method_))); + } + else if (getArguments_.find(name) != getArguments_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "GET argument \"" + name + "\" is already documented"); + } + else + { + getArguments_[name] = Parameter(type, description, required); + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerField(const std::string& name, + Type type, + const std::string& description) + { + if (answerTypes_.find(MimeType_Json) == answerTypes_.end()) + { + answerTypes_[MimeType_Json] = ""; + } + + if (answerFields_.find(name) != answerFields_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON answer is already documented"); + } + else + { + answerFields_[name] = Parameter(type, description, false); + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerHeader(const std::string& name, + const std::string& description) + { + if (answerHeaders_.find(name) != answerHeaders_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Answer HTTP header \"" + name + "\" is already documented"); + } + else + { + answerHeaders_[name] = Parameter(Type_String, description, false); + return *this; + } + } + + + void RestApiCallDocumentation::SetHttpGetSample(const std::string& url, + bool isJson) + { +#if ORTHANC_ENABLE_CURL == 1 + HttpClient client; + client.SetUrl(url); + client.SetHttpsVerifyPeers(false); + + if (isJson) + { + if (!client.Apply(sampleJson_)) + { + LOG(ERROR) << "Cannot GET: " << url; + sampleJson_ = Json::nullValue; + } + } + else + { + if (client.Apply(sampleText_)) + { + hasSampleText_ = true; + } + else + { + LOG(ERROR) << "Cannot GET: " << url; + hasSampleText_ = false; + } + } +#else + LOG(WARNING) << "HTTP client is not available to generated the documentation"; +#endif + } + + + static void Truncate(Json::Value& value, + size_t size) + { + if (value.type() == Json::arrayValue) + { + if (value.size() > size) + { + value.resize(size); + value.append("..."); + } + + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + Truncate(value[i], size); + } + } + else if (value.type() == Json::objectValue) + { + std::vector members = value.getMemberNames(); + if (members.size() > size) + { + members.resize(size); + + Json::Value v = Json::objectValue; + for (size_t i = 0; i < members.size(); i++) + { + v[members[i]] = value[members[i]]; + } + + // We use the "{" symbol, as it the last in the 7bit ASCII + // table, which places "..." at the end of the object in OpenAPI + v["{...}"] = "..."; + + value = v; + } + + for (size_t i = 0; i < members.size(); i++) + { + Truncate(value[members[i]], size); + } + } + } + + + void RestApiCallDocumentation::SetTruncatedJsonHttpGetSample(const std::string& url, + size_t size) + { + SetHttpGetSample(url, true); + Truncate(sampleJson_, size); + } + + + + static void TypeToSchema(Json::Value& target, + RestApiCallDocumentation::Type type) + { + switch (type) + { + case RestApiCallDocumentation::Type_Unknown: + throw OrthancException(ErrorCode_ParameterOutOfRange); + + case RestApiCallDocumentation::Type_String: + case RestApiCallDocumentation::Type_Text: + target["type"] = "string"; + return; + + case RestApiCallDocumentation::Type_Number: + target["type"] = "number"; + return; + + case RestApiCallDocumentation::Type_Boolean: + target["type"] = "boolean"; + return; + + case RestApiCallDocumentation::Type_JsonObject: + target["type"] = "object"; + return; + + case RestApiCallDocumentation::Type_JsonListOfStrings: + target["type"] = "array"; + target["items"]["type"] = "string"; + return; + + case RestApiCallDocumentation::Type_JsonListOfObjects: + target["type"] = "array"; + target["items"]["type"] = "object"; + return; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool RestApiCallDocumentation::FormatOpenApi(Json::Value& target, + const std::set& expectedUriArguments, + const std::string& uri) const + { + if (summary_.empty() && + description_.empty()) + { + return false; + } + else + { + target = Json::objectValue; + + if (!tag_.empty()) + { + target["tags"].append(tag_); + } + + if (!summary_.empty()) + { + target["summary"] = summary_; + } + else if (!description_.empty()) + { + target["summary"] = description_; + } + + if (!description_.empty()) + { + target["description"] = description_; + } + else if (!summary_.empty()) + { + target["description"] = summary_; + } + + target["deprecated"] = deprecated_; + + if (method_ == HttpMethod_Post || + method_ == HttpMethod_Put) + { + for (AllowedTypes::const_iterator it = requestTypes_.begin(); + it != requestTypes_.end(); ++it) + { + Json::Value& schema = target["requestBody"]["content"][EnumerationToString(it->first)]["schema"]; + schema["description"] = it->second; + + if (it->first == MimeType_Json) + { + for (Parameters::const_iterator field = requestFields_.begin(); + field != requestFields_.end(); ++field) + { + Json::Value p = Json::objectValue; + TypeToSchema(p, field->second.GetType()); + p["description"] = field->second.GetDescription(); + schema["properties"][field->first] = p; + } + + if (!it->second.empty() && + answerFields_.size() > 0) + { + LOG(WARNING) << "The JSON description will not be visible if the fields of the JSON request are detailed: " + << EnumerationToString(method_) << " " << uri; + } + } + } + } + + target["responses"]["200"]["description"] = (answerDescription_.empty() ? "" : answerDescription_); + + for (AllowedTypes::const_iterator it = answerTypes_.begin(); + it != answerTypes_.end(); ++it) + { + Json::Value& schema = target["responses"]["200"]["content"][EnumerationToString(it->first)]["schema"]; + schema["description"] = it->second; + + if (it->first == MimeType_Json) + { + for (Parameters::const_iterator field = answerFields_.begin(); + field != answerFields_.end(); ++field) + { + Json::Value p = Json::objectValue; + TypeToSchema(p, field->second.GetType()); + p["description"] = field->second.GetDescription(); + schema["properties"][field->first] = p; + } + + if (!it->second.empty() && + answerFields_.size() > 0) + { + LOG(WARNING) << "The JSON description will not be visible if the fields of the JSON answer are detailed: " + << EnumerationToString(method_) << " " << uri; + } + } + } + + for (AllowedTypes::const_iterator it = answerTypes_.begin(); + it != answerTypes_.end(); ++it) + { + if (it->first == MimeType_Json && + sampleJson_.type() != Json::nullValue) + { + // Handled below + } + else if (it->first == MimeType_PlainText && + hasSampleText_) + { + // Handled below + } + else + { + // No sample for this MIME type + target["responses"]["200"]["content"][EnumerationToString(it->first)]["examples"] = Json::objectValue; + } + } + + if (sampleJson_.type() != Json::nullValue) + { + target["responses"]["200"]["content"][EnumerationToString(MimeType_Json)]["schema"]["example"] = sampleJson_; + } + + if (hasSampleText_) + { + target["responses"]["200"]["content"][EnumerationToString(MimeType_PlainText)]["example"] = sampleText_; + } + + if (!answerHeaders_.empty()) + { + Json::Value answerHeaders = Json::objectValue; + + for (Parameters::const_iterator it = answerHeaders_.begin(); it != answerHeaders_.end(); ++it) + { + Json::Value h = Json::objectValue; + h["description"] = it->second.GetDescription(); + answerHeaders[it->first] = h; + } + + target["responses"]["200"]["headers"] = answerHeaders; + } + + Json::Value parameters = Json::arrayValue; + + for (Parameters::const_iterator it = getArguments_.begin(); + it != getArguments_.end(); ++it) + { + Json::Value p = Json::objectValue; + p["name"] = it->first; + p["in"] = "query"; + p["required"] = it->second.IsRequired(); + TypeToSchema(p["schema"], it->second.GetType()); + p["description"] = it->second.GetDescription(); + parameters.append(p); + } + + for (Parameters::const_iterator it = httpHeaders_.begin(); + it != httpHeaders_.end(); ++it) + { + Json::Value p = Json::objectValue; + p["name"] = it->first; + p["in"] = "header"; + p["required"] = it->second.IsRequired(); + TypeToSchema(p["schema"], it->second.GetType()); + p["description"] = it->second.GetDescription(); + parameters.append(p); + } + + for (Parameters::const_iterator it = uriArguments_.begin(); + it != uriArguments_.end(); ++it) + { + if (expectedUriArguments.find(it->first) == expectedUriArguments.end()) + { + throw OrthancException(ErrorCode_InternalError, "Unexpected URI argument: " + it->first); + } + + Json::Value p = Json::objectValue; + p["name"] = it->first; + p["in"] = "path"; + p["required"] = it->second.IsRequired(); + TypeToSchema(p["schema"], it->second.GetType()); + p["description"] = it->second.GetDescription(); + parameters.append(p); + } + + for (std::set::const_iterator it = expectedUriArguments.begin(); + it != expectedUriArguments.end(); ++it) + { + if (uriArguments_.find(*it) == uriArguments_.end()) + { + throw OrthancException(ErrorCode_InternalError, "Missing URI argument: " + *it); + } + } + + target["parameters"] = parameters; + + return true; + } + } +} diff --git a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h new file mode 100644 index 0000000..39b3f35 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h @@ -0,0 +1,222 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Enumerations.h" + +#include +#include + +#include +#include + +namespace Orthanc +{ + class RestApiCallDocumentation : public boost::noncopyable + { + public: + enum Type + { + Type_Unknown, + Type_Text, + Type_String, + Type_Number, + Type_Boolean, + Type_JsonListOfStrings, + Type_JsonListOfObjects, + Type_JsonObject + }; + + private: + class Parameter + { + private: + Type type_; + std::string description_; + bool required_; + + public: + Parameter() : + type_(Type_Unknown), + required_(false) + { + } + + Parameter(Type type, + const std::string& description, + bool required) : + type_(type), + description_(description), + required_(required) + { + } + + Type GetType() const + { + return type_; + } + + const std::string& GetDescription() const + { + return description_; + } + + bool IsRequired() const + { + return required_; + } + }; + + typedef std::map Parameters; + typedef std::map AllowedTypes; + + HttpMethod method_; + std::string tag_; + std::string summary_; + std::string description_; + Parameters uriArguments_; + Parameters httpHeaders_; + Parameters getArguments_; + AllowedTypes requestTypes_; + Parameters requestFields_; // For JSON request + AllowedTypes answerTypes_; + Parameters answerFields_; // Only if JSON object + std::string answerDescription_; + Parameters answerHeaders_; + bool hasSampleText_; + std::string sampleText_; + Json::Value sampleJson_; + bool deprecated_; + + public: + explicit RestApiCallDocumentation(HttpMethod method) : + method_(method), + hasSampleText_(false), + sampleJson_(Json::nullValue), + deprecated_(false) + { + } + + RestApiCallDocumentation& SetTag(const std::string& tag) + { + tag_ = tag; + return *this; + } + + RestApiCallDocumentation& SetSummary(const std::string& summary) + { + summary_ = summary; + return *this; + } + + RestApiCallDocumentation& SetDescription(const std::string& description) + { + description_ = description; + return *this; + } + + RestApiCallDocumentation& AddRequestType(MimeType mime, + const std::string& description); + + RestApiCallDocumentation& SetRequestField(const std::string& name, + Type type, + const std::string& description, + bool required); + + RestApiCallDocumentation& AddAnswerType(MimeType type, + const std::string& description); + + RestApiCallDocumentation& SetUriArgument(const std::string& name, + Type type, + const std::string& description); + + RestApiCallDocumentation& SetUriArgument(const std::string& name, + const std::string& description) + { + return SetUriArgument(name, Type_String, description); + } + + bool HasUriArgument(const std::string& name) const + { + return (uriArguments_.find(name) != uriArguments_.end()); + } + + RestApiCallDocumentation& SetHttpHeader(const std::string& name, + const std::string& description); + + RestApiCallDocumentation& SetHttpGetArgument(const std::string& name, + Type type, + const std::string& description, + bool required); + + RestApiCallDocumentation& SetAnswerField(const std::string& name, + Type type, + const std::string& description); + + RestApiCallDocumentation& SetAnswerHeader(const std::string& name, + const std::string& description); + + void SetHttpGetSample(const std::string& url, + bool isJson); + + void SetTruncatedJsonHttpGetSample(const std::string& url, + size_t size); + + void SetSample(const Json::Value& sample) + { + sampleJson_ = sample; + } + + bool FormatOpenApi(Json::Value& target, + const std::set& expectedUriArguments, + const std::string& uri /* only used in logs */) const; + + bool HasSummary() const + { + return !summary_.empty(); + } + + const std::string& GetSummary() const + { + return summary_; + } + + const std::string& GetTag() const + { + return tag_; + } + + RestApiCallDocumentation& SetDeprecated() + { + deprecated_ = true; + return *this; + } + + bool IsDeprecated() const + { + return deprecated_; + } + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h b/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h new file mode 100644 index 0000000..34c1067 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h @@ -0,0 +1,56 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "RestApiCall.h" + +namespace Orthanc +{ + class RestApiDeleteCall : public RestApiCall + { + public: + typedef void (*Handler) (RestApiDeleteCall& call); + + RestApiDeleteCall(RestApiOutput& output, + RestApi& context, + RequestOrigin origin, + const char* remoteIp, + const char* username, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri) : + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Delete, + httpHeaders, uriComponents, trailing, fullUri) + { + } + + virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE + { + result.clear(); + return true; + } + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp b/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp new file mode 100644 index 0000000..f9c69bb --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp @@ -0,0 +1,99 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApiGetCall.h" + +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +namespace Orthanc +{ + bool RestApiGetCall::ParseJsonRequest(Json::Value& result) const + { + result.clear(); + + for (HttpToolbox::Arguments::const_iterator + it = getArguments_.begin(); it != getArguments_.end(); ++it) + { + result[it->first] = it->second; + } + + return true; + } + + + bool RestApiGetCall::GetBooleanArgument(const std::string& name, + bool defaultValue) const + { + HttpToolbox::Arguments::const_iterator found = getArguments_.find(name); + + bool value; + + if (found == getArguments_.end()) + { + return defaultValue; + } + else if (found->second.empty()) + { + return true; + } + else if (SerializationToolbox::ParseBoolean(value, found->second)) + { + return value; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Expected a Boolean for GET argument \"" + + name + "\", found: " + found->second); + } + } + + uint32_t RestApiGetCall::GetUnsignedInteger32Argument(const std::string& name, + uint32_t defaultValue) const + { + HttpToolbox::Arguments::const_iterator found = getArguments_.find(name); + + uint32_t value; + + if (found == getArguments_.end()) + { + return defaultValue; + } + else if (found->second.empty()) + { + return true; + } + else if (SerializationToolbox::ParseUnsignedInteger32(value, found->second)) + { + return value; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Expected a Unsigned Int for GET argument \"" + + name + "\", found: " + found->second); + } + } + +} diff --git a/OrthancFramework/Sources/RestApi/RestApiGetCall.h b/OrthancFramework/Sources/RestApi/RestApiGetCall.h new file mode 100644 index 0000000..4b75791 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h @@ -0,0 +1,74 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "RestApiCall.h" + +namespace Orthanc +{ + class RestApiGetCall : public RestApiCall + { + private: + const HttpToolbox::Arguments& getArguments_; + + public: + typedef void (*Handler) (RestApiGetCall& call); + + RestApiGetCall(RestApiOutput& output, + RestApi& context, + RequestOrigin origin, + const char* remoteIp, + const char* username, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri, + const HttpToolbox::Arguments& getArguments) : + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Get, + httpHeaders, uriComponents, trailing, fullUri), + getArguments_(getArguments) + { + } + + std::string GetArgument(const std::string& name, + const std::string& defaultValue) const + { + return HttpToolbox::GetArgument(getArguments_, name, defaultValue); + } + + bool HasArgument(const std::string& name) const + { + return getArguments_.find(name) != getArguments_.end(); + } + + bool GetBooleanArgument(const std::string& name, + bool defaultValue) const; + + uint32_t GetUnsignedInteger32Argument(const std::string& name, + uint32_t defaultValue) const; + + virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE; + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp new file mode 100644 index 0000000..f3bc6cb --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp @@ -0,0 +1,543 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApiHierarchy.h" + +#include "../OrthancException.h" + +#include +#include + +namespace Orthanc +{ + RestApiHierarchy::Resource::Resource() : + getHandler_(NULL), + postHandler_(NULL), + putHandler_(NULL), + deleteHandler_(NULL) + { + } + + + bool RestApiHierarchy::Resource::HasHandler(HttpMethod method) const + { + switch (method) + { + case HttpMethod_Get: + return getHandler_ != NULL; + + case HttpMethod_Post: + return postHandler_ != NULL; + + case HttpMethod_Put: + return putHandler_ != NULL; + + case HttpMethod_Delete: + return deleteHandler_ != NULL; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void RestApiHierarchy::Resource::Register(RestApiGetCall::Handler handler) + { + getHandler_ = handler; + } + + void RestApiHierarchy::Resource::Register(RestApiPutCall::Handler handler) + { + putHandler_ = handler; + } + + void RestApiHierarchy::Resource::Register(RestApiPostCall::Handler handler) + { + postHandler_ = handler; + } + + void RestApiHierarchy::Resource::Register(RestApiDeleteCall::Handler handler) + { + deleteHandler_ = handler; + } + + + bool RestApiHierarchy::Resource::IsEmpty() const + { + return (getHandler_ == NULL && + postHandler_ == NULL && + putHandler_ == NULL && + deleteHandler_ == NULL); + } + + + RestApiHierarchy& RestApiHierarchy::AddChild(Children& children, + const std::string& name) + { + Children::iterator it = children.find(name); + + if (it == children.end()) + { + // Create new child + RestApiHierarchy *child = new RestApiHierarchy; + children[name] = child; + return *child; + } + else + { + return *it->second; + } + } + + + + bool RestApiHierarchy::Resource::Handle(RestApiGetCall& call) const + { + if (getHandler_ != NULL) + { + getHandler_(call); + return true; + } + else + { + return false; + } + } + + + bool RestApiHierarchy::Resource::Handle(RestApiPutCall& call) const + { + if (putHandler_ != NULL) + { + putHandler_(call); + return true; + } + else + { + return false; + } + } + + + bool RestApiHierarchy::Resource::Handle(RestApiPostCall& call) const + { + if (postHandler_ != NULL) + { + postHandler_(call); + return true; + } + else + { + return false; + } + } + + + bool RestApiHierarchy::Resource::Handle(RestApiDeleteCall& call) const + { + if (deleteHandler_ != NULL) + { + deleteHandler_(call); + return true; + } + else + { + return false; + } + } + + + void RestApiHierarchy::DeleteChildren(Children& children) + { + for (Children::iterator it = children.begin(); + it != children.end(); ++it) + { + delete it->second; + } + } + + + template + void RestApiHierarchy::RegisterInternal(const RestApiPath& path, + Handler handler, + size_t level) + { + if (path.GetLevelCount() == level) + { + if (path.IsUniversalTrailing()) + { + handlersWithTrailing_.Register(handler); + } + else + { + handlers_.Register(handler); + } + } + else + { + RestApiHierarchy* child; + if (path.IsWildcardLevel(level)) + { + child = &AddChild(wildcardChildren_, path.GetWildcardName(level)); + } + else + { + child = &AddChild(children_, path.GetLevelName(level)); + } + + child->RegisterInternal(path, handler, level + 1); + } + } + + + bool RestApiHierarchy::LookupResource(HttpToolbox::Arguments& components, + const UriComponents& uri, + IVisitor& visitor, + size_t level) + { + if (uri.size() != 0 && + level > uri.size()) + { + return false; + } + + // Look for an exact match on the resource of interest + if (uri.size() == 0 || + level == uri.size()) + { + UriComponents noTrailing; + + if (!handlers_.IsEmpty() && + visitor.Visit(handlers_, uri, false, components, noTrailing)) + { + return true; + } + } + + + if (level < uri.size()) // A recursive call is possible + { + // Try and go down in the hierarchy, using an exact match for the child + Children::const_iterator child = children_.find(uri[level]); + if (child != children_.end()) + { + if (child->second->LookupResource(components, uri, visitor, level + 1)) + { + return true; + } + } + + // Try and go down in the hierarchy, using wildcard rules for children + for (child = wildcardChildren_.begin(); + child != wildcardChildren_.end(); ++child) + { + HttpToolbox::Arguments subComponents = components; + subComponents[child->first] = uri[level]; + + if (child->second->LookupResource(subComponents, uri, visitor, level + 1)) + { + return true; + } + } + } + + + // As a last resort, call the universal handlers, if any + if (!handlersWithTrailing_.IsEmpty()) + { + UriComponents trailing; + trailing.resize(uri.size() - level); + size_t pos = 0; + for (size_t i = level; i < uri.size(); i++, pos++) + { + trailing[pos] = uri[i]; + } + + assert(pos == trailing.size()); + + if (visitor.Visit(handlersWithTrailing_, uri, true, components, trailing)) + { + return true; + } + } + + return false; + } + + + bool RestApiHierarchy::CanGenerateDirectory() const + { + return (handlersWithTrailing_.IsEmpty() && + wildcardChildren_.empty()); + } + + + bool RestApiHierarchy::GetDirectory(Json::Value& result, + const UriComponents& uri, + size_t level) + { + if (uri.size() == level) + { + if (CanGenerateDirectory()) + { + result = Json::arrayValue; + + for (Children::const_iterator it = children_.begin(); + it != children_.end(); ++it) + { + result.append(it->first); + } + + return true; + } + else + { + return false; + } + } + + Children::const_iterator child = children_.find(uri[level]); + if (child != children_.end()) + { + if (child->second->GetDirectory(result, uri, level + 1)) + { + return true; + } + } + + for (child = wildcardChildren_.begin(); + child != wildcardChildren_.end(); ++child) + { + if (child->second->GetDirectory(result, uri, level + 1)) + { + return true; + } + } + + return false; + } + + + RestApiHierarchy::~RestApiHierarchy() + { + DeleteChildren(children_); + DeleteChildren(wildcardChildren_); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiGetCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiPutCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiPostCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiDeleteCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::CreateSiteMap(Json::Value& target) const + { + target = Json::objectValue; + + /*std::string s = " "; + if (handlers_.HasHandler(HttpMethod_Get)) + { + s += "GET "; + } + + if (handlers_.HasHandler(HttpMethod_Post)) + { + s += "POST "; + } + + if (handlers_.HasHandler(HttpMethod_Put)) + { + s += "PUT "; + } + + if (handlers_.HasHandler(HttpMethod_Delete)) + { + s += "DELETE "; + } + + target = s;*/ + + for (Children::const_iterator it = children_.begin(); + it != children_.end(); ++it) + { + it->second->CreateSiteMap(target[it->first]); + } + + for (Children::const_iterator it = wildcardChildren_.begin(); + it != wildcardChildren_.end(); ++it) + { + it->second->CreateSiteMap(target["<" + it->first + ">"]); + } + } + + bool RestApiHierarchy::GetDirectory(Json::Value &result, const UriComponents &uri) + { + return GetDirectory(result, uri, 0); + } + + + bool RestApiHierarchy::LookupResource(const UriComponents& uri, + IVisitor& visitor) + { + HttpToolbox::Arguments components; + return LookupResource(components, uri, visitor, 0); + } + + + + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + + class AcceptedMethodsVisitor : public RestApiHierarchy::IVisitor + { + private: + std::set& methods_; + + public: + explicit AcceptedMethodsVisitor(std::set& methods) : + methods_(methods) + { + } + + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + bool hasTrailing, + const HttpToolbox::Arguments& components, + const UriComponents& trailing) + { + if (!hasTrailing) // Ignore universal handlers + { + if (resource.HasHandler(HttpMethod_Get)) + { + methods_.insert(HttpMethod_Get); + } + + if (resource.HasHandler(HttpMethod_Post)) + { + methods_.insert(HttpMethod_Post); + } + + if (resource.HasHandler(HttpMethod_Put)) + { + methods_.insert(HttpMethod_Put); + } + + if (resource.HasHandler(HttpMethod_Delete)) + { + methods_.insert(HttpMethod_Delete); + } + } + + return false; // Continue to check all the possible ways to access this URI + } + }; + } + + void RestApiHierarchy::GetAcceptedMethods(std::set& methods, + const UriComponents& uri) + { + HttpToolbox::Arguments components; + AcceptedMethodsVisitor visitor(methods); + if (LookupResource(components, uri, visitor, 0)) + { + Json::Value d; + if (GetDirectory(d, uri)) + { + methods.insert(HttpMethod_Get); + } + } + } + + void RestApiHierarchy::ExploreAllResources(IVisitor& visitor, + const UriComponents& path, + const std::set& uriArguments) const + { + HttpToolbox::Arguments args; + + for (std::set::const_iterator it = uriArguments.begin(); it != uriArguments.end(); ++it) + { + args[*it] = ""; + } + + if (!handlers_.IsEmpty()) + { + visitor.Visit(handlers_, path, false, args, UriComponents()); + } + + if (!handlersWithTrailing_.IsEmpty()) + { + visitor.Visit(handlersWithTrailing_, path, true, args, UriComponents()); + } + + for (Children::const_iterator + it = children_.begin(); it != children_.end(); ++it) + { + assert(it->second != NULL); + UriComponents c = path; + c.push_back(it->first); + it->second->ExploreAllResources(visitor, c, uriArguments); + } + + for (Children::const_iterator + it = wildcardChildren_.begin(); it != wildcardChildren_.end(); ++it) + { + if (uriArguments.find(it->first) != uriArguments.end()) + { + throw OrthancException(ErrorCode_InternalError, "Twice the same URI argument in a path: " + it->first); + } + + std::set d = uriArguments; + d.insert(it->first); + + assert(it->second != NULL); + UriComponents c = path; + c.push_back("{" + it->first + "}"); + it->second->ExploreAllResources(visitor, c, d); + } + } +} diff --git a/OrthancFramework/Sources/RestApi/RestApiHierarchy.h b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h new file mode 100644 index 0000000..30e81c1 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h @@ -0,0 +1,148 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "RestApiGetCall.h" +#include "RestApiPostCall.h" +#include "RestApiPutCall.h" +#include "RestApiDeleteCall.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC RestApiHierarchy : public boost::noncopyable + { + public: + class ORTHANC_PUBLIC Resource : public boost::noncopyable + { + private: + RestApiGetCall::Handler getHandler_; + RestApiPostCall::Handler postHandler_; + RestApiPutCall::Handler putHandler_; + RestApiDeleteCall::Handler deleteHandler_; + + public: + Resource(); + + bool HasHandler(HttpMethod method) const; + + void Register(RestApiGetCall::Handler handler); + + void Register(RestApiPutCall::Handler handler); + + void Register(RestApiPostCall::Handler handler); + + void Register(RestApiDeleteCall::Handler handler); + + bool IsEmpty() const; + + bool Handle(RestApiGetCall& call) const; + + bool Handle(RestApiPutCall& call) const; + + bool Handle(RestApiPostCall& call) const; + + bool Handle(RestApiDeleteCall& call) const; + }; + + + class IVisitor : public boost::noncopyable + { + public: + virtual ~IVisitor() + { + } + + virtual bool Visit(const Resource& resource, + const UriComponents& uri, + bool hasTrailing, + // "uriArguments" only contains their name if using "ExploreAllResources()" + const HttpToolbox::Arguments& uriArguments, + // "trailing" is empty if using "ExploreAllResources()" + const UriComponents& trailing) = 0; + }; + + + private: + typedef std::map Children; + + Resource handlers_; + Children children_; + Children wildcardChildren_; + Resource handlersWithTrailing_; + + static RestApiHierarchy& AddChild(Children& children, + const std::string& name); + + static void DeleteChildren(Children& children); + + template + void RegisterInternal(const RestApiPath& path, + Handler handler, + size_t level); + + bool CanGenerateDirectory() const; + + bool LookupResource(HttpToolbox::Arguments& components, + const UriComponents& uri, + IVisitor& visitor, + size_t level); + + bool GetDirectory(Json::Value& result, + const UriComponents& uri, + size_t level); + + public: + ~RestApiHierarchy(); + + void Register(const std::string& uri, + RestApiGetCall::Handler handler); + + void Register(const std::string& uri, + RestApiPutCall::Handler handler); + + void Register(const std::string& uri, + RestApiPostCall::Handler handler); + + void Register(const std::string& uri, + RestApiDeleteCall::Handler handler); + + void CreateSiteMap(Json::Value& target) const; + + bool GetDirectory(Json::Value& result, + const UriComponents& uri); + + bool LookupResource(const UriComponents& uri, + IVisitor& visitor); + + void GetAcceptedMethods(std::set& methods, + const UriComponents& uri); + + void ExploreAllResources(IVisitor& visitor, + const UriComponents& path, + const std::set& uriArguments) const; + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiOutput.cpp b/OrthancFramework/Sources/RestApi/RestApiOutput.cpp new file mode 100644 index 0000000..23d8bfd --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiOutput.cpp @@ -0,0 +1,224 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApiOutput.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + + +namespace Orthanc +{ + RestApiOutput::RestApiOutput(HttpOutput& output, + HttpMethod method) : + output_(output), + method_(method), + convertJsonToXml_(false) + { + alreadySent_ = false; + } + + RestApiOutput::~RestApiOutput() + { + } + + void RestApiOutput::Finalize() + { + if (!alreadySent_) + { + if (method_ == HttpMethod_Post) + { + output_.SendStatus(HttpStatus_400_BadRequest); + } + else + { + output_.SendStatus(HttpStatus_404_NotFound); + } + } + } + + void RestApiOutput::CheckStatus() + { + if (alreadySent_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void RestApiOutput::AnswerStream(IHttpStreamAnswer& stream) + { + CheckStatus(); + output_.Answer(stream); + alreadySent_ = true; + } + + + void RestApiOutput::AnswerWithoutBuffering(IHttpStreamAnswer& stream) + { + CheckStatus(); + output_.AnswerWithoutBuffering(stream); + alreadySent_ = true; + } + + + void RestApiOutput::AnswerJson(const Json::Value& value) + { + CheckStatus(); + + if (convertJsonToXml_) + { +#if ORTHANC_ENABLE_PUGIXML == 1 + std::string s; + Toolbox::JsonToXml(s, value); + + output_.SetContentType(MIME_XML_UTF8); + output_.Answer(s); +#else + throw OrthancException(ErrorCode_InternalError, + "Orthanc was compiled without XML support"); +#endif + } + else + { + std::string s; + Toolbox::WriteStyledJson(s, value); + output_.SetContentType(MIME_JSON_UTF8); + output_.Answer(s); + } + + alreadySent_ = true; + } + + void RestApiOutput::AnswerBuffer(const std::string& buffer, + MimeType contentType) + { + AnswerBuffer(buffer.size() == 0 ? NULL : buffer.c_str(), + buffer.size(), contentType); + } + + void RestApiOutput::AnswerBuffer(const void* buffer, + size_t length, + MimeType contentType) + { + CheckStatus(); + + if (convertJsonToXml_ && + contentType == MimeType_Json) + { + Json::Value json; + if (Toolbox::ReadJson(json, buffer, length)) + { + AnswerJson(json); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "The REST API tries and answers with an invalid JSON file"); + } + } + else + { + output_.SetContentType(contentType); + output_.Answer(buffer, length); + alreadySent_ = true; + } + } + + void RestApiOutput::Redirect(const std::string& path) + { + CheckStatus(); + output_.Redirect(path); + alreadySent_ = true; + } + + void RestApiOutput::SignalErrorInternal(HttpStatus status, + const char* message, + size_t messageSize) + { + if (status != HttpStatus_400_BadRequest && + status != HttpStatus_403_Forbidden && + status != HttpStatus_500_InternalServerError && + status != HttpStatus_415_UnsupportedMediaType) + { + throw OrthancException(ErrorCode_BadHttpStatusInRest); + } + + CheckStatus(); + output_.SendStatus(status, message, messageSize); + alreadySent_ = true; + } + + void RestApiOutput::SignalError(HttpStatus status) + { + SignalErrorInternal(status, NULL, 0); + } + + void RestApiOutput::SignalError(HttpStatus status, + const std::string& message) + { + SignalErrorInternal(status, message.c_str(), message.size()); + } + + void RestApiOutput::SetCookie(const std::string& name, + const std::string& value, + unsigned int maxAge) + { + if (name.find(";") != std::string::npos || + name.find(" ") != std::string::npos || + value.find(";") != std::string::npos || + value.find(" ") != std::string::npos) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + CheckStatus(); + + std::string v = value + ";path=/"; + + if (maxAge != 0) + { + v += ";max-age=" + boost::lexical_cast(maxAge); + } + + output_.SetCookie(name, v); + } + + void RestApiOutput::ResetCookie(const std::string& name) + { + // This marks the cookie to be deleted by the browser in 1 second, + // and before it actually gets deleted, its value is set to the + // empty string + SetCookie(name, "", 1); + } + + void RestApiOutput::SetContentFilename(const char* filename) + { + output_.SetContentFilename(filename); + } +} diff --git a/OrthancFramework/Sources/RestApi/RestApiOutput.h b/OrthancFramework/Sources/RestApi/RestApiOutput.h new file mode 100644 index 0000000..774b6cc --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h @@ -0,0 +1,99 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../HttpServer/HttpOutput.h" +#include "../HttpServer/HttpFileSender.h" + +#include + +namespace Orthanc +{ + class RestApiOutput + { + private: + HttpOutput& output_; + HttpMethod method_; + bool alreadySent_; + bool convertJsonToXml_; + + void CheckStatus(); + + void SignalErrorInternal(HttpStatus status, + const char* message, + size_t messageSize); + + public: + RestApiOutput(HttpOutput& output, + HttpMethod method); + + ~RestApiOutput(); + + void SetConvertJsonToXml(bool convert) + { + convertJsonToXml_ = convert; + } + + bool IsConvertJsonToXml() const + { + return convertJsonToXml_; + } + + HttpOutput& GetLowLevelOutput() const + { + return output_; + } + + void AnswerStream(IHttpStreamAnswer& stream); + + void AnswerWithoutBuffering(IHttpStreamAnswer& stream); + + void AnswerJson(const Json::Value& value); + + void AnswerBuffer(const std::string& buffer, + MimeType contentType); + + void AnswerBuffer(const void* buffer, + size_t length, + MimeType contentType); + + void SetContentFilename(const char* filename); + + void SignalError(HttpStatus status); + + void SignalError(HttpStatus status, + const std::string& message); + + void Redirect(const std::string& path); + + void SetCookie(const std::string& name, + const std::string& value, + unsigned int maxAge = 0); + + void ResetCookie(const std::string& name); + + void Finalize(); + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiPath.cpp b/OrthancFramework/Sources/RestApi/RestApiPath.cpp new file mode 100644 index 0000000..304a83c --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiPath.cpp @@ -0,0 +1,183 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApiPath.h" + +#include "../OrthancException.h" + +#include + +namespace Orthanc +{ + RestApiPath::RestApiPath(const std::string& uri) + { + Toolbox::SplitUriComponents(uri_, uri); + + if (uri_.size() == 0) + { + hasTrailing_ = false; + return; + } + + if (uri_.back() == "*") + { + hasTrailing_ = true; + uri_.pop_back(); + } + else + { + hasTrailing_ = false; + } + + components_.resize(uri_.size()); + for (size_t i = 0; i < uri_.size(); i++) + { + size_t s = uri_[i].size(); + assert(s > 0); + + if (uri_[i][0] == '{' && + uri_[i][s - 1] == '}') + { + components_[i] = uri_[i].substr(1, s - 2); + uri_[i] = ""; + } + else + { + components_[i] = ""; + } + } + } + + bool RestApiPath::Match(HttpToolbox::Arguments& components, + UriComponents& trailing, + const std::string& uriRaw) const + { + UriComponents uri; + Toolbox::SplitUriComponents(uri, uriRaw); + return Match(components, trailing, uri); + } + + bool RestApiPath::Match(HttpToolbox::Arguments& components, + UriComponents& trailing, + const UriComponents& uri) const + { + assert(uri_.size() == components_.size()); + + if (uri.size() < uri_.size()) + { + return false; + } + + if (!hasTrailing_ && uri.size() > uri_.size()) + { + return false; + } + + components.clear(); + trailing.clear(); + + assert(uri_.size() <= uri.size()); + for (size_t i = 0; i < uri_.size(); i++) + { + if (components_[i].size() == 0) + { + // This URI component is not a free parameter + if (uri_[i] != uri[i]) + { + return false; + } + } + else + { + // This URI component is a free parameter + components[components_[i]] = uri[i]; + } + } + + if (hasTrailing_) + { + trailing.assign(uri.begin() + uri_.size(), uri.end()); + } + + return true; + } + + + bool RestApiPath::Match(const UriComponents& uri) const + { + HttpToolbox::Arguments components; + UriComponents trailing; + return Match(components, trailing, uri); + } + + + size_t RestApiPath::GetLevelCount() const + { + return uri_.size(); + } + + + bool RestApiPath::IsWildcardLevel(size_t level) const + { + assert(uri_.size() == components_.size()); + + if (level >= uri_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + return uri_[level].length() == 0; + } + + bool RestApiPath::IsUniversalTrailing() const + { + return hasTrailing_; + } + + const std::string& RestApiPath::GetWildcardName(size_t level) const + { + assert(uri_.size() == components_.size()); + + if (!IsWildcardLevel(level)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + return components_[level]; + } + + const std::string& RestApiPath::GetLevelName(size_t level) const + { + assert(uri_.size() == components_.size()); + + if (IsWildcardLevel(level)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + return uri_[level]; + } +} + diff --git a/OrthancFramework/Sources/RestApi/RestApiPath.h b/OrthancFramework/Sources/RestApi/RestApiPath.h new file mode 100644 index 0000000..8325bf6 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiPath.h @@ -0,0 +1,65 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../HttpServer/HttpToolbox.h" + +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC RestApiPath : public boost::noncopyable + { + private: + UriComponents uri_; + bool hasTrailing_; + std::vector components_; + + public: + explicit RestApiPath(const std::string& uri); + + // This version is slower + bool Match(HttpToolbox::Arguments& components, + UriComponents& trailing, + const std::string& uriRaw) const; + + bool Match(HttpToolbox::Arguments& components, + UriComponents& trailing, + const UriComponents& uri) const; + + bool Match(const UriComponents& uri) const; + + size_t GetLevelCount() const; + + bool IsWildcardLevel(size_t level) const; + + bool IsUniversalTrailing() const; + + const std::string& GetWildcardName(size_t level) const; + + const std::string& GetLevelName(size_t level) const; + + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiPostCall.h b/OrthancFramework/Sources/RestApi/RestApiPostCall.h new file mode 100644 index 0000000..58a6650 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiPostCall.h @@ -0,0 +1,85 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "RestApiCall.h" + +namespace Orthanc +{ + class RestApiPostCall : public RestApiCall + { + private: + const void* bodyData_; + size_t bodySize_; + + public: + typedef void (*Handler) (RestApiPostCall& call); + + RestApiPostCall(RestApiOutput& output, + RestApi& context, + RequestOrigin origin, + const char* remoteIp, + const char* username, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri, + const void* bodyData, + size_t bodySize) : + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Post, + httpHeaders, uriComponents, trailing, fullUri), + bodyData_(bodyData), + bodySize_(bodySize) + { + } + + const void* GetBodyData() const + { + return bodyData_; + } + + size_t GetBodySize() const + { + return bodySize_; + } + + void BodyToString(std::string& result) const + { + result.assign(reinterpret_cast(bodyData_), bodySize_); + } + + virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE + { + return Toolbox::ReadJson(result, bodyData_, bodySize_); + } + + bool ParseBooleanBody() const + { + std::string s; + BodyToString(s); + return RestApiCall::ParseBoolean(s); + } + }; +} diff --git a/OrthancFramework/Sources/RestApi/RestApiPutCall.h b/OrthancFramework/Sources/RestApi/RestApiPutCall.h new file mode 100644 index 0000000..7cbf688 --- /dev/null +++ b/OrthancFramework/Sources/RestApi/RestApiPutCall.h @@ -0,0 +1,85 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "RestApiCall.h" + +namespace Orthanc +{ + class RestApiPutCall : public RestApiCall + { + private: + const void* bodyData_; + size_t bodySize_; + + public: + typedef void (*Handler) (RestApiPutCall& call); + + RestApiPutCall(RestApiOutput& output, + RestApi& context, + RequestOrigin origin, + const char* remoteIp, + const char* username, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri, + const void* bodyData, + size_t bodySize) : + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Put, + httpHeaders, uriComponents, trailing, fullUri), + bodyData_(bodyData), + bodySize_(bodySize) + { + } + + const void* GetBodyData() const + { + return bodyData_; + } + + size_t GetBodySize() const + { + return bodySize_; + } + + void BodyToString(std::string& result) const + { + result.assign(reinterpret_cast(bodyData_), bodySize_); + } + + virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE + { + return Toolbox::ReadJson(result, bodyData_, bodySize_); + } + + bool ParseBooleanBody() const + { + std::string s; + BodyToString(s); + return RestApiCall::ParseBoolean(s); + } + }; +} diff --git a/OrthancFramework/Sources/SQLite/Connection.cpp b/OrthancFramework/Sources/SQLite/Connection.cpp new file mode 100644 index 0000000..740d557 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/Connection.cpp @@ -0,0 +1,417 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../PrecompiledHeaders.h" +#endif + +#include "Connection.h" +#include "OrthancSQLiteException.h" + +#include +#include +#include + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../Logging.h" +#endif + +#include "sqlite3.h" + + +namespace Orthanc +{ + namespace SQLite + { + Connection::Connection() : + db_(NULL), + transactionNesting_(0), + needsRollback_(false) + { + } + + + Connection::~Connection() + { + Close(); + } + + + void Connection::CheckIsOpen() const + { + if (!db_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteNotOpened); + } + } + + void Connection::Open(const std::string& path) + { + if (db_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteAlreadyOpened); + } + + int err = sqlite3_open(path.c_str(), &db_); + if (err != SQLITE_OK) + { + Close(); + db_ = NULL; + throw OrthancSQLiteException(ErrorCode_SQLiteCannotOpen); + } + + // Execute PRAGMAs at this point + // http://www.sqlite.org/pragma.html + Execute("PRAGMA FOREIGN_KEYS=ON;"); + Execute("PRAGMA RECURSIVE_TRIGGERS=ON;"); + } + + void Connection::OpenInMemory() + { + Open(":memory:"); + } + + void Connection::Close() + { + ClearCache(); + + if (db_) + { + sqlite3_close(db_); + db_ = NULL; + } + } + + void Connection::ClearCache() + { + for (CachedStatements::iterator + it = cachedStatements_.begin(); + it != cachedStatements_.end(); ++it) + { + delete it->second; + } + + cachedStatements_.clear(); + } + + + StatementReference& Connection::GetCachedStatement(const StatementId& id, + const char* sql) + { + CachedStatements::iterator i = cachedStatements_.find(id); + if (i != cachedStatements_.end()) + { + if (i->second->GetReferenceCount() >= 1) + { + throw OrthancSQLiteException(ErrorCode_SQLiteStatementAlreadyUsed); + } + + return *i->second; + } + else + { + StatementReference* statement = new StatementReference(db_, sql); + cachedStatements_[id] = statement; + return *statement; + } + } + + + bool Connection::Execute(const char* sql) + { +#if ORTHANC_SQLITE_STANDALONE != 1 + CLOG(TRACE, SQLITE) << "SQLite::Connection::Execute " << sql; +#endif + + CheckIsOpen(); + + int error = sqlite3_exec(db_, sql, NULL, NULL, NULL); + if (error == SQLITE_ERROR) + { +#if ORTHANC_SQLITE_STANDALONE != 1 + LOG(ERROR) << "SQLite execute error: " << sqlite3_errmsg(db_) + << " (" << sqlite3_extended_errcode(db_) << ")"; +#endif + + throw OrthancSQLiteException(ErrorCode_SQLiteExecute); + } + else + { + return error == SQLITE_OK; + } + } + + bool Connection::Execute(const std::string &sql) + { + return Execute(sql.c_str()); + } + + // Info querying ------------------------------------------------------------- + + bool Connection::IsSQLValid(const char* sql) + { + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db_, sql, -1, &stmt, NULL) != SQLITE_OK) + return false; + + sqlite3_finalize(stmt); + return true; + } + + bool Connection::DoesTableOrIndexExist(const char* name, + const char* type) const + { + // Our SQL is non-mutating, so this cast is OK. + Statement statement(const_cast(*this), + "SELECT name FROM sqlite_master WHERE type=? AND name=?"); + statement.BindString(0, type); + statement.BindString(1, name); + return statement.Step(); // Table exists if any row was returned. + } + + bool Connection::DoesTableExist(const char* table_name) const + { + return DoesTableOrIndexExist(table_name, "table"); + } + + bool Connection::DoesIndexExist(const char* index_name) const + { + return DoesTableOrIndexExist(index_name, "index"); + } + + bool Connection::DoesColumnExist(const char* table_name, const char* column_name) const + { + std::string sql("PRAGMA TABLE_INFO("); + sql.append(table_name); + sql.append(")"); + + // Our SQL is non-mutating, so this cast is OK. + Statement statement(const_cast(*this), sql.c_str()); + + while (statement.Step()) { + if (!statement.ColumnString(1).compare(column_name)) + return true; + } + return false; + } + + int64_t Connection::GetLastInsertRowId() const + { + return sqlite3_last_insert_rowid(db_); + } + + int Connection::GetLastChangeCount() const + { + return sqlite3_changes(db_); + } + + int Connection::GetErrorCode() const + { + return sqlite3_errcode(db_); + } + + int Connection::GetLastErrno() const + { + int err = 0; + if (SQLITE_OK != sqlite3_file_control(db_, NULL, SQLITE_LAST_ERRNO, &err)) + return -2; + + return err; + } + + const char* Connection::GetErrorMessage() const + { + return sqlite3_errmsg(db_); + } + + + int Connection::ExecuteAndReturnErrorCode(const char* sql) + { + CheckIsOpen(); + return sqlite3_exec(db_, sql, NULL, NULL, NULL); + } + + bool Connection::HasCachedStatement(const StatementId &id) const + { + return cachedStatements_.find(id) != cachedStatements_.end(); + } + + int Connection::GetTransactionNesting() const + { + return transactionNesting_; + } + + bool Connection::BeginTransaction() + { + if (needsRollback_) + { + assert(transactionNesting_ > 0); + + // When we're going to rollback, fail on this begin and don't actually + // mark us as entering the nested transaction. + return false; + } + + bool success = true; + if (!transactionNesting_) + { + needsRollback_ = false; + + Statement begin(*this, SQLITE_FROM_HERE, "BEGIN TRANSACTION"); + if (!begin.Run()) + return false; + } + transactionNesting_++; + return success; + } + + void Connection::RollbackTransaction() + { + if (!transactionNesting_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction); + } + + transactionNesting_--; + + if (transactionNesting_ > 0) + { + // Mark the outermost transaction as needing rollback. + needsRollback_ = true; + return; + } + + DoRollback(); + } + + bool Connection::CommitTransaction() + { + if (!transactionNesting_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteCommitWithoutTransaction); + } + transactionNesting_--; + + if (transactionNesting_ > 0) + { + // Mark any nested transactions as failing after we've already got one. + return !needsRollback_; + } + + if (needsRollback_) + { + DoRollback(); + return false; + } + + Statement commit(*this, SQLITE_FROM_HERE, "COMMIT"); + return commit.Run(); + } + + void Connection::DoRollback() + { + Statement rollback(*this, SQLITE_FROM_HERE, "ROLLBACK"); + rollback.Run(); + needsRollback_ = false; + } + + + + + + + static void ScalarFunctionCaller(sqlite3_context* rawContext, + int argc, + sqlite3_value** argv) + { + FunctionContext context(rawContext, argc, argv); + + void* payload = sqlite3_user_data(rawContext); + assert(payload != NULL); + + IScalarFunction& func = *reinterpret_cast(payload); + func.Compute(context); + } + + + static void ScalarFunctionDestroyer(void* payload) + { + assert(payload != NULL); + delete reinterpret_cast(payload); + } + + + IScalarFunction* Connection::Register(IScalarFunction* func) + { + int err = sqlite3_create_function_v2(db_, + func->GetName(), + func->GetCardinality(), + SQLITE_UTF8, + func, + ScalarFunctionCaller, + NULL, + NULL, + ScalarFunctionDestroyer); + + if (err != SQLITE_OK) + { + delete func; + throw OrthancSQLiteException(ErrorCode_SQLiteRegisterFunction); + } + + return func; + } + + + void Connection::FlushToDisk() + { +#if ORTHANC_SQLITE_STANDALONE != 1 + CLOG(TRACE, SQLITE) << "SQLite::Connection::FlushToDisk"; +#endif + + int err = sqlite3_wal_checkpoint(db_, NULL); + + if (err != SQLITE_OK) + { + throw OrthancSQLiteException(ErrorCode_SQLiteFlush); + } + } + } +} diff --git a/OrthancFramework/Sources/SQLite/Connection.h b/OrthancFramework/Sources/SQLite/Connection.h new file mode 100644 index 0000000..1d547f7 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/Connection.h @@ -0,0 +1,179 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#include "Statement.h" +#include "IScalarFunction.h" +#include "SQLiteTypes.h" + +#include +#include + +#if !defined(__ORTHANC_FILE__) +# if defined(_MSC_VER) +# pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries") +# else +# warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries +# endif +# define __ORTHANC_FILE__ __FILE__ +#endif + +#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__) +#define SQLITE_FROM_HERE_DYNAMIC(sql) ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__, sql) + +namespace Orthanc +{ + namespace SQLite + { + class ORTHANC_PUBLIC Connection : NonCopyable + { + friend class Statement; + friend class Transaction; + + private: + // All cached statements. Keeping a reference to these statements means that + // they'll remain active. + typedef std::map CachedStatements; + CachedStatements cachedStatements_; + + // The actual sqlite database. Will be NULL before Init has been called or if + // Init resulted in an error. + sqlite3* db_; + + // Number of currently-nested transactions. + int transactionNesting_; + + // True if any of the currently nested transactions have been rolled back. + // When we get to the outermost transaction, this will determine if we do + // a rollback instead of a commit. + bool needsRollback_; + + void ClearCache(); + + void CheckIsOpen() const; + + sqlite3* GetWrappedObject() + { + return db_; + } + + StatementReference& GetCachedStatement(const StatementId& id, + const char* sql); + + bool DoesTableOrIndexExist(const char* name, + const char* type) const; + + void DoRollback(); + + public: + // The database is opened by calling Open[InMemory](). Any uncommitted + // transactions will be rolled back when this object is deleted. + Connection(); + ~Connection(); + + void Open(const std::string& path); + + void OpenInMemory(); + + void Close(); + + bool Execute(const char* sql); + + bool Execute(const std::string& sql); + + void FlushToDisk(); + + IScalarFunction* Register(IScalarFunction* func); // Takes the ownership of the function + + // Info querying ------------------------------------------------------------- + + // Used to check a |sql| statement for syntactic validity. If the + // statement is valid SQL, returns true. + bool IsSQLValid(const char* sql); + + // Returns true if the given table exists. + bool DoesTableExist(const char* table_name) const; + + // Returns true if the given index exists. + bool DoesIndexExist(const char* index_name) const; + + // Returns true if a column with the given name exists in the given table. + bool DoesColumnExist(const char* table_name, const char* column_name) const; + + // Returns sqlite's internal ID for the last inserted row. Valid only + // immediately after an insert. + int64_t GetLastInsertRowId() const; + + // Returns sqlite's count of the number of rows modified by the last + // statement executed. Will be 0 if no statement has executed or the database + // is closed. + int GetLastChangeCount() const; + + // Errors -------------------------------------------------------------------- + + // Returns the error code associated with the last sqlite operation. + int GetErrorCode() const; + + // Returns the errno associated with GetErrorCode(). See + // SQLITE_LAST_ERRNO in SQLite documentation. + int GetLastErrno() const; + + // Returns a pointer to a statically allocated string associated with the + // last sqlite operation. + const char* GetErrorMessage() const; + + + // Diagnostics (for unit tests) ---------------------------------------------- + + int ExecuteAndReturnErrorCode(const char* sql); + + bool HasCachedStatement(const StatementId& id) const; + + int GetTransactionNesting() const; + + // Transactions -------------------------------------------------------------- + + bool BeginTransaction(); + void RollbackTransaction(); + bool CommitTransaction(); + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/FunctionContext.cpp b/OrthancFramework/Sources/SQLite/FunctionContext.cpp new file mode 100644 index 0000000..69e4477 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/FunctionContext.cpp @@ -0,0 +1,136 @@ +/** + * 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 + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of the University Hospital of Liege, nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../PrecompiledHeaders.h" +#endif + +#include "FunctionContext.h" +#include "OrthancSQLiteException.h" + +#include +#include + +#include "sqlite3.h" + +namespace Orthanc +{ + namespace SQLite + { + FunctionContext::FunctionContext(struct sqlite3_context* context, + int argc, + Internals::SQLiteValue** argv) + { + assert(context != NULL); + assert(argc >= 0); + assert(argv != NULL); + + context_ = context; + argc_ = static_cast(argc); + argv_ = argv; + } + + void FunctionContext::CheckIndex(unsigned int index) const + { + if (index >= argc_) + { + throw OrthancSQLiteException(ErrorCode_ParameterOutOfRange); + } + } + + ColumnType FunctionContext::GetColumnType(unsigned int index) const + { + CheckIndex(index); + return static_cast(sqlite3_value_type(argv_[index])); + } + + unsigned int FunctionContext::GetParameterCount() const + { + return argc_; + } + + int FunctionContext::GetIntValue(unsigned int index) const + { + CheckIndex(index); + return sqlite3_value_int(argv_[index]); + } + + int64_t FunctionContext::GetInt64Value(unsigned int index) const + { + CheckIndex(index); + return sqlite3_value_int64(argv_[index]); + } + + double FunctionContext::GetDoubleValue(unsigned int index) const + { + CheckIndex(index); + return sqlite3_value_double(argv_[index]); + } + + std::string FunctionContext::GetStringValue(unsigned int index) const + { + CheckIndex(index); + return std::string(reinterpret_cast(sqlite3_value_text(argv_[index]))); + } + + bool FunctionContext::IsNullValue(unsigned int index) const + { + CheckIndex(index); + return sqlite3_value_type(argv_[index]) == SQLITE_NULL; + } + + void FunctionContext::SetNullResult() + { + sqlite3_result_null(context_); + } + + void FunctionContext::SetIntResult(int value) + { + sqlite3_result_int(context_, value); + } + + void FunctionContext::SetDoubleResult(double value) + { + sqlite3_result_double(context_, value); + } + + void FunctionContext::SetStringResult(const std::string& str) + { + sqlite3_result_text(context_, str.data(), static_cast(str.size()), SQLITE_TRANSIENT); + } + } +} diff --git a/OrthancFramework/Sources/SQLite/FunctionContext.h b/OrthancFramework/Sources/SQLite/FunctionContext.h new file mode 100644 index 0000000..562c02e --- /dev/null +++ b/OrthancFramework/Sources/SQLite/FunctionContext.h @@ -0,0 +1,85 @@ +/** + * 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 + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of the University Hospital of Liege, nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#include "Statement.h" + +namespace Orthanc +{ + namespace SQLite + { + class ORTHANC_PUBLIC FunctionContext : public NonCopyable + { + friend class Connection; + + private: + struct sqlite3_context* context_; + unsigned int argc_; + Internals::SQLiteValue** argv_; + + void CheckIndex(unsigned int index) const; + + public: + FunctionContext(struct sqlite3_context* context, + int argc, + Internals::SQLiteValue** argv); + + ColumnType GetColumnType(unsigned int index) const; + + unsigned int GetParameterCount() const; + + int GetIntValue(unsigned int index) const; + + int64_t GetInt64Value(unsigned int index) const; + + double GetDoubleValue(unsigned int index) const; + + std::string GetStringValue(unsigned int index) const; + + bool IsNullValue(unsigned int index) const; + + void SetNullResult(); + + void SetIntResult(int value); + + void SetDoubleResult(double value); + + void SetStringResult(const std::string& str); + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/IScalarFunction.h b/OrthancFramework/Sources/SQLite/IScalarFunction.h new file mode 100644 index 0000000..fe9d53e --- /dev/null +++ b/OrthancFramework/Sources/SQLite/IScalarFunction.h @@ -0,0 +1,61 @@ +/** + * 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 + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of the University Hospital of Liege, nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#include "NonCopyable.h" +#include "FunctionContext.h" + +namespace Orthanc +{ + namespace SQLite + { + class IScalarFunction : public NonCopyable + { + public: + virtual ~IScalarFunction() + { + } + + virtual const char* GetName() const = 0; + + virtual unsigned int GetCardinality() const = 0; + + virtual void Compute(FunctionContext& context) = 0; + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/ITransaction.h b/OrthancFramework/Sources/SQLite/ITransaction.h new file mode 100644 index 0000000..306414c --- /dev/null +++ b/OrthancFramework/Sources/SQLite/ITransaction.h @@ -0,0 +1,69 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#include "NonCopyable.h" + +namespace Orthanc +{ + namespace SQLite + { + class ITransaction : public NonCopyable + { + public: + virtual ~ITransaction() + { + } + + // Begins the transaction. This uses the default sqlite "deferred" transaction + // type, which means that the DB lock is lazily acquired the next time the + // database is accessed, not in the begin transaction command. + virtual void Begin() = 0; + + // Rolls back the transaction. This will happen automatically if you do + // nothing when the transaction goes out of scope. + virtual void Rollback() = 0; + + // Commits the transaction, returning true on success. + virtual void Commit() = 0; + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/NonCopyable.h b/OrthancFramework/Sources/SQLite/NonCopyable.h new file mode 100644 index 0000000..fa531a3 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/NonCopyable.h @@ -0,0 +1,63 @@ +/** + * 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 + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +namespace Orthanc +{ + namespace SQLite + { + // This class mimics "boost::noncopyable" + class NonCopyable + { + private: + NonCopyable(const NonCopyable&); + + NonCopyable& operator= (const NonCopyable&); + + protected: + NonCopyable() + { + } + + ~NonCopyable() + { + } + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h new file mode 100644 index 0000000..f608933 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h @@ -0,0 +1,157 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + + +#if ORTHANC_ENABLE_SQLITE != 1 +# error Macro ORTHANC_ENABLE_SQLITE must be set to 1 to use SQLite +#endif + + +#if ORTHANC_SQLITE_STANDALONE == 1 +#include + +namespace Orthanc +{ + namespace SQLite + { + // Auto-generated by "Resources/CodeGeneration/GenerateErrorCodes.py" + enum ErrorCode + { + ErrorCode_ParameterOutOfRange, + ErrorCode_BadParameterType, + ErrorCode_SQLiteNotOpened, + ErrorCode_SQLiteAlreadyOpened, + ErrorCode_SQLiteCannotOpen, + ErrorCode_SQLiteStatementAlreadyUsed, + ErrorCode_SQLiteExecute, + ErrorCode_SQLiteRollbackWithoutTransaction, + ErrorCode_SQLiteCommitWithoutTransaction, + ErrorCode_SQLiteRegisterFunction, + ErrorCode_SQLiteFlush, + ErrorCode_SQLiteCannotRun, + ErrorCode_SQLiteCannotStep, + ErrorCode_SQLiteBindOutOfRange, + ErrorCode_SQLitePrepareStatement, + ErrorCode_SQLiteTransactionAlreadyStarted, + ErrorCode_SQLiteTransactionCommit, + ErrorCode_SQLiteTransactionBegin + }; + + class OrthancSQLiteException : public ::std::runtime_error + { + public: + OrthancSQLiteException(ErrorCode error) : + ::std::runtime_error(EnumerationToString(error)) + { + } + + // Auto-generated by "Resources/CodeGeneration/GenerateErrorCodes.py" + static const char* EnumerationToString(ErrorCode code) + { + switch (code) + { + case ErrorCode_ParameterOutOfRange: + return "Parameter out of range"; + + case ErrorCode_BadParameterType: + return "Bad type for a parameter"; + + case ErrorCode_SQLiteNotOpened: + return "SQLite: The database is not opened"; + + case ErrorCode_SQLiteAlreadyOpened: + return "SQLite: Connection is already open"; + + case ErrorCode_SQLiteCannotOpen: + return "SQLite: Unable to open the database"; + + case ErrorCode_SQLiteStatementAlreadyUsed: + return "SQLite: This cached statement is already being referred to"; + + case ErrorCode_SQLiteExecute: + return "SQLite: Cannot execute a command"; + + case ErrorCode_SQLiteRollbackWithoutTransaction: + return "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)"; + + case ErrorCode_SQLiteCommitWithoutTransaction: + return "SQLite: Committing a nonexistent transaction"; + + case ErrorCode_SQLiteRegisterFunction: + return "SQLite: Unable to register a function"; + + case ErrorCode_SQLiteFlush: + return "SQLite: Unable to flush the database"; + + case ErrorCode_SQLiteCannotRun: + return "SQLite: Cannot run a cached statement"; + + case ErrorCode_SQLiteCannotStep: + return "SQLite: Cannot step over a cached statement"; + + case ErrorCode_SQLiteBindOutOfRange: + return "SQLite: Bind a value while out of range (serious error)"; + + case ErrorCode_SQLitePrepareStatement: + return "SQLite: Cannot prepare a cached statement"; + + case ErrorCode_SQLiteTransactionAlreadyStarted: + return "SQLite: Beginning the same transaction twice"; + + case ErrorCode_SQLiteTransactionCommit: + return "SQLite: Failure when committing the transaction"; + + case ErrorCode_SQLiteTransactionBegin: + return "SQLite: Cannot start a transaction"; + + default: + return "Unknown error code"; + } + } + }; + } +} + +#else +# include "../OrthancException.h" +# define OrthancSQLiteException ::Orthanc::OrthancException +#endif diff --git a/OrthancFramework/Sources/SQLite/README.txt b/OrthancFramework/Sources/SQLite/README.txt new file mode 100644 index 0000000..195fb39 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/README.txt @@ -0,0 +1,40 @@ +Introduction +============ + +The code in this folder is a standalone object-oriented wrapper around +SQLite3. It is derived from the code of Chromium: + +http://src.chromium.org/viewvc/chrome/trunk/src/sql/ +http://maxradi.us/documents/sqlite/ + + +Main differences with Chromium +============================== + +* The reference counting mechanism has been reimplemented to make it + simpler. +* The OrthancException class is used for the exception mechanisms. +* A statement is always valid (is_valid() always return true). +* The classes and the methods have been renamed to meet Orthanc's + coding conventions. + + +Reuse in another software +========================= + +To use the Orthanc SQLite wrapper in another project than Orthanc, you +just have to define the "ORTHANC_SQLITE_STANDALONE" macro. + +All the C++ exceptions generated by the wrapper will be objects of the +class "::Orthanc::SQLite::OrthancSQLiteException", that derives from +the standard exception class "::std::runtime_error". + + +Licensing +========= + +The code in this folder is licensed under the 3-clause BSD license, in +order to respect the original license of the code. + +It is pretty straightforward to extract the code from this folder and +to include it in another project. diff --git a/OrthancFramework/Sources/SQLite/SQLiteTypes.h b/OrthancFramework/Sources/SQLite/SQLiteTypes.h new file mode 100644 index 0000000..0b7f047 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/SQLiteTypes.h @@ -0,0 +1,76 @@ +/** + * 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 + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of the University Hospital of Liege, nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +struct sqlite3; +struct sqlite3_context; +struct sqlite3_stmt; + +#if !defined(ORTHANC_SQLITE_VERSION) +#error Please define macro ORTHANC_SQLITE_VERSION +#endif + + +/** + * "sqlite3_value" is defined as: + * - "typedef struct Mem sqlite3_value;" up to SQLite <= 3.18.2 + * - "typedef struct sqlite3_value sqlite3_value;" since SQLite >= 3.19.0. + * We create our own copy of this typedef to get around this API incompatibility. + * https://github.com/mackyle/sqlite/commit/db1d90df06a78264775a14d22c3361eb5b42be17 + **/ + +#if ORTHANC_SQLITE_VERSION < 3019000 +struct Mem; +#else +struct sqlite3_value; +#endif + +namespace Orthanc +{ + namespace SQLite + { + namespace Internals + { +#if ORTHANC_SQLITE_VERSION < 3019000 + typedef struct ::Mem SQLiteValue; +#else + typedef struct ::sqlite3_value SQLiteValue; +#endif + } + } +} diff --git a/OrthancFramework/Sources/SQLite/Statement.cpp b/OrthancFramework/Sources/SQLite/Statement.cpp new file mode 100644 index 0000000..36d31ba --- /dev/null +++ b/OrthancFramework/Sources/SQLite/Statement.cpp @@ -0,0 +1,390 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../PrecompiledHeaders.h" +#endif + +#include "Statement.h" +#include "Connection.h" + +#include +#include +#include + +#if (ORTHANC_SQLITE_STANDALONE == 1) +// Trace logging is disabled if this SQLite wrapper is used +// independently of Orthanc +# define LOG_CREATE(message); +# define LOG_APPLY(message); +#elif defined(NDEBUG) +// Trace logging is disabled in release builds +# include "../Logging.h" +# define LOG_CREATE(message); +# define LOG_APPLY(message); +#else +// Trace logging is enabled in debug builds +# include "../Logging.h" +# define LOG_CREATE(message) CLOG(TRACE, SQLITE) << "SQLite::Statement create: " << message; +# define LOG_APPLY(message); // CLOG(TRACE, SQLITE) << "SQLite::Statement apply: " << message; +#endif + +#include "sqlite3.h" + +#if defined(_MSC_VER) +#define snprintf _snprintf +#endif + + +namespace Orthanc +{ + namespace SQLite + { + int Statement::CheckError(int err, ErrorCode code) const + { + bool succeeded = (err == SQLITE_OK || err == SQLITE_ROW || err == SQLITE_DONE); + if (!succeeded) + { +#if ORTHANC_SQLITE_STANDALONE != 1 + char buffer[128]; + snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err); + LOG(ERROR) << buffer; +#endif + + throw OrthancSQLiteException(code); + } + + return err; + } + + void Statement::CheckOk(int err, ErrorCode code) const + { + if (err == SQLITE_RANGE) + { + // Binding to a non-existent variable is evidence of a serious error. + throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange); + } + else if (err != SQLITE_OK) + { +#if ORTHANC_SQLITE_STANDALONE != 1 + char buffer[128]; + snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err); + LOG(ERROR) << buffer; +#endif + + throw OrthancSQLiteException(code); + } + } + + + Statement::Statement(Connection& database, + const StatementId& id, + const std::string& sql) : + reference_(database.GetCachedStatement(id, sql.c_str())) + { + Reset(true); + LOG_CREATE(sql); + } + + + Statement::Statement(Connection& database, + const StatementId& id, + const char* sql) : + reference_(database.GetCachedStatement(id, sql)) + { + Reset(true); + LOG_CREATE(sql); + } + + Statement::~Statement() + { + Reset(); + } + + + Statement::Statement(Connection& database, + const std::string& sql) : + reference_(database.GetWrappedObject(), sql.c_str()) + { + LOG_CREATE(sql); + } + + + Statement::Statement(Connection& database, + const char* sql) : + reference_(database.GetWrappedObject(), sql) + { + LOG_CREATE(sql); + } + + + bool Statement::Run() + { + LOG_APPLY(sqlite3_sql(GetStatement())); + + return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotRun) == SQLITE_DONE; + } + + bool Statement::Step() + { + LOG_APPLY(sqlite3_sql(GetStatement())); + + return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotStep) == SQLITE_ROW; + } + + void Statement::Reset(bool clear_bound_vars) + { + // We don't call CheckError() here because sqlite3_reset() returns + // the last error that Step() caused thereby generating a second + // spurious error callback. + if (clear_bound_vars) + sqlite3_clear_bindings(GetStatement()); + //CLOG(TRACE, SQLITE) << "SQLite::Statement::Reset"; + sqlite3_reset(GetStatement()); + } + + std::string Statement::GetOriginalSQLStatement() + { + return std::string(sqlite3_sql(GetStatement())); + } + + + void Statement::BindNull(int col) + { + CheckOk(sqlite3_bind_null(GetStatement(), col + 1), + ErrorCode_BadParameterType); + } + + void Statement::BindBool(int col, bool val) + { + BindInt(col, val ? 1 : 0); + } + + void Statement::BindInt(int col, int val) + { + CheckOk(sqlite3_bind_int(GetStatement(), col + 1, val), + ErrorCode_BadParameterType); + } + + void Statement::BindInt64(int col, int64_t val) + { + CheckOk(sqlite3_bind_int64(GetStatement(), col + 1, val), + ErrorCode_BadParameterType); + } + + void Statement::BindDouble(int col, double val) + { + CheckOk(sqlite3_bind_double(GetStatement(), col + 1, val), + ErrorCode_BadParameterType); + } + + void Statement::BindCString(int col, const char* val) + { + CheckOk(sqlite3_bind_text(GetStatement(), col + 1, val, -1, SQLITE_TRANSIENT), + ErrorCode_BadParameterType); + } + + void Statement::BindString(int col, const std::string& val) + { + CheckOk(sqlite3_bind_text(GetStatement(), + col + 1, + val.data(), + static_cast(val.size()), + SQLITE_TRANSIENT), + ErrorCode_BadParameterType); + } + + /*void Statement::BindString16(int col, const string16& value) + { + BindString(col, UTF16ToUTF8(value)); + }*/ + + void Statement::BindBlob(int col, const void* val, size_t val_len) + { + if (static_cast(static_cast(val_len)) != val_len) + { + throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange); + } + else + { + CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, static_cast(val_len), SQLITE_TRANSIENT), + ErrorCode_BadParameterType); + } + } + + void Statement::BindBlob(int col, const std::string& value) + { + BindBlob(col, value.empty() ? NULL : value.c_str(), value.size()); + } + + + int Statement::ColumnCount() const + { + return sqlite3_column_count(GetStatement()); + } + + + ColumnType Statement::GetColumnType(int col) const + { + // Verify that our enum matches sqlite's values. + assert(COLUMN_TYPE_INTEGER == SQLITE_INTEGER); + assert(COLUMN_TYPE_FLOAT == SQLITE_FLOAT); + assert(COLUMN_TYPE_TEXT == SQLITE_TEXT); + assert(COLUMN_TYPE_BLOB == SQLITE_BLOB); + assert(COLUMN_TYPE_NULL == SQLITE_NULL); + + return static_cast(sqlite3_column_type(GetStatement(), col)); + } + + ColumnType Statement::GetDeclaredColumnType(int col) const + { + std::string column_type(sqlite3_column_decltype(GetStatement(), col)); + std::transform(column_type.begin(), column_type.end(), column_type.begin(), tolower); + + if (column_type == "integer") + return COLUMN_TYPE_INTEGER; + else if (column_type == "float") + return COLUMN_TYPE_FLOAT; + else if (column_type == "text") + return COLUMN_TYPE_TEXT; + else if (column_type == "blob") + return COLUMN_TYPE_BLOB; + + return COLUMN_TYPE_NULL; + } + + bool Statement::ColumnIsNull(int col) const + { + return sqlite3_column_type(GetStatement(), col) == SQLITE_NULL; + } + + bool Statement::ColumnBool(int col) const + { + return !!ColumnInt(col); + } + + int Statement::ColumnInt(int col) const + { + return sqlite3_column_int(GetStatement(), col); + } + + int64_t Statement::ColumnInt64(int col) const + { + return sqlite3_column_int64(GetStatement(), col); + } + + double Statement::ColumnDouble(int col) const + { + return sqlite3_column_double(GetStatement(), col); + } + + std::string Statement::ColumnString(int col) const + { + const char* str = reinterpret_cast( + sqlite3_column_text(GetStatement(), col)); + int len = sqlite3_column_bytes(GetStatement(), col); + + std::string result; + if (str && len > 0) + result.assign(str, len); + return result; + } + + /*string16 Statement::ColumnString16(int col) const + { + std::string s = ColumnString(col); + return !s.empty() ? UTF8ToUTF16(s) : string16(); + }*/ + + int Statement::ColumnByteLength(int col) const + { + return sqlite3_column_bytes(GetStatement(), col); + } + + const void* Statement::ColumnBlob(int col) const + { + return sqlite3_column_blob(GetStatement(), col); + } + + bool Statement::ColumnBlobAsString(int col, std::string* blob) + { + const void* p = ColumnBlob(col); + size_t len = ColumnByteLength(col); + blob->resize(len); + if (blob->size() != len) { + return false; + } + blob->assign(reinterpret_cast(p), len); + return true; + } + + /*bool Statement::ColumnBlobAsString16(int col, string16* val) const + { + const void* data = ColumnBlob(col); + size_t len = ColumnByteLength(col) / sizeof(char16); + val->resize(len); + if (val->size() != len) + return false; + val->assign(reinterpret_cast(data), len); + return true; + }*/ + + /*bool Statement::ColumnBlobAsVector(int col, std::vector* val) const + { + val->clear(); + + const void* data = sqlite3_column_blob(GetStatement(), col); + int len = sqlite3_column_bytes(GetStatement(), col); + if (data && len > 0) { + val->resize(len); + memcpy(&(*val)[0], data, len); + } + return true; + }*/ + + /*bool Statement::ColumnBlobAsVector( + int col, + std::vector* val) const + { + return ColumnBlobAsVector(col, reinterpret_cast< std::vector* >(val)); + }*/ + + } +} diff --git a/OrthancFramework/Sources/SQLite/Statement.h b/OrthancFramework/Sources/SQLite/Statement.h new file mode 100644 index 0000000..ffe9a79 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/Statement.h @@ -0,0 +1,175 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#include "NonCopyable.h" +#include "OrthancSQLiteException.h" +#include "StatementId.h" +#include "StatementReference.h" + +#include +#include + +#if ORTHANC_BUILD_UNIT_TESTS == 1 +# include +#endif + + +namespace Orthanc +{ + namespace SQLite + { + class Connection; + + // Possible return values from ColumnType in a statement. These + // should match the values in sqlite3.h. + enum ColumnType + { + COLUMN_TYPE_INTEGER = 1, + COLUMN_TYPE_FLOAT = 2, + COLUMN_TYPE_TEXT = 3, + COLUMN_TYPE_BLOB = 4, + COLUMN_TYPE_NULL = 5 + }; + + class ORTHANC_PUBLIC Statement : public NonCopyable + { + friend class Connection; + +#if ORTHANC_BUILD_UNIT_TESTS == 1 + FRIEND_TEST(SQLStatementTest, Run); + FRIEND_TEST(SQLStatementTest, Reset); +#endif + + private: + StatementReference reference_; + + int CheckError(int err, + ErrorCode code) const; + + void CheckOk(int err, + ErrorCode code) const; + + struct sqlite3_stmt* GetStatement() const + { + return reference_.GetWrappedObject(); + } + + public: + Statement(Connection& database, + const std::string& sql); + + Statement(Connection& database, + const StatementId& id, + const std::string& sql); + + Statement(Connection& database, + const char* sql); + + Statement(Connection& database, + const StatementId& id, + const char* sql); + + ~Statement(); + + bool Run(); + + bool Step(); + + // Diagnostics -------------------------------------------------------------- + + std::string GetOriginalSQLStatement(); + + + // Binding ------------------------------------------------------------------- + + // These all take a 0-based argument index + void BindNull(int col); + void BindBool(int col, bool val); + void BindInt(int col, int val); + void BindInt64(int col, int64_t val); + void BindDouble(int col, double val); + void BindCString(int col, const char* val); + void BindString(int col, const std::string& val); + //void BindString16(int col, const string16& value); + void BindBlob(int col, const void* value, size_t value_len); + void BindBlob(int col, const std::string& value); + + + // Retrieving ---------------------------------------------------------------- + + // Returns the number of output columns in the result. + int ColumnCount() const; + + // Returns the type associated with the given column. + // + // Watch out: the type may be undefined if you've done something to cause a + // "type conversion." This means requesting the value of a column of a type + // where that type is not the native type. For safety, call ColumnType only + // on a column before getting the value out in any way. + ColumnType GetColumnType(int col) const; + ColumnType GetDeclaredColumnType(int col) const; + + // These all take a 0-based argument index. + bool ColumnIsNull(int col) const ; + bool ColumnBool(int col) const; + int ColumnInt(int col) const; + int64_t ColumnInt64(int col) const; + double ColumnDouble(int col) const; + std::string ColumnString(int col) const; + //string16 ColumnString16(int col) const; + + // When reading a blob, you can get a raw pointer to the underlying data, + // along with the length, or you can just ask us to copy the blob into a + // vector. Danger! ColumnBlob may return NULL if there is no data! + int ColumnByteLength(int col) const; + const void* ColumnBlob(int col) const; + bool ColumnBlobAsString(int col, std::string* blob); + //bool ColumnBlobAsString16(int col, string16* val) const; + //bool ColumnBlobAsVector(int col, std::vector* val) const; + //bool ColumnBlobAsVector(int col, std::vector* val) const; + + // Resets the statement to its initial condition. This includes any current + // result row, and also the bound variables if the |clear_bound_vars| is true. + void Reset(bool clear_bound_vars = true); + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/StatementId.cpp b/OrthancFramework/Sources/SQLite/StatementId.cpp new file mode 100644 index 0000000..1772290 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/StatementId.cpp @@ -0,0 +1,80 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../PrecompiledHeaders.h" +#endif + +#include "StatementId.h" + +#include + +namespace Orthanc +{ + namespace SQLite + { + Orthanc::SQLite::StatementId::StatementId(const char *file, + int line) : + file_(file), + line_(line) + { + } + + Orthanc::SQLite::StatementId::StatementId(const char *file, + int line, + const std::string& statement) : + file_(file), + line_(line), + statement_(statement) + { + } + + bool StatementId::operator< (const StatementId& other) const + { + if (line_ != other.line_) + return line_ < other.line_; + + if (strcmp(file_, other.file_) < 0) + return true; + + return statement_ < other.statement_; + } + } +} diff --git a/OrthancFramework/Sources/SQLite/StatementId.h b/OrthancFramework/Sources/SQLite/StatementId.h new file mode 100644 index 0000000..76f0513 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/StatementId.h @@ -0,0 +1,73 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#if ORTHANC_SQLITE_STANDALONE == 1 +# define ORTHANC_PUBLIC +#else +# include "../OrthancFramework.h" +#endif + +namespace Orthanc +{ + namespace SQLite + { + class ORTHANC_PUBLIC StatementId + { + private: + const char* file_; + int line_; + std::string statement_; + + StatementId(); // Forbidden + + public: + StatementId(const char* file, + int line); + + StatementId(const char* file, + int line, + const std::string& statement); + + bool operator< (const StatementId& other) const; + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/StatementReference.cpp b/OrthancFramework/Sources/SQLite/StatementReference.cpp new file mode 100644 index 0000000..d67f981 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/StatementReference.cpp @@ -0,0 +1,170 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../PrecompiledHeaders.h" +#endif + +#include "StatementReference.h" +#include "OrthancSQLiteException.h" + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../Logging.h" +#endif + +#include +#include +#include "sqlite3.h" + +namespace Orthanc +{ + namespace SQLite + { + bool StatementReference::IsRoot() const + { + return root_ == NULL; + } + + StatementReference::StatementReference() + { + root_ = NULL; + refCount_ = 0; + statement_ = NULL; + assert(IsRoot()); + } + + StatementReference::StatementReference(sqlite3* database, + const char* sql) + { + if (database == NULL || sql == NULL) + { + throw OrthancSQLiteException(ErrorCode_ParameterOutOfRange); + } + + root_ = NULL; + refCount_ = 0; + + int error = sqlite3_prepare_v2(database, sql, -1, &statement_, NULL); + if (error != SQLITE_OK) + { +#if ORTHANC_SQLITE_STANDALONE != 1 + int extended = sqlite3_extended_errcode(database); + LOG(ERROR) << "SQLite: " << sqlite3_errmsg(database) << " (" << extended << ")"; + if (extended == SQLITE_IOERR_SHMSIZE /* 4874 */) + { + LOG(ERROR) << " This probably indicates that your filesystem is full"; + } +#endif + + throw OrthancSQLiteException(ErrorCode_SQLitePrepareStatement); + } + + assert(IsRoot()); + } + + StatementReference::StatementReference(StatementReference& other) + { + refCount_ = 0; + + if (other.IsRoot()) + { + root_ = &other; + } + else + { + root_ = other.root_; + } + + root_->refCount_++; + statement_ = root_->statement_; + + assert(!IsRoot()); + } + + StatementReference::~StatementReference() + { + if (IsRoot()) + { + if (refCount_ != 0) + { + // There remain references to this object. We cannot throw + // an exception because: + // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html + +#if ORTHANC_SQLITE_STANDALONE != 1 + LOG(ERROR) << "Bad value of the reference counter"; +#endif + } + else if (statement_ != NULL) + { + sqlite3_finalize(statement_); + } + } + else + { + if (root_->refCount_ == 0) + { + // There remain references to this object. We cannot throw + // an exception because: + // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html + +#if ORTHANC_SQLITE_STANDALONE != 1 + LOG(ERROR) << "Bad value of the reference counter"; +#endif + } + else + { + root_->refCount_--; + } + } + } + + uint32_t StatementReference::GetReferenceCount() const + { + return refCount_; + } + + sqlite3_stmt *StatementReference::GetWrappedObject() const + { + assert(statement_ != NULL); + return statement_; + } + } +} diff --git a/OrthancFramework/Sources/SQLite/StatementReference.h b/OrthancFramework/Sources/SQLite/StatementReference.h new file mode 100644 index 0000000..6c3a8f0 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/StatementReference.h @@ -0,0 +1,84 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#if ORTHANC_SQLITE_STANDALONE == 1 +# define ORTHANC_PUBLIC +#else +# include "../OrthancFramework.h" +#endif + +#include "NonCopyable.h" +#include "SQLiteTypes.h" + +#include +#include + + +namespace Orthanc +{ + namespace SQLite + { + class ORTHANC_PUBLIC StatementReference : NonCopyable + { + private: + StatementReference* root_; // Only used for non-root nodes + uint32_t refCount_; // Only used for root node + struct sqlite3_stmt* statement_; + + bool IsRoot() const; + + public: + StatementReference(); + + StatementReference(sqlite3* database, + const char* sql); + + StatementReference(StatementReference& other); + + ~StatementReference(); + + uint32_t GetReferenceCount() const; + + struct sqlite3_stmt* GetWrappedObject() const; + }; + } +} diff --git a/OrthancFramework/Sources/SQLite/Transaction.cpp b/OrthancFramework/Sources/SQLite/Transaction.cpp new file mode 100644 index 0000000..3fd91f1 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/Transaction.cpp @@ -0,0 +1,112 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#if ORTHANC_SQLITE_STANDALONE != 1 +#include "../PrecompiledHeaders.h" +#endif + +#include "Transaction.h" +#include "OrthancSQLiteException.h" + +namespace Orthanc +{ + namespace SQLite + { + Transaction::Transaction(Connection& connection) : + connection_(connection), + isOpen_(false) + { + } + + Transaction::~Transaction() + { + if (isOpen_) + { + connection_.RollbackTransaction(); + } + } + + bool Transaction::IsOpen() const + { + return isOpen_; + } + + void Transaction::Begin() + { + if (isOpen_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteTransactionAlreadyStarted); + } + + isOpen_ = connection_.BeginTransaction(); + if (!isOpen_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteTransactionBegin); + } + } + + void Transaction::Rollback() + { + if (!isOpen_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction); + } + + isOpen_ = false; + + connection_.RollbackTransaction(); + } + + void Transaction::Commit() + { + if (!isOpen_) + { + throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction); + } + + isOpen_ = false; + + if (!connection_.CommitTransaction()) + { + throw OrthancSQLiteException(ErrorCode_SQLiteTransactionCommit); + } + } + } +} diff --git a/OrthancFramework/Sources/SQLite/Transaction.h b/OrthancFramework/Sources/SQLite/Transaction.h new file mode 100644 index 0000000..ce352e9 --- /dev/null +++ b/OrthancFramework/Sources/SQLite/Transaction.h @@ -0,0 +1,74 @@ +/** + * 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 + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the University Hospital of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#include "Connection.h" +#include "ITransaction.h" + +namespace Orthanc +{ + namespace SQLite + { + class ORTHANC_PUBLIC Transaction : public ITransaction + { + private: + Connection& connection_; + + // True when the transaction is open, false when it's already been committed + // or rolled back. + bool isOpen_; + + public: + explicit Transaction(Connection& connection); + + virtual ~Transaction(); + + // Returns true when there is a transaction that has been successfully begun. + bool IsOpen() const; + + virtual void Begin() ORTHANC_OVERRIDE; + + virtual void Rollback() ORTHANC_OVERRIDE; + + virtual void Commit() ORTHANC_OVERRIDE; + }; + } +} diff --git a/OrthancFramework/Sources/SerializationToolbox.cpp b/OrthancFramework/Sources/SerializationToolbox.cpp new file mode 100644 index 0000000..c03784d --- /dev/null +++ b/OrthancFramework/Sources/SerializationToolbox.cpp @@ -0,0 +1,739 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "SerializationToolbox.h" + +#include "OrthancException.h" +#include "Toolbox.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "DicomParsing/FromDcmtkBridge.h" +#endif + +#include + + +namespace Orthanc +{ + static bool ParseTagInternal(DicomTag& tag, + const char* name) + { +#if ORTHANC_ENABLE_DCMTK == 1 + try + { + tag = FromDcmtkBridge::ParseTag(name); + return true; + } + catch (OrthancException&) + { + return false; + } +#else + return DicomTag::ParseHexadecimal(tag, name); +#endif + } + + + std::string SerializationToolbox::ReadString(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "String value expected in field: " + field); + } + else + { + return value[field.c_str()].asString(); + } + } + + + std::string SerializationToolbox::ReadString(const Json::Value& value, + const std::string& field, + const std::string& defaultValue) + { + if (value.isMember(field.c_str())) + { + return ReadString(value, field); + } + else + { + return defaultValue; + } + } + + + int SerializationToolbox::ReadInteger(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + (value[field.c_str()].type() != Json::intValue && + value[field.c_str()].type() != Json::uintValue)) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Integer value expected in field: " + field); + } + else + { + return value[field.c_str()].asInt(); + } + } + + + int SerializationToolbox::ReadInteger(const Json::Value& value, + const std::string& field, + int defaultValue) + { + if (value.isMember(field.c_str())) + { + return ReadInteger(value, field); + } + else + { + return defaultValue; + } + } + + + unsigned int SerializationToolbox::ReadUnsignedInteger(const Json::Value& value, + const std::string& field) + { + int tmp = ReadInteger(value, field); + + if (tmp < 0) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Unsigned integer value expected in field: " + field); + } + else + { + return static_cast(tmp); + } + } + + + unsigned int SerializationToolbox::ReadUnsignedInteger(const Json::Value& value, + const std::string& field, + unsigned int defaultValue) + { + if (value.isMember(field.c_str())) + { + return ReadUnsignedInteger(value, field); + } + else + { + return defaultValue; + } + } + + + bool SerializationToolbox::ReadBoolean(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::booleanValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Boolean value expected in field: " + field); + } + else + { + return value[field.c_str()].asBool(); + } + } + + + void SerializationToolbox::ReadArrayOfStrings(std::vector& target, + const Json::Value& valueObject, + const std::string& field) + { + if (valueObject.type() != Json::objectValue || + !valueObject.isMember(field.c_str()) || + valueObject[field.c_str()].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "List of strings expected in field: " + field); + } + + const Json::Value& arr = valueObject[field.c_str()]; + + try + { + ReadArrayOfStrings(target, arr); + } + catch (OrthancException&) + { + // more detailed error + throw OrthancException(ErrorCode_BadFileFormat, + "List of strings expected in field: " + field); + } + } + + + void SerializationToolbox::ReadArrayOfStrings(std::vector& target, + const Json::Value& valueArray) + { + if (valueArray.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "List of strings expected"); + } + + target.resize(valueArray.size()); + + for (Json::Value::ArrayIndex i = 0; i < valueArray.size(); i++) + { + if (valueArray[i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "List of strings expected"); + } + else + { + target[i] = valueArray[i].asString(); + } + } + } + + + void SerializationToolbox::ReadListOfStrings(std::list& target, + const Json::Value& value, + const std::string& field) + { + std::vector tmp; + ReadArrayOfStrings(tmp, value, field); + + target.clear(); + for (size_t i = 0; i < tmp.size(); i++) + { + target.push_back(tmp[i]); + } + } + + + void SerializationToolbox::ReadSetOfStrings(std::set& target, + const Json::Value& valueObject, + const std::string& field) + { + std::vector tmp; + ReadArrayOfStrings(tmp, valueObject, field); + + target.clear(); + for (size_t i = 0; i < tmp.size(); i++) + { + target.insert(tmp[i]); + } + } + + + void SerializationToolbox::ReadSetOfStrings(std::set& target, + const Json::Value& valueArray) + { + std::vector tmp; + ReadArrayOfStrings(tmp, valueArray); + + target.clear(); + for (size_t i = 0; i < tmp.size(); i++) + { + target.insert(tmp[i]); + } + } + + + void SerializationToolbox::ReadSetOfTags(std::set& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Set of DICOM tags expected in field: " + field); + } + + const Json::Value& arr = value[field.c_str()]; + + target.clear(); + + for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++) + { + DicomTag tag(0, 0); + + if (arr[i].type() != Json::stringValue || + !ParseTagInternal(tag, arr[i].asCString())) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Set of DICOM tags expected in field: " + field); + } + else + { + target.insert(tag); + } + } + } + + + void SerializationToolbox::ReadMapOfStrings(std::map& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of strings to strings expected in field: " + field); + } + + const Json::Value& source = value[field.c_str()]; + + target.clear(); + + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& tmp = source[members[i]]; + + if (tmp.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of string to strings expected in field: " + field); + } + else + { + target[members[i]] = tmp.asString(); + } + } + } + + + void SerializationToolbox::ReadMapOfTags(std::map& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of DICOM tags to strings expected in field: " + field); + } + + const Json::Value& source = value[field.c_str()]; + + target.clear(); + + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& tmp = source[members[i]]; + + DicomTag tag(0, 0); + + if (!ParseTagInternal(tag, members[i].c_str()) || + tmp.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Associative array of DICOM tags to strings expected in field: " + field); + } + else + { + target[tag] = tmp.asString(); + } + } + } + + + void SerializationToolbox::WriteArrayOfStrings(Json::Value& target, + const std::vector& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + for (size_t i = 0; i < values.size(); i++) + { + value.append(values[i]); + } + } + + + void SerializationToolbox::WriteListOfStrings(Json::Value& target, + const std::list& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + + for (std::list::const_iterator it = values.begin(); + it != values.end(); ++it) + { + value.append(*it); + } + } + + + void SerializationToolbox::WriteSetOfStrings(Json::Value& targetObject, + const std::set& values, + const std::string& field) + { + if (targetObject.type() != Json::objectValue || + targetObject.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& targetArray = targetObject[field]; + + targetArray = Json::arrayValue; + + WriteSetOfStrings(targetArray, values); + } + + + void SerializationToolbox::WriteSetOfStrings(Json::Value& targetArray, + const std::set& values) + { + if (targetArray.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + targetArray.clear(); + + for (std::set::const_iterator it = values.begin(); + it != values.end(); ++it) + { + targetArray.append(*it); + } + } + + + void SerializationToolbox::WriteSetOfTags(Json::Value& target, + const std::set& tags, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + + for (std::set::const_iterator it = tags.begin(); + it != tags.end(); ++it) + { + value.append(it->Format()); + } + } + + + void SerializationToolbox::WriteMapOfStrings(Json::Value& target, + const std::map& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::objectValue; + + for (std::map::const_iterator + it = values.begin(); it != values.end(); ++it) + { + value[it->first] = it->second; + } + } + + + void SerializationToolbox::WriteMapOfTags(Json::Value& target, + const std::map& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::objectValue; + + for (std::map::const_iterator + it = values.begin(); it != values.end(); ++it) + { + value[it->first.Format()] = it->second; + } + } + + + template + static bool ParseValue(T& target, + const std::string& source) + { + try + { + std::string value = Toolbox::StripSpaces(source); + if (value.empty()) + { + return false; + } + else if (!allowSigned && + value[0] == '-') + { + return false; + } + else + { + target = boost::lexical_cast(value); + return true; + } + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } + + + bool SerializationToolbox::ParseInteger32(int32_t& target, + const std::string& source) + { + int64_t tmp; + if (ParseValue(tmp, source)) + { + target = static_cast(tmp); + return (tmp == static_cast(target)); // Check no overflow occurs + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseInteger64(int64_t& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + bool SerializationToolbox::ParseUnsignedInteger32(uint32_t& target, + const std::string& source) + { + uint64_t tmp; + if (ParseValue(tmp, source)) + { + target = static_cast(tmp); + return (tmp == static_cast(target)); // Check no overflow occurs + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseUnsignedInteger64(uint64_t& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + bool SerializationToolbox::ParseFloat(float& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + bool SerializationToolbox::ParseDouble(double& target, + const std::string& source) + { + return ParseValue(target, source); + } + + + static bool GetFirstItem(std::string& target, + const std::string& source) + { + std::vector tokens; + Toolbox::TokenizeString(tokens, source, '\\'); + + if (tokens.empty()) + { + return false; + } + else + { + target = tokens[0]; + return true; + } + } + + + bool SerializationToolbox::ParseFirstInteger32(int32_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseInteger32(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstInteger64(int64_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseInteger64(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstUnsignedInteger32(uint32_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseUnsignedInteger32(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstUnsignedInteger64(uint64_t& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseUnsignedInteger64(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstFloat(float& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseFloat(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseFirstDouble(double& target, + const std::string& source) + { + std::string first; + if (GetFirstItem(first, source)) + { + return ParseDouble(target, first); + } + else + { + return false; + } + } + + + bool SerializationToolbox::ParseBoolean(bool& result, + const std::string& value) + { + if (value == "0" || + value == "false") + { + result = false; + return true; + } + else if (value == "1" || + value == "true") + { + result = true; + return true; + } + else + { + return false; + } + } +} diff --git a/OrthancFramework/Sources/SerializationToolbox.h b/OrthancFramework/Sources/SerializationToolbox.h new file mode 100644 index 0000000..e6412ed --- /dev/null +++ b/OrthancFramework/Sources/SerializationToolbox.h @@ -0,0 +1,159 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "DicomFormat/DicomTag.h" +#include "OrthancFramework.h" + +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC SerializationToolbox + { + public: + static std::string ReadString(const Json::Value& value, + const std::string& field); + + static std::string ReadString(const Json::Value& value, + const std::string& field, + const std::string& defaultValue); + + static int ReadInteger(const Json::Value& value, + const std::string& field); + + static int ReadInteger(const Json::Value& value, + const std::string& field, + int defaultValue); + + static unsigned int ReadUnsignedInteger(const Json::Value& value, + const std::string& field); + + static unsigned int ReadUnsignedInteger(const Json::Value& value, + const std::string& field, + unsigned int defaultValue); + + static bool ReadBoolean(const Json::Value& value, + const std::string& field); + + static void ReadArrayOfStrings(std::vector& target, + const Json::Value& valueObject, + const std::string& field); + + static void ReadArrayOfStrings(std::vector& target, + const Json::Value& valueArray); + + static void ReadListOfStrings(std::list& target, + const Json::Value& value, + const std::string& field); + + static void ReadSetOfStrings(std::set& target, + const Json::Value& valueObject, + const std::string& field); + + static void ReadSetOfStrings(std::set& target, + const Json::Value& valueArray); + + static void ReadSetOfTags(std::set& target, + const Json::Value& value, + const std::string& field); + + static void ReadMapOfStrings(std::map& target, + const Json::Value& value, + const std::string& field); + + static void ReadMapOfTags(std::map& target, + const Json::Value& value, + const std::string& field); + + static void WriteArrayOfStrings(Json::Value& target, + const std::vector& values, + const std::string& field); + + static void WriteListOfStrings(Json::Value& target, + const std::list& values, + const std::string& field); + + static void WriteSetOfStrings(Json::Value& targetObject, + const std::set& values, + const std::string& field); + + static void WriteSetOfStrings(Json::Value& targetArray, + const std::set& values); + + static void WriteSetOfTags(Json::Value& target, + const std::set& tags, + const std::string& field); + + static void WriteMapOfStrings(Json::Value& target, + const std::map& values, + const std::string& field); + + static void WriteMapOfTags(Json::Value& target, + const std::map& values, + const std::string& field); + + static bool ParseInteger32(int32_t& result, + const std::string& value); + + static bool ParseInteger64(int64_t& result, + const std::string& value); + + static bool ParseUnsignedInteger32(uint32_t& result, + const std::string& value); + + static bool ParseUnsignedInteger64(uint64_t& result, + const std::string& value); + + static bool ParseFloat(float& result, + const std::string& value); + + static bool ParseDouble(double& result, + const std::string& value); + + static bool ParseFirstInteger32(int32_t& result, + const std::string& value); + + static bool ParseFirstInteger64(int64_t& result, + const std::string& value); + + static bool ParseFirstUnsignedInteger32(uint32_t& result, + const std::string& value); + + static bool ParseFirstUnsignedInteger64(uint64_t& result, + const std::string& value); + + static bool ParseFirstFloat(float& result, + const std::string& value); + + static bool ParseFirstDouble(double& result, + const std::string& value); + + static bool ParseBoolean(bool& result, + const std::string& value); + }; +} diff --git a/OrthancFramework/Sources/SharedLibrary.cpp b/OrthancFramework/Sources/SharedLibrary.cpp new file mode 100644 index 0000000..93ec7d5 --- /dev/null +++ b/OrthancFramework/Sources/SharedLibrary.cpp @@ -0,0 +1,167 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "SharedLibrary.h" + +#include "Logging.h" +#include "OrthancException.h" + +#include + +#if defined(_WIN32) +#include +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#include +#else +#error Support your platform here +#endif + +namespace Orthanc +{ + SharedLibrary::SharedLibrary(const std::string& path) : + path_(path), + handle_(NULL) + { +#if defined(_WIN32) + handle_ = ::LoadLibraryA(path_.c_str()); + if (handle_ == NULL) + { + LOG(ERROR) << "LoadLibrary(" << path_ << ") failed: Error " << ::GetLastError(); + + if (::GetLastError() == ERROR_BAD_EXE_FORMAT && + sizeof(void*) == 4) + { + throw OrthancException(ErrorCode_SharedLibrary, + "You are most probably trying to load a 64bit plugin into a 32bit version of Orthanc"); + } + else if (::GetLastError() == ERROR_BAD_EXE_FORMAT && + sizeof(void*) == 8) + { + throw OrthancException(ErrorCode_SharedLibrary, + "You are most probably trying to load a 32bit plugin into a 64bit version of Orthanc"); + } + else + { + throw OrthancException(ErrorCode_SharedLibrary); + } + } + +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) + + /** + * "RTLD_LOCAL" is the default, and is only present to be + * explicit. "RTLD_DEEPBIND" was added in Orthanc 1.6.0, in order + * to avoid crashes while loading plugins from the LSB binaries of + * the Orthanc core. + * + * BUT this had no effect, and this results in a crash if loading + * the Python 2.7 plugin => We disabled it again in Orthanc 1.6.1. + **/ + +#if 0 // && defined(RTLD_DEEPBIND) // This is a GNU extension + // Disabled in Orthanc 1.6.1 + handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND); +#else + handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL); +#endif + + if (handle_ == NULL) + { + std::string explanation; + const char *tmp = ::dlerror(); + if (tmp) + { + explanation = ": Error " + std::string(tmp); + } + + LOG(ERROR) << "dlopen(" << path_ << ") failed" << explanation; + throw OrthancException(ErrorCode_SharedLibrary); + } + +#else +#error Support your platform here +#endif + } + + SharedLibrary::~SharedLibrary() + { + if (handle_) + { +#if defined(_WIN32) + ::FreeLibrary((HMODULE)handle_); +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) + ::dlclose(handle_); +#else +#error Support your platform here +#endif + } + } + + + const std::string &SharedLibrary::GetPath() const + { + return path_; + } + + + SharedLibrary::FunctionPointer SharedLibrary::GetFunctionInternal(const std::string& name) + { + if (!handle_) + { + throw OrthancException(ErrorCode_InternalError); + } + +#if defined(_WIN32) + return ::GetProcAddress((HMODULE)handle_, name.c_str()); +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) + return ::dlsym(handle_, name.c_str()); +#else +#error Support your platform here +#endif + } + + + SharedLibrary::FunctionPointer SharedLibrary::GetFunction(const std::string& name) + { + SharedLibrary::FunctionPointer result = GetFunctionInternal(name); + + if (result == NULL) + { + throw OrthancException( + ErrorCode_SharedLibrary, + "Shared library does not expose function \"" + name + "\""); + } + else + { + return result; + } + } + + + bool SharedLibrary::HasFunction(const std::string& name) + { + return GetFunctionInternal(name) != NULL; + } +} diff --git a/OrthancFramework/Sources/SharedLibrary.h b/OrthancFramework/Sources/SharedLibrary.h new file mode 100644 index 0000000..73d42d5 --- /dev/null +++ b/OrthancFramework/Sources/SharedLibrary.h @@ -0,0 +1,72 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "OrthancFramework.h" + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The namespace SystemToolbox cannot be used in sandboxed environments +#endif + +#if defined(_WIN32) +#include +#endif + +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC SharedLibrary : public boost::noncopyable + { + public: +#if defined(_WIN32) + typedef FARPROC FunctionPointer; +#else + typedef void* FunctionPointer; +#endif + + private: + std::string path_; + void *handle_; + + FunctionPointer GetFunctionInternal(const std::string& name); + + public: + explicit SharedLibrary(const std::string& path); + + ~SharedLibrary(); + + const std::string& GetPath() const; + + bool HasFunction(const std::string& name); + + FunctionPointer GetFunction(const std::string& name); + }; +} diff --git a/OrthancFramework/Sources/StringMemoryBuffer.cpp b/OrthancFramework/Sources/StringMemoryBuffer.cpp new file mode 100644 index 0000000..fc654a3 --- /dev/null +++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp @@ -0,0 +1,62 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "StringMemoryBuffer.h" + + +namespace Orthanc +{ + void StringMemoryBuffer::MoveToString(std::string& target) + { + buffer_.swap(target); + buffer_.clear(); + } + + + IMemoryBuffer* StringMemoryBuffer::CreateFromSwap(std::string& buffer) + { + std::unique_ptr result(new StringMemoryBuffer); + result->Swap(buffer); + return result.release(); + } + + + IMemoryBuffer* StringMemoryBuffer::CreateFromCopy(const std::string& buffer) + { + std::unique_ptr result(new StringMemoryBuffer); + result->Copy(buffer); + return result.release(); + } + + + IMemoryBuffer* StringMemoryBuffer::CreateFromCopy(const std::string& buffer, + size_t start /* inclusive */, + size_t end /* exclusive */) + { + std::unique_ptr result(new StringMemoryBuffer); + result->Copy(buffer, start, end); + return result.release(); + } +} diff --git a/OrthancFramework/Sources/StringMemoryBuffer.h b/OrthancFramework/Sources/StringMemoryBuffer.h new file mode 100644 index 0000000..a57b829 --- /dev/null +++ b/OrthancFramework/Sources/StringMemoryBuffer.h @@ -0,0 +1,71 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "IMemoryBuffer.h" +#include "Compatibility.h" + +namespace Orthanc +{ + class StringMemoryBuffer : public IMemoryBuffer + { + private: + std::string buffer_; + + public: + void Copy(const std::string& buffer) + { + buffer_ = buffer; + } + + void Copy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */) + { + buffer_.assign(buffer, start, end - start); + } + + void Swap(std::string& buffer) + { + buffer_.swap(buffer); + } + + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; + + virtual const void* GetData() const ORTHANC_OVERRIDE + { + return (buffer_.empty() ? NULL : buffer_.c_str()); + } + + virtual size_t GetSize() const ORTHANC_OVERRIDE + { + return buffer_.size(); + } + + static IMemoryBuffer* CreateFromSwap(std::string& buffer); + + static IMemoryBuffer* CreateFromCopy(const std::string& buffer); + + static IMemoryBuffer* CreateFromCopy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */); + }; +} diff --git a/OrthancFramework/Sources/SystemToolbox.cpp b/OrthancFramework/Sources/SystemToolbox.cpp new file mode 100644 index 0000000..d8b687b --- /dev/null +++ b/OrthancFramework/Sources/SystemToolbox.cpp @@ -0,0 +1,1269 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "SystemToolbox.h" + + +#if defined(_WIN32) +# include // For GetMacAddresses(), must be included before "windows.h" +# include + +# include // For GetMacAddresses() +# include // For "_spawnvp()" and "_getpid()" +# include // For "environ" +#else +# include // For GetMacAddresses() +# include // For GetMacAddresses() +# include // For GetMacAddresses() +# include // For "waitpid()" +# include // For "execvp()" +#endif + + +#if defined(__APPLE__) && defined(__MACH__) +# include // PATH_MAX +# include // _NSGetExecutablePath +#endif + + +#if (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) +# include // For GetMacAddresses() +# include // For GetMacAddresses() +# include // For GetMacAddresses() +#endif + + +#if defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) +# include // PATH_MAX +# include +# include +#endif + + +#if defined(__OpenBSD__) +# include // For "sysctl", "CTL_KERN" and "KERN_PROC_ARGS" +#endif + + +#include "Logging.h" +#include "OrthancException.h" +#include "Toolbox.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + + + +/*========================================================================= + The section below comes from the Boost 1.68.0 project: + https://github.com/boostorg/program_options/blob/boost-1.68.0/src/parsers.cpp + + Copyright Vladimir Prus 2002-2004. + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt + or copy at http://www.boost.org/LICENSE_1_0.txt) + =========================================================================*/ + +// The 'environ' should be declared in some cases. E.g. Linux man page says: +// (This variable must be declared in the user program, but is declared in +// the header file unistd.h in case the header files came from libc4 or libc5, +// and in case they came from glibc and _GNU_SOURCE was defined.) +// To be safe, declare it here. + +// It appears that on Mac OS X the 'environ' variable is not +// available to dynamically linked libraries. +// See: http://article.gmane.org/gmane.comp.lib.boost.devel/103843 +// See: http://lists.gnu.org/archive/html/bug-guile/2004-01/msg00013.html +#if defined(__APPLE__) && defined(__DYNAMIC__) +// The proper include for this is crt_externs.h, however it's not +// available on iOS. The right replacement is not known. See +// https://svn.boost.org/trac/boost/ticket/5053 +extern "C" +{ + extern char ***_NSGetEnviron(void); +} +# define environ (*_NSGetEnviron()) +#else +# if defined(__MWERKS__) +# include +# else +# if !defined(_WIN32) || defined(__COMO_VERSION__) +extern char** environ; +# endif +# endif +#endif + + +/*========================================================================= + End of section from the Boost 1.68.0 project + =========================================================================*/ + + +namespace Orthanc +{ + static bool finish_; + static ServerBarrierEvent barrierEvent_; + +#if defined(_WIN32) + static BOOL WINAPI ConsoleControlHandler(DWORD dwCtrlType) + { + // http://msdn.microsoft.com/en-us/library/ms683242(v=vs.85).aspx + finish_ = true; + return true; + } +#else + static void SignalHandler(int signal) + { + if (signal == SIGHUP) + { + barrierEvent_ = ServerBarrierEvent_Reload; + } + + finish_ = true; + } +#endif + + + static ServerBarrierEvent ServerBarrierInternal(const bool* stopFlag) + { +#if defined(_WIN32) + SetConsoleCtrlHandler(ConsoleControlHandler, true); +#else + signal(SIGINT, SignalHandler); + signal(SIGQUIT, SignalHandler); + signal(SIGTERM, SignalHandler); + signal(SIGHUP, SignalHandler); +#endif + + // Active loop that awakens every 100ms + finish_ = false; + barrierEvent_ = ServerBarrierEvent_Stop; + while (!(*stopFlag || finish_)) + { + SystemToolbox::USleep(100 * 1000); + } + +#if defined(_WIN32) + SetConsoleCtrlHandler(ConsoleControlHandler, false); +#else + signal(SIGINT, NULL); + signal(SIGQUIT, NULL); + signal(SIGTERM, NULL); + signal(SIGHUP, NULL); +#endif + + return barrierEvent_; + } + + + ServerBarrierEvent SystemToolbox::ServerBarrier(const bool& stopFlag) + { + return ServerBarrierInternal(&stopFlag); + } + + + ServerBarrierEvent SystemToolbox::ServerBarrier() + { + const bool stopFlag = false; + return ServerBarrierInternal(&stopFlag); + } + + + void SystemToolbox::USleep(uint64_t microSeconds) + { +#if defined(_WIN32) + ::Sleep(static_cast(microSeconds / static_cast(1000))); +#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__native_client__) + usleep(microSeconds); +#else +#error Support your platform here +#endif + } + + + static std::streamsize GetStreamSize(std::istream& f) + { + // http://www.cplusplus.com/reference/iostream/istream/tellg/ + f.seekg(0, std::ios::end); + std::streamsize size = f.tellg(); + f.seekg(0, std::ios::beg); + + return size; + } + + + void SystemToolbox::ReadFile(std::string& content, + const std::string& path, + bool log) + { + if (!IsRegularFile(path)) + { + throw OrthancException(ErrorCode_RegularFileExpected, + "The path does not point to a regular file: " + path, + log); + } + + try + { + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + path, + log); + } + + std::streamsize size = GetStreamSize(f); + content.resize(static_cast(size)); + + if (static_cast(content.size()) != size) + { + throw OrthancException(ErrorCode_InternalError, + "Reading a file that is too large for a 32bit architecture"); + } + + if (size != 0) + { + f.read(&content[0], size); + } + + f.close(); + } + catch (boost::filesystem::filesystem_error&) + { + throw OrthancException(ErrorCode_InexistentFile); + } + catch (...) // To catch "std::system_error&" in C++11 + { + throw OrthancException(ErrorCode_InexistentFile); + } + } + + + void SystemToolbox::ReadFile(std::string &content, const std::string &path) + { + ReadFile(content, path, true /* log */); + } + + + bool SystemToolbox::ReadHeader(std::string& header, + const std::string& path, + size_t headerSize) + { + if (!IsRegularFile(path)) + { + throw OrthancException(ErrorCode_RegularFileExpected, + "The path does not point to a regular file: " + path); + } + + try + { + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + + bool full = true; + + { + std::streamsize size = GetStreamSize(f); + if (size <= 0) + { + headerSize = 0; + full = false; + } + else if (static_cast(size) < headerSize) + { + headerSize = static_cast(size); // Truncate to the size of the file + full = false; + } + } + + header.resize(headerSize); + if (headerSize != 0) + { + f.read(&header[0], headerSize); + } + + f.close(); + + return full; + } + catch (boost::filesystem::filesystem_error&) + { + throw OrthancException(ErrorCode_InexistentFile); + } + catch (...) // To catch "std::system_error&" in C++11 + { + throw OrthancException(ErrorCode_InexistentFile); + } + } + + + void SystemToolbox::WriteFile(const void* content, + size_t size, + const std::string& path, + bool callFsync) + { + try + { + //boost::filesystem::ofstream f; + boost::iostreams::stream f; + + f.open(path, std::ofstream::out | std::ofstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + + if (size != 0) + { + f.write(reinterpret_cast(content), size); + + if (!f.good()) + { + f.close(); + throw OrthancException(ErrorCode_CannotWriteFile); + } + } + + if (callFsync) + { + // https://stackoverflow.com/a/23826489/881731 + f.flush(); + + bool success; + + /** + * "f->handle()" corresponds to "FILE*" (aka "HANDLE") on + * Microsoft Windows, and to "int" (file descriptor) on other + * systems: + * https://github.com/boostorg/iostreams/blob/develop/include/boost/iostreams/detail/file_handle.hpp + **/ + +#if defined(_WIN32) + // https://docs.microsoft.com/fr-fr/windows/win32/api/fileapi/nf-fileapi-flushfilebuffers + success = (::FlushFileBuffers(f->handle()) != 0); +#elif (_POSIX_C_SOURCE >= 199309L || _XOPEN_SOURCE >= 500) + success = (::fdatasync(f->handle()) == 0); +#else + success = (::fsync(f->handle()) == 0); +#endif + + if (!success) + { + throw OrthancException(ErrorCode_CannotWriteFile, "Cannot force flush to disk"); + } + } + + f.close(); + } + catch (boost::filesystem::filesystem_error&) + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + catch (...) // To catch "std::system_error&" in C++11 + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + } + + + void SystemToolbox::WriteFile(const void *content, size_t size, const std::string &path) + { + WriteFile(content, size, path, false /* don't automatically call fsync */); + } + + + void SystemToolbox::WriteFile(const std::string& content, + const std::string& path, + bool callFsync) + { + WriteFile(content.size() > 0 ? content.c_str() : NULL, + content.size(), path, callFsync); + } + + + void SystemToolbox::WriteFile(const std::string &content, const std::string &path) + { + WriteFile(content, path, false /* don't automatically call fsync */); + } + + + void SystemToolbox::RemoveFile(const std::string& path) + { + if (boost::filesystem::exists(path)) + { + if (IsRegularFile(path)) + { + boost::filesystem::remove(path); + } + else + { + throw OrthancException(ErrorCode_RegularFileExpected); + } + } + } + + + uint64_t SystemToolbox::GetFileSize(const std::string& path) + { + try + { + return static_cast(boost::filesystem::file_size(path)); + } + catch (boost::filesystem::filesystem_error&) + { + throw OrthancException(ErrorCode_InexistentFile); + } + catch (...) // To catch "std::system_error&" in C++11 + { + throw OrthancException(ErrorCode_InexistentFile); + } + } + +#if ORTHANC_ENABLE_MD5 == 1 + void SystemToolbox::ComputeStreamMD5(std::string& result, + std::istream& inputStream) + { + Toolbox::MD5Context context; + + const size_t bufferSize = 1024; + char buffer[bufferSize]; + + while (inputStream.good()) + { + inputStream.read(buffer, bufferSize); + std::streamsize bytesRead = inputStream.gcount(); + + if (bytesRead > 0) + { + context.Append(buffer, bytesRead); + } + } + + context.Export(result); + } + + + void SystemToolbox::ComputeFileMD5(std::string& result, + const std::string& path) + { + boost::filesystem::ifstream fileStream; + fileStream.open(path, std::ifstream::in | std::ifstream::binary); + + if (!fileStream.good()) + { + throw OrthancException(ErrorCode_InexistentFile, "File not found: " + path); + } + + ComputeStreamMD5(result, fileStream); + } + + + bool SystemToolbox::CompareFilesMD5(const std::string& path1, + const std::string& path2) + { + if (GetFileSize(path1) != GetFileSize(path2)) + { + return false; + } + else + { + std::string path1md5, path2md5; + + ComputeFileMD5(path1md5, path1); + ComputeFileMD5(path2md5, path2); + + return path1md5 == path2md5; + } + } +#endif + + + void SystemToolbox::MakeDirectory(const std::string& path) + { + if (boost::filesystem::exists(path)) + { + if (!boost::filesystem::is_directory(path)) + { + throw OrthancException(ErrorCode_DirectoryOverFile); + } + } + else + { + if (!boost::filesystem::create_directories(path)) + { + throw OrthancException(ErrorCode_MakeDirectory); + } + } + } + + + bool SystemToolbox::IsExistingFile(const std::string& path) + { + return boost::filesystem::exists(path); + } + + +#if defined(_WIN32) + static std::string GetPathToExecutableInternal() + { + // Yes, this is ugly, but there is no simple way to get the + // required buffer size, so we use a big constant + std::vector buffer(32768); + /*int bytes =*/ GetModuleFileNameA(NULL, &buffer[0], static_cast(buffer.size() - 1)); + return std::string(&buffer[0]); + } + +#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) + static std::string GetPathToExecutableInternal() + { + // NOTE: For FreeBSD, using KERN_PROC_PATHNAME might be a better alternative + + std::vector buffer(PATH_MAX + 1); + ssize_t bytes = readlink("/proc/self/exe", &buffer[0], buffer.size() - 1); + if (bytes == 0) + { + throw OrthancException(ErrorCode_PathToExecutable); + } + + return std::string(&buffer[0]); + } + +#elif defined(__APPLE__) && defined(__MACH__) + static std::string GetPathToExecutableInternal() + { + char pathbuf[PATH_MAX + 1]; + unsigned int bufsize = static_cast(sizeof(pathbuf)); + + _NSGetExecutablePath( pathbuf, &bufsize); + + return std::string(pathbuf); + } + +#elif defined(__OpenBSD__) + static std::string GetPathToExecutableInternal() + { + // This is an adapted version of the patch proposed in issue #64 + // without an explicit call to "malloc()" to prevent memory leak + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=64 + // https://stackoverflow.com/q/31494901/881731 + + const int mib[4] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV }; + + size_t len; + if (sysctl(mib, 4, NULL, &len, NULL, 0) == -1) + { + throw OrthancException(ErrorCode_PathToExecutable); + } + + std::string tmp; + tmp.resize(len); + + char** buffer = reinterpret_cast(&tmp[0]); + + if (sysctl(mib, 4, buffer, &len, NULL, 0) == -1) + { + throw OrthancException(ErrorCode_PathToExecutable); + } + else + { + return std::string(buffer[0]); + } + } + +#else +#error Support your platform here +#endif + + + std::string SystemToolbox::GetPathToExecutable() + { + boost::filesystem::path p(GetPathToExecutableInternal()); + return boost::filesystem::absolute(p).string(); + } + + + std::string SystemToolbox::GetDirectoryOfExecutable() + { + boost::filesystem::path p(GetPathToExecutableInternal()); + return boost::filesystem::absolute(p.parent_path()).string(); + } + + + void SystemToolbox::ExecuteSystemCommand(const std::string& command, + const std::vector& arguments) + { + // Convert the arguments as a C array + std::vector args(arguments.size() + 2); + + args.front() = const_cast(command.c_str()); + + for (size_t i = 0; i < arguments.size(); i++) + { + args[i + 1] = const_cast(arguments[i].c_str()); + } + + args.back() = NULL; + + int status; + +#if defined(_WIN32) + // http://msdn.microsoft.com/en-us/library/275khfab.aspx + status = static_cast(_spawnvp(_P_OVERLAY, command.c_str(), &args[0])); + +#else + int pid = fork(); + + if (pid == -1) + { + // Error in fork() + throw OrthancException(ErrorCode_SystemCommand, "Cannot fork a child process"); + } + else if (pid == 0) + { + // Execute the system command in the child process + execvp(command.c_str(), &args[0]); + + // We should never get here + _exit(1); + } + else + { + // Wait for the system command to exit + waitpid(pid, &status, 0); + } +#endif + + if (status != 0) + { + throw OrthancException(ErrorCode_SystemCommand, + "System command failed with status code " + + boost::lexical_cast(status)); + } + } + + + int SystemToolbox::GetProcessId() + { +#if defined(_WIN32) + return static_cast(_getpid()); +#else + return static_cast(getpid()); +#endif + } + + + bool SystemToolbox::IsRegularFile(const std::string& path) + { + try + { + if (boost::filesystem::exists(path)) + { + boost::filesystem::file_status status = boost::filesystem::status(path); + return (status.type() == boost::filesystem::regular_file || + status.type() == boost::filesystem::reparse_file); // Fix BitBucket issue #11 + } + } + catch (boost::filesystem::filesystem_error&) + { + } + + return false; + } + + + FILE* SystemToolbox::OpenFile(const std::string& path, + FileMode mode) + { +#if defined(_WIN32) + // TODO Deal with special characters by converting to the current locale +#endif + + const char* m; + switch (mode) + { + case FileMode_ReadBinary: + m = "rb"; + break; + + case FileMode_WriteBinary: + m = "wb"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + return fopen(path.c_str(), m); + } + + + static boost::posix_time::ptime GetNow(bool utc) + { + if (utc) + { + return boost::posix_time::second_clock::universal_time(); + } + else + { + return boost::posix_time::second_clock::local_time(); + } + } + + + std::string SystemToolbox::GetNowIsoString(bool utc) + { + return boost::posix_time::to_iso_string(GetNow(utc)); + } + + + void SystemToolbox::GetNowDicom(std::string& date, + std::string& time, + bool utc) + { + boost::posix_time::ptime now = GetNow(utc); + 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); + } + + + unsigned int SystemToolbox::GetHardwareConcurrency() + { + // Get the number of available hardware threads (e.g. number of + // CPUs or cores or hyperthreading units) + unsigned int threads = boost::thread::hardware_concurrency(); + + if (threads == 0) + { + return 1; + } + else + { + return threads; + } + } + + bool SystemToolbox::IsContentCompressible(MimeType mime) + { + switch (mime) + { + case MimeType_Css: + case MimeType_Html: + case MimeType_JavaScript: + case MimeType_Json: + case MimeType_Pam: + case MimeType_Pdf: + case MimeType_PlainText: + case MimeType_WebAssembly: + case MimeType_Xml: + case MimeType_PrometheusText: + case MimeType_DicomWebJson: + case MimeType_DicomWebXml: + return true; + default: // for all other (JPEG, DICOM, binary, ...) + return false; + } + } + + bool SystemToolbox::IsContentCompressible(const std::string& contentType) + { + if (contentType.empty()) + { + return false; + } + + if (contentType.find(MIME_JSON) != std::string::npos || + contentType.find(MIME_XML) != std::string::npos || + contentType.find(MIME_DICOM_WEB_JSON) != std::string::npos || + contentType.find(MIME_DICOM_WEB_XML) != std::string::npos || + contentType.find(MIME_PDF) != std::string::npos || + contentType.find(MIME_CSS) != std::string::npos || + contentType.find(MIME_HTML) != std::string::npos || + contentType.find(MIME_JAVASCRIPT) != std::string::npos || + contentType.find(MIME_PLAIN_TEXT) != std::string::npos || + contentType.find(MIME_WEB_ASSEMBLY) != std::string::npos || + contentType.find(MIME_XML_2) != std::string::npos) + { + return true; + } + + return false; + } + + MimeType SystemToolbox::AutodetectMimeType(const std::string& path) + { + std::string extension = boost::filesystem::path(path).extension().string(); + Toolbox::ToLowerCase(extension); + + // http://en.wikipedia.org/wiki/Mime_types + // Text types + if (extension == ".txt") + { + return MimeType_PlainText; + } + else if (extension == ".html") + { + return MimeType_Html; + } + else if (extension == ".xml") + { + return MimeType_Xml; + } + else if (extension == ".css") + { + return MimeType_Css; + } + + // Application types + else if (extension == ".js") + { + return MimeType_JavaScript; + } + else if (extension == ".json" || + extension == ".nmf" /* manifest */) + { + return MimeType_Json; + } + else if (extension == ".pdf") + { + return MimeType_Pdf; + } + else if (extension == ".wasm") + { + return MimeType_WebAssembly; + } + else if (extension == ".nexe") + { + return MimeType_NaCl; + } + else if (extension == ".pexe") + { + return MimeType_PNaCl; + } + + // Images types + else if (extension == ".dcm") + { + return MimeType_Dicom; + } + else if (extension == ".jpg" || + extension == ".jpeg") + { + return MimeType_Jpeg; + } + else if (extension == ".gif") + { + return MimeType_Gif; + } + else if (extension == ".png") + { + return MimeType_Png; + } + else if (extension == ".pam") + { + return MimeType_Pam; + } + else if (extension == ".svg") + { + return MimeType_Svg; + } + + // Various types + else if (extension == ".woff") + { + return MimeType_Woff; + } + else if (extension == ".woff2") + { + return MimeType_Woff2; + } + else if (extension == ".ico") + { + return MimeType_Ico; + } + else if (extension == ".gz") + { + return MimeType_Gzip; + } + else if (extension == ".zip") + { + return MimeType_Zip; + } + else if (extension == ".mtl") + { + return MimeType_Mtl; + } + else if (extension == ".obj") + { + return MimeType_Obj; + } + else if (extension == ".stl") + { + return MimeType_Stl; + } + + // Default type + else + { + LOG(INFO) << "Unknown MIME type for extension \"" << extension << "\""; + return MimeType_Binary; + } + } + + + void SystemToolbox::GetEnvironmentVariables(std::map& env) + { + env.clear(); + + for (char **p = environ; *p != NULL; p++) + { + std::string v(*p); + size_t pos = v.find('='); + + if (pos != std::string::npos) + { + std::string key = v.substr(0, pos); + std::string value = v.substr(pos + 1); + env[key] = value; + } + } + } + + + std::string SystemToolbox::InterpretRelativePath(const std::string& baseDirectory, + const std::string& relativePath) + { + boost::filesystem::path base(baseDirectory); + boost::filesystem::path relative(relativePath); + + /** + The following lines should be equivalent to this one: + + return (base / relative).string(); + + However, for some unknown reason, some versions of Boost do not + make the proper path resolution when "baseDirectory" is an + absolute path. So, a hack is used below. + **/ + + if (relative.is_absolute()) + { + return relative.string(); + } + else + { + return (base / relative).string(); + } + } + + + void SystemToolbox::ReadFileRange(std::string& content, + const std::string& path, + uint64_t start, // Inclusive + uint64_t end, // Exclusive + bool throwIfOverflow) + { + if (start > end) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (!IsRegularFile(path)) + { + throw OrthancException(ErrorCode_RegularFileExpected, + "The path does not point to a regular file: " + path); + } + + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + path); + } + + uint64_t fileSize = static_cast(GetStreamSize(f)); + if (end > fileSize) + { + if (throwIfOverflow) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Reading beyond the end of a file"); + } + else + { + end = fileSize; + } + } + + if (start <= end) + { + content.resize(static_cast(end - start)); + + if (static_cast(content.size()) != (end - start)) + { + throw OrthancException(ErrorCode_InternalError, + "Reading a file that is too large for a 32bit architecture"); + } + + if (!content.empty()) + { + f.seekg(start, std::ios::beg); + f.read(&content[0], static_cast(content.size())); + } + } + else + { + content.clear(); + } + + f.close(); + } + + +#if defined(_WIN32) + void SystemToolbox::GetMacAddresses(std::set& target) + { + target.clear(); + + // 15Ko is the recommanded size to start with + std::vector buffer(15 * 1024); + + for (unsigned int iteration = 0; iteration < 3; iteration++) + { + ULONG outBufLen = static_cast(buffer.size()); + DWORD result = GetAdaptersAddresses + (AF_UNSPEC, 0, NULL, + reinterpret_cast(&buffer[0]), &outBufLen); + + if (result == NO_ERROR) + { + IP_ADAPTER_ADDRESSES* current = + reinterpret_cast(&buffer[0]); + + while (current != NULL) + { + if (current->PhysicalAddressLength == 6 && + (current->PhysicalAddress[0] != 0 || + current->PhysicalAddress[1] != 0 || + current->PhysicalAddress[2] != 0 || + current->PhysicalAddress[3] != 0 || + current->PhysicalAddress[4] != 0 || + current->PhysicalAddress[5] != 0)) + { + char tmp[32]; + sprintf(tmp, "%02x:%02x:%02x:%02x:%02x:%02x", + (unsigned char) current->PhysicalAddress[0], + (unsigned char) current->PhysicalAddress[1], + (unsigned char) current->PhysicalAddress[2], + (unsigned char) current->PhysicalAddress[3], + (unsigned char) current->PhysicalAddress[4], + (unsigned char) current->PhysicalAddress[5]); + target.insert(tmp); + } + + current = current->Next; + } + + return; + } + else if (result != ERROR_BUFFER_OVERFLOW || + iteration >= 3 || + outBufLen == 0) + { + return; + } + else + { + buffer.resize(outBufLen); + iteration++; + } + } + } + +#else + namespace + { + class SocketRaii : public boost::noncopyable + { + private: + int socket_; + + public: + SocketRaii() + { + socket_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + } + + ~SocketRaii() + { + if (socket_ != -1) + { + close(socket_); + } + } + + int GetDescriptor() const + { + return socket_; + } + }; + + + class NetworkInterfaces : public boost::noncopyable + { + private: + struct if_nameindex* list_; + struct if_nameindex* current_; + + public: + NetworkInterfaces() + { + list_ = if_nameindex(); + current_ = list_; + } + + ~NetworkInterfaces() + { + if (list_ != NULL) + { + if_freenameindex(list_); + } + } + + bool IsDone() const + { + return (current_ == NULL || + (current_->if_index == 0 && + current_->if_name == NULL)); + } + + const char* GetCurrentName() const + { + assert(!IsDone()); + return current_->if_name; + } + + unsigned int GetCurrentIndex() const + { + assert(!IsDone()); + return current_->if_index; + } + + void Next() + { + assert(!IsDone()); + current_++; + } + }; + } + + + void SystemToolbox::GetMacAddresses(std::set& target) + { + target.clear(); + + SocketRaii socket; + + if (socket.GetDescriptor() != 1) + { + NetworkInterfaces interfaces; + + while (!interfaces.IsDone()) + { +#if (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) + int mib[6]; + mib[0] = CTL_NET; + mib[1] = AF_ROUTE; + mib[2] = 0; + mib[3] = AF_LINK; + mib[4] = NET_RT_IFLIST; + mib[5] = interfaces.GetCurrentIndex(); + + size_t len; + if (sysctl(mib, 6, NULL, &len, NULL, 0) == 0 && + len > 0) + { + std::string tmp; + tmp.resize(len); + if (sysctl(mib, 6, &tmp[0], &len, NULL, 0) == 0) + { + struct if_msghdr* ifm = reinterpret_cast(&tmp[0]); + struct sockaddr_dl* sdl = reinterpret_cast(ifm + 1); + + if (sdl->sdl_type == IFT_ETHER) // Only consider Ethernet interfaces + { + const unsigned char* mac = reinterpret_cast(LLADDR(sdl)); + char tmp[32]; + sprintf(tmp, "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + target.insert(tmp); + } + } + } + +#else + struct ifreq ifr; + strcpy(ifr.ifr_name, interfaces.GetCurrentName()); + + if (ioctl(socket.GetDescriptor(), SIOCGIFFLAGS, &ifr) == 0 && + !(ifr.ifr_flags & IFF_LOOPBACK) && // ignore loopback interface + ioctl(socket.GetDescriptor(), SIOCGIFHWADDR, &ifr) == 0) + { + const unsigned char* mac = reinterpret_cast(ifr.ifr_hwaddr.sa_data); + + char tmp[32]; + sprintf(tmp, "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + target.insert(tmp); + } +#endif + + interfaces.Next(); + } + } + } + +#endif +} diff --git a/OrthancFramework/Sources/SystemToolbox.h b/OrthancFramework/Sources/SystemToolbox.h new file mode 100644 index 0000000..d1127ce --- /dev/null +++ b/OrthancFramework/Sources/SystemToolbox.h @@ -0,0 +1,147 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "OrthancFramework.h" // Must be before "ORTHANC_SANDBOXED" + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if !defined(ORTHANC_ENABLE_MD5) +# error The macro ORTHANC_ENABLE_MD5 must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The namespace SystemToolbox cannot be used in sandboxed environments +#endif + +#include "Enumerations.h" + +#include +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC SystemToolbox + { + public: + static void USleep(uint64_t microSeconds); + + static ServerBarrierEvent ServerBarrier(const bool& stopFlag); + + static ServerBarrierEvent ServerBarrier(); + + static void ReadFile(std::string& content, + const std::string& path, + bool log); + + static void ReadFile(std::string& content, + const std::string& path); + + static bool ReadHeader(std::string& header, + const std::string& path, + size_t headerSize); + + static void WriteFile(const void* content, + size_t size, + const std::string& path, + bool callFsync); + + static void WriteFile(const void* content, + size_t size, + const std::string& path); + + static void WriteFile(const std::string& content, + const std::string& path, + bool callFsync); + + static void WriteFile(const std::string& content, + const std::string& path); + + static void RemoveFile(const std::string& path); + + static uint64_t GetFileSize(const std::string& path); + +#if ORTHANC_ENABLE_MD5 == 1 + static void ComputeStreamMD5(std::string& result, + std::istream& stream); + + static void ComputeFileMD5(std::string& result, + const std::string& path); + + // returns true if file have the same MD5 + static bool CompareFilesMD5(const std::string& path1, + const std::string& path2); +#endif + + static void MakeDirectory(const std::string& path); + + static bool IsExistingFile(const std::string& path); + + static std::string GetPathToExecutable(); + + static std::string GetDirectoryOfExecutable(); + + static void ExecuteSystemCommand(const std::string& command, + const std::vector& arguments); + + static int GetProcessId(); + + static bool IsRegularFile(const std::string& path); + + static FILE* OpenFile(const std::string& path, + FileMode mode); + + static std::string GetNowIsoString(bool utc); + + static void GetNowDicom(std::string& date, + std::string& time, + bool utc); + + static unsigned int GetHardwareConcurrency(); + + static bool IsContentCompressible(MimeType mime); + + static bool IsContentCompressible(const std::string& contentType); + + static MimeType AutodetectMimeType(const std::string& path); + + static void GetEnvironmentVariables(std::map& env); + + static std::string InterpretRelativePath(const std::string& baseDirectory, + const std::string& relativePath); + + static void ReadFileRange(std::string& content, + const std::string& path, + uint64_t start, // Inclusive + uint64_t end, // Exclusive + bool throwIfOverflow); + + static void GetMacAddresses(std::set& target); + }; +} diff --git a/OrthancFramework/Sources/TemporaryFile.cpp b/OrthancFramework/Sources/TemporaryFile.cpp new file mode 100644 index 0000000..6dfa61c --- /dev/null +++ b/OrthancFramework/Sources/TemporaryFile.cpp @@ -0,0 +1,152 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "TemporaryFile.h" + +#include "OrthancException.h" +#include "SystemToolbox.h" +#include "Toolbox.h" + +#include +#include + +namespace Orthanc +{ + static std::string CreateTemporaryPath(const char* temporaryDirectory, + const char* extension) + { + boost::filesystem::path dir; + + if (temporaryDirectory == NULL) + { +#if BOOST_HAS_FILESYSTEM_V3 == 1 + dir = boost::filesystem::temp_directory_path(); +#elif defined(__linux__) + dir = "/tmp"; +#else +# error Support your platform here +#endif + } + else + { + dir = temporaryDirectory; + } + + // We use UUID to create unique path to temporary files + const std::string uuid = Toolbox::GenerateUuid(); + + // New in Orthanc 1.5.8: Prefix the process ID to the name of the + // temporary files, in order to locate orphan temporary files that + // were left by instances of Orthanc that exited in non-clean way + // https://groups.google.com/d/msg/orthanc-users/MSJX53bw6Lw/d3S3lRRLAwAJ + std::string filename = "Orthanc-" + boost::lexical_cast(SystemToolbox::GetProcessId()) + "-" + uuid; + + if (extension != NULL) + { + filename.append(extension); + } + + dir /= filename; + return dir.string(); + } + + + TemporaryFile::TemporaryFile() : + path_(CreateTemporaryPath(NULL, NULL)) + { + } + + + TemporaryFile::TemporaryFile(const std::string& temporaryDirectory, + const std::string& extension) : + path_(CreateTemporaryPath(temporaryDirectory.c_str(), extension.c_str())) + { + } + + + TemporaryFile::~TemporaryFile() + { + boost::filesystem::remove(path_); + } + + const std::string &TemporaryFile::GetPath() const + { + return path_; + } + + + void TemporaryFile::Write(const std::string& content) + { + try + { + SystemToolbox::WriteFile(content, path_); + } + catch (OrthancException& e) + { + throw OrthancException(e.GetErrorCode(), + "Can't create temporary file \"" + path_ + + "\" with " + boost::lexical_cast(content.size()) + + " bytes: Check you have write access to the " + "temporary directory and that it is not full"); + } + } + + + void TemporaryFile::Read(std::string& content) const + { + try + { + SystemToolbox::ReadFile(content, path_); + } + catch (OrthancException& e) + { + throw OrthancException(e.GetErrorCode(), + "Can't read temporary file \"" + path_ + + "\": Another process has corrupted the temporary directory"); + } + } + + + void TemporaryFile::Touch() + { + std::string empty; + Write(empty); + } + + + uint64_t TemporaryFile::GetFileSize() const + { + return SystemToolbox::GetFileSize(path_); + } + + + void TemporaryFile::ReadRange(std::string& content, + uint64_t start, + uint64_t end, + bool throwIfOverflow) const + { + SystemToolbox::ReadFileRange(content, path_, start, end, throwIfOverflow); + } +} diff --git a/OrthancFramework/Sources/TemporaryFile.h b/OrthancFramework/Sources/TemporaryFile.h new file mode 100644 index 0000000..e252fe5 --- /dev/null +++ b/OrthancFramework/Sources/TemporaryFile.h @@ -0,0 +1,71 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "OrthancFramework.h" + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class TemporaryFile cannot be used in sandboxed environments +#endif + +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC TemporaryFile : public boost::noncopyable + { + private: + std::string path_; + + public: + TemporaryFile(); + + TemporaryFile(const std::string& temporaryFolder, + const std::string& extension); + + ~TemporaryFile(); + + const std::string& GetPath() const; + + void Write(const std::string& content); + + void Read(std::string& content) const; + + void Touch(); + + uint64_t GetFileSize() const; + + void ReadRange(std::string& content, + uint64_t start, + uint64_t end, + bool throwIfOverflow) const; + }; +} diff --git a/OrthancFramework/Sources/Toolbox.cpp b/OrthancFramework/Sources/Toolbox.cpp new file mode 100644 index 0000000..64b1f5c --- /dev/null +++ b/OrthancFramework/Sources/Toolbox.cpp @@ -0,0 +1,2962 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "Toolbox.h" + +#include "Compatibility.h" +#include "OrthancException.h" +#include "Logging.h" + +#include +#include +#include + +#if !defined(JSONCPP_VERSION_MAJOR) || !defined(JSONCPP_VERSION_MINOR) +# error Cannot access the version of JsonCpp +#endif + +#if !defined(ORTHANC_ENABLE_ICU) +# define ORTHANC_ENABLE_ICU 1 +#endif + + +/** + * We use deprecated "Json::Reader", "Json::StyledWriter" and + * "Json::FastWriter" if JsonCpp < 1.7.0. This choice is rather + * arbitrary, but if Json >= 1.9.0, gcc generates explicit deprecation + * warnings (clang was warning in earlier versions). For reference, + * these classes seem to have been deprecated since JsonCpp 1.4.0 (on + * February 2015) by the following changeset: + * https://github.com/open-source-parsers/jsoncpp/commit/8df98f6112890d6272734975dd6d70cf8999bb22 + **/ +#if (JSONCPP_VERSION_MAJOR >= 2 || \ + (JSONCPP_VERSION_MAJOR == 1 && JSONCPP_VERSION_MINOR >= 8)) +# define JSONCPP_USE_DEPRECATED 0 +#else +# define JSONCPP_USE_DEPRECATED 1 +#endif + + +#include +#include +#include +#include +#include +#include + +#if BOOST_VERSION >= 106600 +# include +#else +# include +#endif + +#include +#include +#include +#include +#include + + +#if ORTHANC_ENABLE_MD5 == 1 +// TODO - Could be replaced by starting +// with Boost >= 1.66.0 +# include "../Resources/ThirdParty/md5/md5.h" +#endif + +#if ORTHANC_ENABLE_BASE64 == 1 +# include "../Resources/ThirdParty/base64/base64.h" +#endif + +#if ORTHANC_ENABLE_LOCALE == 1 +# include +#endif + +#if ORTHANC_ENABLE_SSL == 1 +// For OpenSSL initialization and finalization +# include +# include +# include +# include +# include + +# if OPENSSL_VERSION_NUMBER < 0x30000000L +# if defined(_MSC_VER) +# pragma message("You are linking Orthanc against OpenSSL 1.x, whose license is incompatible with the GPLv3+ used by Orthanc >= 1.10.0. Please update to OpenSSL 3.x, that uses the Apache 2 license.") +# else +# warning You are linking Orthanc against OpenSSL 1.x, whose license is incompatible with the GPLv3+ used by Orthanc >= 1.10.0. Please update to OpenSSL 3.x, that uses the Apache 2 license. +# endif +# endif + +#endif + + +#if defined(_MSC_VER) && (_MSC_VER < 1800) +// Patch for the missing "_strtoll" symbol when compiling with Visual Studio < 2013 +extern "C" +{ + int64_t _strtoi64(const char *nptr, char **endptr, int base); + int64_t strtoll(const char *nptr, char **endptr, int base) + { + return _strtoi64(nptr, endptr, base); + } +} +#endif + + +#if defined(_WIN32) +# include // For ::Sleep +#endif + + +#if ORTHANC_ENABLE_PUGIXML == 1 +# include "ChunkedBuffer.h" +#endif + + +// Inclusions for UUID +// http://stackoverflow.com/a/1626302 + +extern "C" +{ +#if defined(_WIN32) +# include +#else +# include +#endif +} + + +#if defined(ORTHANC_STATIC_ICU) + +# if (ORTHANC_STATIC_ICU == 1) && (ORTHANC_ENABLE_ICU == 1) +# if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1) +# include +# endif +# endif + +# if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1) +# include +# include +# include "Compression/GzipCompressor.h" + +static std::string globalIcuData_; + +extern "C" +{ + // This is dummy content for the "icudt58_dat" (resp. "icudt63_dat") + // global variable from the autogenerated "icudt58l_dat.c" + // (resp. "icudt63l_dat.c") file that contains a huge C array. In + // Orthanc, this array is compressed using gzip and attached as a + // resource, then uncompressed during the launch of Orthanc by + // static function "InitializeIcu()". + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# if defined(__LSB_VERSION__) +extern "C" +{ + /** + * The "tzname" global variable is declared as "extern" but is not + * defined in any compilation module, if using Linux Standard Base, + * as soon as OpenSSL or cURL is in use on Ubuntu >= 18.04 (glibc >= + * 2.27). The variable "__tzname" is always properly declared *and* + * defined. The reason is unclear, and is maybe a bug in the gcc 4.8 + * linker that is used by LSB if facing a weak symbol (as "tzname"). + * This makes Orthanc crash if the timezone is set to UTC. + * https://groups.google.com/d/msg/orthanc-users/0m8sxxwSm1E/2p8du_89CAAJ + **/ + char *tzname[2] = { (char *) "GMT", (char *) "GMT" }; +} +# endif + +# endif +#endif + + + +#if defined(__unix__) && ORTHANC_SANDBOXED != 1 +# include "SystemToolbox.h" // Check out "InitializeGlobalLocale()" +#endif + + + +namespace Orthanc +{ +#if ORTHANC_ENABLE_MD5 == 1 + static char GetHexadecimalCharacter(uint8_t value) + { + assert(value < 16); + + if (value < 10) + { + return value + '0'; + } + else + { + return (value - 10) + 'a'; + } + } + + + struct Toolbox::MD5Context::PImpl + { + md5_state_s state_; + bool done_; + + PImpl() : + done_(false) + { + md5_init(&state_); + } + }; + + + Toolbox::MD5Context::MD5Context() : + pimpl_(new PImpl) + { + } + + + void Toolbox::MD5Context::Append(const void* data, + size_t size) + { + static const size_t MAX_SIZE = 128 * 1024 * 1024; + + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const uint8_t *p = reinterpret_cast(data); + + while (size > 0) + { + /** + * The built-in implementation of MD5 requires that "size" can + * be casted to "int", so we feed it by chunks of maximum + * 128MB. This fixes an incorrect behavior in Orthanc <= 1.12.7. + **/ + + int chunkSize; + if (size > MAX_SIZE) + { + chunkSize = static_cast(MAX_SIZE); + } + else + { + chunkSize = static_cast(size); + } + + md5_append(&pimpl_->state_, reinterpret_cast(p), chunkSize); + + p += chunkSize; + + assert(static_cast(chunkSize) <= size); + size -= chunkSize; + } + } + + + void Toolbox::MD5Context::Append(const std::string& source) + { + if (source.size() > 0) + { + Append(source.c_str(), source.size()); + } + } + + + void Toolbox::MD5Context::Export(std::string& target) + { + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + pimpl_->done_ = true; + + md5_byte_t actualHash[16]; + md5_finish(&pimpl_->state_, actualHash); + + target.resize(32); + for (unsigned int i = 0; i < 16; i++) + { + target[2 * i] = GetHexadecimalCharacter(static_cast(actualHash[i] / 16)); + target[2 * i + 1] = GetHexadecimalCharacter(static_cast(actualHash[i] % 16)); + } + } +#endif /* ORTHANC_ENABLE_MD5 */ + + + void Toolbox::LinesIterator::FindEndOfLine() + { + lineEnd_ = lineStart_; + + while (lineEnd_ < content_.size() && + content_[lineEnd_] != '\n' && + content_[lineEnd_] != '\r') + { + lineEnd_ += 1; + } + } + + + Toolbox::LinesIterator::LinesIterator(const std::string& content) : + content_(content), + lineStart_(0) + { + FindEndOfLine(); + } + + + bool Toolbox::LinesIterator::GetLine(std::string& target) const + { + assert(lineStart_ <= content_.size() && + lineEnd_ <= content_.size() && + lineStart_ <= lineEnd_); + + if (lineStart_ == content_.size()) + { + return false; + } + else + { + target = content_.substr(lineStart_, lineEnd_ - lineStart_); + return true; + } + } + + + void Toolbox::LinesIterator::Next() + { + lineStart_ = lineEnd_; + + if (lineStart_ != content_.size()) + { + assert(content_[lineStart_] == '\r' || + content_[lineStart_] == '\n'); + + char second; + + if (content_[lineStart_] == '\r') + { + second = '\n'; + } + else + { + second = '\r'; + } + + lineStart_ += 1; + + if (lineStart_ < content_.size() && + content_[lineStart_] == second) + { + lineStart_ += 1; + } + + FindEndOfLine(); + } + } + + + void Toolbox::ToUpperCase(std::string& s) + { + std::transform(s.begin(), s.end(), s.begin(), toupper); + } + + + void Toolbox::ToLowerCase(std::string& s) + { + std::transform(s.begin(), s.end(), s.begin(), tolower); + } + + + void Toolbox::ToUpperCase(std::string& result, + const std::string& source) + { + result = source; + ToUpperCase(result); + } + + void Toolbox::ToLowerCase(std::string& result, + const std::string& source) + { + result = source; + ToLowerCase(result); + } + + + void Toolbox::SplitUriComponents(UriComponents& components, + const std::string& uri) + { + static const char URI_SEPARATOR = '/'; + + components.clear(); + + if (uri.size() == 0 || + uri[0] != URI_SEPARATOR) + { + throw OrthancException(ErrorCode_UriSyntax); + } + + // Count the number of slashes in the URI to make an assumption + // about the number of components in the URI + unsigned int estimatedSize = 0; + for (unsigned int i = 0; i < uri.size(); i++) + { + if (uri[i] == URI_SEPARATOR) + estimatedSize++; + } + + components.reserve(estimatedSize - 1); + + unsigned int start = 1; + unsigned int end = 1; + while (end < uri.size()) + { + // This is the loop invariant + assert(uri[start - 1] == '/' && (end >= start)); + + if (uri[end] == '/') + { + components.push_back(std::string(&uri[start], end - start)); + end++; + start = end; + } + else + { + end++; + } + } + + if (start < uri.size()) + { + components.push_back(std::string(&uri[start], end - start)); + } + + for (size_t i = 0; i < components.size(); i++) + { + if (components[i].size() == 0) + { + // Empty component, as in: "/coucou//e" + throw OrthancException(ErrorCode_UriSyntax); + } + } + } + + + void Toolbox::TruncateUri(UriComponents& target, + const UriComponents& source, + size_t fromLevel) + { + target.clear(); + + if (source.size() > fromLevel) + { + target.resize(source.size() - fromLevel); + + size_t j = 0; + for (size_t i = fromLevel; i < source.size(); i++, j++) + { + target[j] = source[i]; + } + + assert(j == target.size()); + } + } + + + + bool Toolbox::IsChildUri(const UriComponents& baseUri, + const UriComponents& testedUri) + { + if (testedUri.size() < baseUri.size()) + { + return false; + } + + for (size_t i = 0; i < baseUri.size(); i++) + { + if (baseUri[i] != testedUri[i]) + return false; + } + + return true; + } + + + std::string Toolbox::FlattenUri(const UriComponents& components, + size_t fromLevel) + { + if (components.size() <= fromLevel) + { + return "/"; + } + else + { + std::string r; + + for (size_t i = fromLevel; i < components.size(); i++) + { + r += "/" + components[i]; + } + + return r; + } + } + + std::string Toolbox::JoinUri(const std::string& base, const std::string& uri) + { + if (uri.size() > 0 && base.size() > 0) + { + if (base[base.size() - 1] == '/' && uri[0] == '/') + { + return base + uri.substr(1, uri.size() - 1); + } + else if (base[base.size() - 1] != '/' && uri[0] != '/') + { + return base + "/" + uri; + } + } + + return base + uri; + } + + +#if ORTHANC_ENABLE_MD5 == 1 + void Toolbox::ComputeMD5(std::string& result, + const std::string& data) + { + if (data.size() > 0) + { + ComputeMD5(result, &data[0], data.size()); + } + else + { + ComputeMD5(result, NULL, 0); + } + } + + + void Toolbox::ComputeMD5(std::string& result, + const void* data, + size_t size) + { + MD5Context context; + context.Append(data, size); + context.Export(result); + } + + void Toolbox::ComputeMD5(std::string& result, + const std::set& data) + { + std::string s; + + for (std::set::const_iterator it = data.begin(); it != data.end(); ++it) + { + s += *it; + } + + ComputeMD5(result, s); + } + +#endif + + +#if ORTHANC_ENABLE_BASE64 == 1 + void Toolbox::EncodeBase64(std::string& result, + const std::string& data) + { + result.clear(); + base64_encode(result, data); + } + + void Toolbox::DecodeBase64(std::string& result, + const std::string& data) + { + for (size_t i = 0; i < data.length(); i++) + { + if (!isalnum(data[i]) && + data[i] != '+' && + data[i] != '/' && + data[i] != '=') + { + // This is not a valid character for a Base64 string + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + result.clear(); + base64_decode(result, data); + } + + + bool Toolbox::DecodeDataUriScheme(std::string& mime, + std::string& content, + const std::string& source) + { + boost::regex pattern("data:([^;]+);base64,([a-zA-Z0-9=+/]*)", + boost::regex::icase /* case insensitive search */); + + boost::cmatch what; + if (regex_match(source.c_str(), what, pattern)) + { + mime = what[1]; + DecodeBase64(content, what[2]); + return true; + } + else + { + return false; + } + } + + + void Toolbox::EncodeDataUriScheme(std::string& result, + const std::string& mime, + const std::string& content) + { + result = "data:" + mime + ";base64,"; + base64_encode(result, content); + } + +#endif + + +#if ORTHANC_ENABLE_LOCALE == 1 + static const char* GetBoostLocaleEncoding(const Encoding sourceEncoding) + { + switch (sourceEncoding) + { + case Encoding_Utf8: + return "UTF-8"; + + case Encoding_Ascii: + return "ASCII"; + + case Encoding_Latin1: + return "ISO-8859-1"; + + case Encoding_Latin2: + return "ISO-8859-2"; + + case Encoding_Latin3: + return "ISO-8859-3"; + + case Encoding_Latin4: + return "ISO-8859-4"; + + case Encoding_Latin5: + return "ISO-8859-9"; + + case Encoding_Cyrillic: + return "ISO-8859-5"; + + case Encoding_Windows1251: + return "WINDOWS-1251"; + + case Encoding_Arabic: + return "ISO-8859-6"; + + case Encoding_Greek: + return "ISO-8859-7"; + + case Encoding_Hebrew: + return "ISO-8859-8"; + + case Encoding_Japanese: + return "SHIFT-JIS"; + + case Encoding_Chinese: + return "GB18030"; + + case Encoding_Thai: +#if BOOST_LOCALE_WITH_ICU == 1 + return "tis620.2533"; +#else + return "TIS620.2533-0"; +#endif + + case Encoding_Korean: + return "ISO-IR-149"; + + case Encoding_JapaneseKanji: + return "JIS"; + + case Encoding_SimplifiedChinese: + return "GB2312"; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } +#endif + + +#if ORTHANC_ENABLE_LOCALE == 1 + // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2 + std::string Toolbox::ConvertToUtf8(const std::string& source, + Encoding sourceEncoding, + bool hasCodeExtensions) + { +#if ORTHANC_STATIC_ICU == 1 +# if ORTHANC_ENABLE_ICU == 0 + throw OrthancException(ErrorCode_NotImplemented, "ICU is disabled for this target"); +# else + if (globalIcuData_.empty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Call Toolbox::InitializeGlobalLocale()"); + } +# endif +#endif + + // The "::skip" flag makes boost skip invalid UTF-8 + // characters. This can occur in badly-encoded DICOM files. + + try + { + if (sourceEncoding == Encoding_Ascii) + { + return ConvertToAscii(source); + } + else + { + std::string s; + + if (sourceEncoding == Encoding_Utf8) + { + // Already in UTF-8: No conversion is required, but we ensure + // the output is correctly encoded + s = boost::locale::conv::utf_to_utf(source, boost::locale::conv::skip); + } + else + { + const char* encoding = GetBoostLocaleEncoding(sourceEncoding); + s = boost::locale::conv::to_utf(source, encoding, boost::locale::conv::skip); + } + + if (hasCodeExtensions) + { + std::string t; + RemoveIso2022EscapeSequences(t, s); + return t; + } + else + { + return s; + } + } + } + catch (std::runtime_error& e) + { + // Bad input string or bad encoding + LOG(INFO) << e.what(); + return ConvertToAscii(source); + } + } +#endif + + +#if ORTHANC_ENABLE_LOCALE == 1 + std::string Toolbox::ConvertFromUtf8(const std::string& source, + Encoding targetEncoding) + { +#if ORTHANC_STATIC_ICU == 1 +# if ORTHANC_ENABLE_ICU == 0 + throw OrthancException(ErrorCode_NotImplemented, "ICU is disabled for this target"); +# else + if (globalIcuData_.empty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Call Toolbox::InitializeGlobalLocale()"); + } +# endif +#endif + + // The "::skip" flag makes boost skip invalid UTF-8 + // characters. This can occur in badly-encoded DICOM files. + + try + { + if (targetEncoding == Encoding_Utf8) + { + // Already in UTF-8: No conversion is required. + return boost::locale::conv::utf_to_utf(source, boost::locale::conv::skip); + } + else if (targetEncoding == Encoding_Ascii) + { + return ConvertToAscii(source); + } + else + { + const char* encoding = GetBoostLocaleEncoding(targetEncoding); + return boost::locale::conv::from_utf(source, encoding, boost::locale::conv::skip); + } + } + catch (std::runtime_error&) + { + // Bad input string or bad encoding + return ConvertToAscii(source); + } + } +#endif + + + static bool IsAsciiCharacter(uint8_t c) + { + return (c != 0 && + c <= 127 && + (c == '\n' || !iscntrl(c))); + } + + + bool Toolbox::IsAsciiString(const void* data, + size_t size) + { + const uint8_t* p = reinterpret_cast(data); + + for (size_t i = 0; i < size; i++, p++) + { + if (!IsAsciiCharacter(*p)) + { + return false; + } + } + + return true; + } + + + bool Toolbox::IsAsciiString(const std::string& s) + { + return IsAsciiString(s.c_str(), s.size()); + } + + + std::string Toolbox::ConvertToAscii(const std::string& source) + { + std::string result; + + result.reserve(source.size() + 1); + for (size_t i = 0; i < source.size(); i++) + { + if (IsAsciiCharacter(source[i])) + { + result.push_back(source[i]); + } + } + + return result; + } + + void Toolbox::ComputeSHA1(std::string& result, + const void* data, + size_t size) + { + boost::uuids::detail::sha1 sha1; + + if (size > 0) + { + sha1.process_bytes(data, size); + } + +#if BOOST_VERSION >= 108600 + unsigned char digest[20]; + + // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide + assert(sizeof(digest) == (160 / 8)); + assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20); + + // From Boost 1.86, digest_type is "unsigned char[20]" while it was "unsigned int[5]"" in previous versions. + // Always perform the cast even if it is useless for Boost < 1.86 + sha1.get_digest(digest); + + result.resize(8 * 5 + 4); + sprintf(&result[0], "%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], + digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], + digest[12], digest[13], digest[14], digest[15], + digest[16], digest[17], digest[18], digest[19]); + +#else + unsigned int digest[5]; + // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide + assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8)); + assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20); + + sha1.get_digest(digest); + + result.resize(8 * 5 + 4); + sprintf(&result[0], "%08x-%08x-%08x-%08x-%08x", + digest[0], + digest[1], + digest[2], + digest[3], + digest[4]); + +#endif + + } + + void Toolbox::ComputeSHA1(std::string& result, + const std::string& data) + { + if (data.size() > 0) + { + ComputeSHA1(result, data.c_str(), data.size()); + } + else + { + ComputeSHA1(result, NULL, 0); + } + } + + + bool Toolbox::IsSHA1(const void* str, + size_t size) + { + if (size == 0) + { + return false; + } + + const char* start = reinterpret_cast(str); + const char* end = start + size; + + // Trim the beginning of the string + while (start < end) + { + if (*start == '\0' || + isspace(*start)) + { + start++; + } + else + { + break; + } + } + + // Trim the trailing of the string + while (start < end) + { + if (*(end - 1) == '\0' || + isspace(*(end - 1))) + { + end--; + } + else + { + break; + } + } + + if (end - start != 44) + { + return false; + } + + for (unsigned int i = 0; i < 44; i++) + { + if (i == 8 || + i == 17 || + i == 26 || + i == 35) + { + if (start[i] != '-') + return false; + } + else + { + if (!isalnum(start[i])) + return false; + } + } + + return true; + } + + + bool Toolbox::IsSHA1(const std::string& s) + { + if (s.size() == 0) + { + return false; + } + else + { + return IsSHA1(s.c_str(), s.size()); + } + } + + + std::string Toolbox::StripSpaces(const std::string& source) + { + size_t first = 0; + + while (first < source.length() && + isspace(source[first])) + { + first++; + } + + if (first == source.length()) + { + // String containing only spaces + return ""; + } + + size_t last = source.length(); + while (last > first && + isspace(source[last - 1])) + { + last--; + } + + assert(first <= last); + return source.substr(first, last - first); + } + + + static char Hex2Dec(char c) + { + return ((c >= '0' && c <= '9') ? c - '0' : + ((c >= 'a' && c <= 'f') ? c - 'a' + 10 : c - 'A' + 10)); + } + + void Toolbox::UrlDecode(std::string& s) + { + // http://en.wikipedia.org/wiki/Percent-encoding + // http://www.w3schools.com/tags/ref_urlencode.asp + // http://stackoverflow.com/questions/154536/encode-decode-urls-in-c + + if (s.size() == 0) + { + return; + } + + size_t source = 0; + size_t target = 0; + + while (source < s.size()) + { + if (s[source] == '%' && + source + 2 < s.size() && + isalnum(s[source + 1]) && + isalnum(s[source + 2])) + { + s[target] = (Hex2Dec(s[source + 1]) << 4) | Hex2Dec(s[source + 2]); + source += 3; + target += 1; + } + else + { + if (s[source] == '+') + s[target] = ' '; + else + s[target] = s[source]; + + source++; + target++; + } + } + + s.resize(target); + } + + + Endianness Toolbox::DetectEndianness() + { + // http://sourceforge.net/p/predef/wiki/Endianness/ + + uint32_t bufferView = 0; + + uint8_t* buffer = reinterpret_cast(&bufferView); + + buffer[0] = 0x00; + buffer[1] = 0x01; + buffer[2] = 0x02; + buffer[3] = 0x03; + + switch (bufferView) + { + case 0x00010203: + return Endianness_Big; + + case 0x03020100: + return Endianness_Little; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + std::string Toolbox::WildcardToRegularExpression(const std::string& source) + { + // TODO - Speed up this with a regular expression + + std::string result = source; + + // Escape all special characters + boost::replace_all(result, "\\", "\\\\"); + boost::replace_all(result, "^", "\\^"); + boost::replace_all(result, ".", "\\."); + boost::replace_all(result, "$", "\\$"); + boost::replace_all(result, "|", "\\|"); + boost::replace_all(result, "(", "\\("); + boost::replace_all(result, ")", "\\)"); + boost::replace_all(result, "[", "\\["); + boost::replace_all(result, "]", "\\]"); + boost::replace_all(result, "+", "\\+"); + boost::replace_all(result, "/", "\\/"); + boost::replace_all(result, "{", "\\{"); + boost::replace_all(result, "}", "\\}"); + + // Convert wildcards '*' and '?' to their regex equivalents + boost::replace_all(result, "?", "."); + boost::replace_all(result, "*", ".*"); + + return result; + } + + static void TokenizeStringInternal(std::vector& result, + const std::string& value, + char separator, + bool includeEmptyStrings) + { + size_t countSeparators = 0; + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == separator) + { + countSeparators++; + } + } + + result.clear(); + result.reserve(countSeparators + 1); + + std::string currentItem; + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == separator) + { + result.push_back(currentItem); + currentItem.clear(); + } + else + { + currentItem.push_back(value[i]); + } + } + + if (includeEmptyStrings || !currentItem.empty()) + { + result.push_back(currentItem); + } + } + + + void Toolbox::TokenizeString(std::vector& result, + const std::string& value, + char separator) + { + TokenizeStringInternal(result, value, separator, true); + } + + + void Toolbox::SplitString(std::set& result, + const std::string& value, + char separator) + { + result.clear(); + + std::vector temp; + TokenizeStringInternal(temp, value, separator, false); + for (size_t i = 0; i < temp.size(); ++i) + { + result.insert(temp[i]); + } + } + + + void Toolbox::SplitString(std::vector& result, + const std::string& value, + char separator) + { + TokenizeStringInternal(result, value, separator, false); + } + + + void Toolbox::JoinStrings(std::string& result, + const std::set& source, + const char* separator) + { + result = boost::algorithm::join(source, separator); + } + + void Toolbox::JoinStrings(std::string& result, + const std::vector& source, + const char* separator) + { + result = boost::algorithm::join(source, separator); + } + + +#if ORTHANC_ENABLE_PUGIXML == 1 + class ChunkedBufferWriter : public pugi::xml_writer + { + private: + ChunkedBuffer buffer_; + + public: + virtual void write(const void *data, size_t size) + { + if (size > 0) + { + buffer_.AddChunk(reinterpret_cast(data), size); + } + } + + void Flatten(std::string& s) + { + buffer_.Flatten(s); + } + }; + + + static void JsonToXmlInternal(pugi::xml_node& target, + const Json::Value& source, + const std::string& arrayElement) + { + // http://jsoncpp.sourceforge.net/value_8h_source.html#l00030 + + switch (source.type()) + { + case Json::nullValue: + { + target.append_child(pugi::node_pcdata).set_value("null"); + break; + } + + case Json::intValue: + { + std::string s = boost::lexical_cast(source.asInt()); + target.append_child(pugi::node_pcdata).set_value(s.c_str()); + break; + } + + case Json::uintValue: + { + std::string s = boost::lexical_cast(source.asUInt()); + target.append_child(pugi::node_pcdata).set_value(s.c_str()); + break; + } + + case Json::realValue: + { + std::string s = boost::lexical_cast(source.asFloat()); + target.append_child(pugi::node_pcdata).set_value(s.c_str()); + break; + } + + case Json::stringValue: + { + target.append_child(pugi::node_pcdata).set_value(source.asString().c_str()); + break; + } + + case Json::booleanValue: + { + target.append_child(pugi::node_pcdata).set_value(source.asBool() ? "true" : "false"); + break; + } + + case Json::arrayValue: + { + for (Json::Value::ArrayIndex i = 0; i < source.size(); i++) + { + pugi::xml_node node = target.append_child(); + node.set_name(arrayElement.c_str()); + JsonToXmlInternal(node, source[i], arrayElement); + } + break; + } + + case Json::objectValue: + { + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + pugi::xml_node node = target.append_child(); + node.set_name(members[i].c_str()); + JsonToXmlInternal(node, source[members[i]], arrayElement); + } + + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void Toolbox::JsonToXml(std::string& target, + const Json::Value& source, + const std::string& rootElement, + const std::string& arrayElement) + { + pugi::xml_document doc; + + pugi::xml_node n = doc.append_child(rootElement.c_str()); + JsonToXmlInternal(n, source, arrayElement); + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("utf-8"); + + XmlToString(target, doc); + } + + void Toolbox::XmlToString(std::string& target, + const pugi::xml_document& source) + { + ChunkedBufferWriter writer; + source.save(writer, " ", pugi::format_default, pugi::encoding_utf8); + writer.Flatten(target); + } +#endif + + + + bool Toolbox::IsInteger(const std::string& str) + { + std::string s = StripSpaces(str); + + if (s.size() == 0) + { + return false; + } + + size_t pos = 0; + if (s[0] == '-') + { + if (s.size() == 1) + { + return false; + } + + pos = 1; + } + + while (pos < s.size()) + { + if (!isdigit(s[pos])) + { + return false; + } + + pos++; + } + + return true; + } + + + void Toolbox::CopyJsonWithoutComments(Json::Value& target, + const Json::Value& source) + { + switch (source.type()) + { + case Json::nullValue: + target = Json::nullValue; + break; + + case Json::intValue: + target = source.asInt64(); + break; + + case Json::uintValue: + target = source.asUInt64(); + break; + + case Json::realValue: + target = source.asDouble(); + break; + + case Json::stringValue: + target = source.asString(); + break; + + case Json::booleanValue: + target = source.asBool(); + break; + + case Json::arrayValue: + { + target = Json::arrayValue; + for (Json::Value::ArrayIndex i = 0; i < source.size(); i++) + { + Json::Value& item = target.append(Json::nullValue); + CopyJsonWithoutComments(item, source[i]); + } + + break; + } + + case Json::objectValue: + { + target = Json::objectValue; + Json::Value::Members members = source.getMemberNames(); + for (Json::Value::ArrayIndex i = 0; i < members.size(); i++) + { + const std::string item = members[i]; + CopyJsonWithoutComments(target[item], source[item]); + } + + break; + } + + default: + break; + } + } + + + bool Toolbox::StartsWith(const std::string& str, + const std::string& prefix) + { + if (str.size() < prefix.size()) + { + return false; + } + else + { + return str.compare(0, prefix.size(), prefix) == 0; + } + } + + + static bool IsUnreservedCharacter(char c) + { + // This function checks whether "c" is an unserved character + // wrt. an URI percent-encoding + // https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding%5Fin%5Fa%5FURI + + return ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '_' || + c == '.' || + c == '~' || + c == '/'); + } + + void Toolbox::UriEncode(std::string& target, + const std::string& source) + { + // Estimate the length of the percent-encoded URI + size_t length = 0; + + for (size_t i = 0; i < source.size(); i++) + { + if (IsUnreservedCharacter(source[i])) + { + length += 1; + } + else + { + // This character must be percent-encoded + length += 3; + } + } + + target.clear(); + target.reserve(length); + + for (size_t i = 0; i < source.size(); i++) + { + if (IsUnreservedCharacter(source[i])) + { + target.push_back(source[i]); + } + else + { + // This character must be percent-encoded + uint8_t byte = static_cast(source[i]); + uint8_t a = byte >> 4; + uint8_t b = byte & 0x0f; + + target.push_back('%'); + target.push_back(a < 10 ? a + '0' : a - 10 + 'A'); + target.push_back(b < 10 ? b + '0' : b - 10 + 'A'); + } + } + } + + + static bool HasField(const Json::Value& json, + const std::string& key, + Json::ValueType expectedType) + { + if (json.type() != Json::objectValue || + !json.isMember(key)) + { + return false; + } + else if (json[key].type() == expectedType) + { + return true; + } + else + { + throw OrthancException(ErrorCode_BadParameterType); + } + } + + + std::string Toolbox::GetJsonStringField(const Json::Value& json, + const std::string& key, + const std::string& defaultValue) + { + if (HasField(json, key, Json::stringValue)) + { + return json[key].asString(); + } + else + { + return defaultValue; + } + } + + + bool Toolbox::GetJsonBooleanField(const ::Json::Value& json, + const std::string& key, + bool defaultValue) + { + if (HasField(json, key, Json::booleanValue)) + { + return json[key].asBool(); + } + else + { + return defaultValue; + } + } + + + int Toolbox::GetJsonIntegerField(const ::Json::Value& json, + const std::string& key, + int defaultValue) + { + if (HasField(json, key, Json::intValue)) + { + return json[key].asInt(); + } + else + { + return defaultValue; + } + } + + + unsigned int Toolbox::GetJsonUnsignedIntegerField(const ::Json::Value& json, + const std::string& key, + unsigned int defaultValue) + { + int v = GetJsonIntegerField(json, key, defaultValue); + + if (v < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return static_cast(v); + } + } + + + bool Toolbox::IsUuid(const std::string& str) + { + if (str.size() != 36) + { + return false; + } + + for (size_t i = 0; i < str.length(); i++) + { + if (i == 8 || i == 13 || i == 18 || i == 23) + { + if (str[i] != '-') + return false; + } + else + { + if (!isalnum(str[i])) + return false; + } + } + + return true; + } + + + bool Toolbox::StartsWithUuid(const std::string& str) + { + if (str.size() < 36) + { + return false; + } + + if (str.size() == 36) + { + return IsUuid(str); + } + + assert(str.size() > 36); + if (!isspace(str[36])) + { + return false; + } + + return IsUuid(str.substr(0, 36)); + } + + +#if ORTHANC_ENABLE_LOCALE == 1 + static std::unique_ptr globalLocale_; + + static bool SetGlobalLocale(const char* locale) + { + try + { + if (locale == NULL) + { + LOG(WARNING) << "Falling back to system-wide default locale"; + globalLocale_.reset(new std::locale()); + } + else + { + LOG(INFO) << "Using locale: \"" << locale << "\" for case-insensitive comparison of strings"; + globalLocale_.reset(new std::locale(locale)); + } + } + catch (std::runtime_error& e) + { + LOG(ERROR) << "Cannot set globale locale to " + << (locale ? std::string(locale) : "(null)") + << ": " << e.what(); + globalLocale_.reset(NULL); + } + + return (globalLocale_.get() != NULL); + } + + + static void InitializeIcu() + { +#if (ORTHANC_STATIC_ICU == 1) && (ORTHANC_ENABLE_ICU == 1) + if (globalIcuData_.empty()) + { + LOG(INFO) << "Setting up the ICU common data"; + + GzipCompressor compressor; + compressor.Uncompress(globalIcuData_, + FrameworkResources::GetFileResourceBuffer(FrameworkResources::LIBICU_DATA), + FrameworkResources::GetFileResourceSize(FrameworkResources::LIBICU_DATA)); + + std::string md5; + Toolbox::ComputeMD5(md5, globalIcuData_); + + if (md5 != ORTHANC_ICU_DATA_MD5 || + globalIcuData_.empty()) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot decode the ICU common data"); + } + + // "ICU data is designed to be 16-aligned" + // http://userguide.icu-project.org/icudata#TOC-Alignment + + { + static const size_t ALIGN = 16; + + UErrorCode status = U_ZERO_ERROR; + + if (reinterpret_cast(globalIcuData_.c_str()) % ALIGN == 0) + { + // Data is already properly aligned + udata_setCommonData(globalIcuData_.c_str(), &status); + } + else + { + std::string aligned; + aligned.resize(globalIcuData_.size() + ALIGN - 1); + + intptr_t offset = reinterpret_cast(aligned.c_str()) % ALIGN; + if (offset != 0) + { + offset = ALIGN - offset; + } + + if (offset + globalIcuData_.size() > aligned.size()) + { + throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary"); + } + + // We don't use "memcpy()", as it expects its data to be aligned + const uint8_t* p = reinterpret_cast(&globalIcuData_[0]); + uint8_t* q = reinterpret_cast(&aligned[0]) + offset; + for (size_t i = 0; i < globalIcuData_.size(); i++, p++, q++) + { + *q = *p; + } + + globalIcuData_.swap(aligned); + + const uint8_t* data = reinterpret_cast(globalIcuData_.c_str()) + offset; + + if (reinterpret_cast(data) % ALIGN != 0) + { + throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary"); + } + else + { + udata_setCommonData(data, &status); + } + } + + if (status != U_ZERO_ERROR) + { + throw OrthancException(ErrorCode_InternalError, "Cannot initialize ICU"); + } + } + + if (Toolbox::DetectEndianness() != Endianness_Little) + { + // TODO - The data table must be swapped (uint16_t) + throw OrthancException(ErrorCode_NotImplemented); + } + + // "First-use of ICU from a single thread before the + // multi-threaded use of ICU begins", to make sure everything is + // properly initialized (should not be mandatory in our + // case). We let boost handle calls to "u_init()" and "u_cleanup()". + // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination + uloc_getDefault(); + } +#endif + } + + void Toolbox::InitializeGlobalLocale(const char* locale) + { + InitializeIcu(); + +#if defined(__unix__) && ORTHANC_SANDBOXED != 1 + static const char* LOCALTIME = "/etc/localtime"; + + if (!SystemToolbox::IsExistingFile(LOCALTIME)) + { + // Check out file + // "boost_1_69_0/libs/locale/src/icu/time_zone.cpp": Direct + // access is made to this file if ICU is not used. Crash arises + // in Boost if the file is a symbolic link to a non-existing + // file (such as in Ubuntu 16.04 base Docker image). + throw OrthancException( + ErrorCode_InternalError, + "On UNIX-like systems, the file " + std::string(LOCALTIME) + + " must be present on the filesystem (install \"tzdata\" package on Debian)"); + } +#endif + + bool ok; + + if (locale == NULL) + { + // Make Orthanc use English, United States locale + // Linux: use "en_US.UTF-8" + // Windows: use "" + // Wine: use NULL + +#if defined(__MINGW32__) + // Visibly, there is no support of locales in MinGW yet + // http://mingw.5.n7.nabble.com/How-to-use-std-locale-global-with-MinGW-correct-td33048.html + static const char* DEFAULT_LOCALE = NULL; +#elif defined(_WIN32) + // For Windows: use default locale (using "en_US" does not work) + static const char* DEFAULT_LOCALE = ""; +#else + // For Linux & cie + static const char* DEFAULT_LOCALE = "en_US.UTF-8"; +#endif + + ok = SetGlobalLocale(DEFAULT_LOCALE); + +#if defined(__MINGW32__) + LOG(WARNING) << "This is a MinGW build, case-insensitive comparison of " + << "strings with accents will not work outside of Wine"; +#endif + } + else + { + ok = SetGlobalLocale(locale); + } + + if (!ok && + !SetGlobalLocale(NULL)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot initialize global locale"); + } + + } + + + void Toolbox::FinalizeGlobalLocale() + { + globalLocale_.reset(); + } + + + std::string Toolbox::ToUpperCaseWithAccents(const std::string& source) + { + bool error = (globalLocale_.get() == NULL); + +#if ORTHANC_STATIC_ICU == 1 +# if ORTHANC_ENABLE_ICU == 0 + throw OrthancException(ErrorCode_NotImplemented, "ICU is disabled for this target"); +# else + if (globalIcuData_.empty()) + { + error = true; + } +# endif +#endif + + if (error) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "No global locale was set, call Toolbox::InitializeGlobalLocale()"); + } + + /** + * A few notes about locales: + * + * (1) We don't use "case folding": + * http://www.boost.org/doc/libs/1_64_0/libs/locale/doc/html/conversions.html + * + * Characters are made uppercase one by one. This is because, in + * static builds, we are using iconv, which is visibly not + * supported correctly (TODO: Understand why). Case folding seems + * to be working correctly if using the default backend under + * Linux (ICU or POSIX?). If one wishes to use case folding, one + * would use: + * + * boost::locale::generator gen; + * std::locale::global(gen(DEFAULT_LOCALE)); + * return boost::locale::to_upper(source); + * + * (2) The function "boost::algorithm::to_upper_copy" does not + * make use of the "std::locale::global()". We therefore create a + * global variable "globalLocale_". + * + * (3) The variant of "boost::algorithm::to_upper_copy()" that + * uses std::string does not work properly. We need to apply it + * one wide strings (std::wstring). This explains the two calls to + * "utf_to_utf" in order to convert to/from std::wstring. + **/ + + std::wstring w = boost::locale::conv::utf_to_utf(source, boost::locale::conv::skip); + w = boost::algorithm::to_upper_copy(w, *globalLocale_); + return boost::locale::conv::utf_to_utf(w, boost::locale::conv::skip); + } +#endif + + + +#if ORTHANC_ENABLE_SSL == 0 + /** + * OpenSSL is disabled + **/ + void Toolbox::InitializeOpenSsl() + { + LOG(INFO) << "OpenSSL is disabled"; + } + + void Toolbox::FinalizeOpenSsl() + { + } + + +#elif (ORTHANC_ENABLE_SSL == 1 && \ + OPENSSL_VERSION_NUMBER < 0x10100000L) + /** + * OpenSSL < 1.1.0 + **/ + void Toolbox::InitializeOpenSsl() + { + LOG(INFO) << "OpenSSL version: " << OPENSSL_VERSION_TEXT; + + // https://wiki.openssl.org/index.php/Library_Initialization + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + ERR_load_crypto_strings(); + } + + void Toolbox::FinalizeOpenSsl() + { + // Finalize OpenSSL + // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup +#ifdef FIPS_mode_set + FIPS_mode_set(0); +#endif + +#if !defined(OPENSSL_NO_ENGINE) + ENGINE_cleanup(); +#endif + + CONF_modules_unload(1); + EVP_cleanup(); + CRYPTO_cleanup_all_ex_data(); + ERR_remove_state(0); + ERR_free_strings(); + } + + +#elif (ORTHANC_ENABLE_SSL == 1 && \ + OPENSSL_VERSION_NUMBER >= 0x10100000L) + /** + * OpenSSL >= 1.1.0. In this case, the initialization is + * automatically done by the functions of OpenSSL. + * https://wiki.openssl.org/index.php/Library_Initialization + **/ + void Toolbox::InitializeOpenSsl() + { + LOG(INFO) << "OpenSSL version: " << OPENSSL_VERSION_TEXT; + } + + void Toolbox::FinalizeOpenSsl() + { + } + +#else +# error "Support your platform here" +#endif + + + + std::string Toolbox::GenerateUuid() + { +#ifdef _WIN32 + UUID uuid; + UuidCreate ( &uuid ); + + unsigned char * str; + UuidToStringA ( &uuid, &str ); + + std::string s( ( char* ) str ); + + RpcStringFreeA ( &str ); +#else + uuid_t uuid; + uuid_generate_random ( uuid ); + char s[37]; + uuid_unparse ( uuid, s ); +#endif + return s; + } + + + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + + class VariableFormatter + { + public: + typedef std::map Dictionary; + + private: + const Dictionary& dictionary_; + + public: + explicit VariableFormatter(const Dictionary& dictionary) : + dictionary_(dictionary) + { + } + + template + Out operator()(const boost::smatch& what, + Out out) const + { + if (!what[1].str().empty()) + { + // Variable without a default value + Dictionary::const_iterator found = dictionary_.find(what[1]); + + if (found != dictionary_.end()) + { + const std::string& value = found->second; + out = std::copy(value.begin(), value.end(), out); + } + } + else + { + // Variable with a default value + std::string key; + std::string defaultValue; + + if (!what[2].str().empty()) + { + key = what[2].str(); + defaultValue = what[3].str(); + } + else if (!what[4].str().empty()) + { + key = what[4].str(); + defaultValue = what[5].str(); + } + else if (!what[6].str().empty()) + { + key = what[6].str(); + defaultValue = what[7].str(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + + Dictionary::const_iterator found = dictionary_.find(key); + + if (found == dictionary_.end()) + { + out = std::copy(defaultValue.begin(), defaultValue.end(), out); + } + else + { + const std::string& value = found->second; + out = std::copy(value.begin(), value.end(), out); + } + } + + return out; + } + }; + } + + + std::string Toolbox::SubstituteVariables(const std::string& source, + const std::map& dictionary) + { + const boost::regex pattern("\\$\\{([^:]*?)\\}|" // ${what[1]} + "\\$\\{([^:]*?):-([^'\"]*?)\\}|" // ${what[2]:-what[3]} + "\\$\\{([^:]*?):-\"([^\"]*?)\"\\}|" // ${what[4]:-"what[5]"} + "\\$\\{([^:]*?):-'([^']*?)'\\}"); // ${what[6]:-'what[7]'} + + VariableFormatter formatter(dictionary); + + return boost::regex_replace(source, pattern, formatter); + } + + + namespace Iso2022 + { + /** + Returns whether the string s contains a single-byte control message + at index i + **/ + static inline bool IsControlMessage1(const std::string& s, size_t i) + { + if (i < s.size()) + { + char c = s[i]; + return + (c == '\x0f') || // Locking shift zero + (c == '\x0e'); // Locking shift one + } + else + { + return false; + } + } + + /** + Returns whether the string s contains a double-byte control message + at index i + **/ + static inline size_t IsControlMessage2(const std::string& s, size_t i) + { + if (i + 1 < s.size()) + { + char c1 = s[i]; + char c2 = s[i + 1]; + return (c1 == 0x1b) && ( + (c2 == '\x6e') || // Locking shift two + (c2 == '\x6f') || // Locking shift three + (c2 == '\x4e') || // Single shift two (alt) + (c2 == '\x4f') || // Single shift three (alt) + (c2 == '\x7c') || // Locking shift three right + (c2 == '\x7d') || // Locking shift two right + (c2 == '\x7e') // Locking shift one right + ); + } + else + { + return false; + } + } + + /** + Returns whether the string s contains a triple-byte control message + at index i + **/ + static inline size_t IsControlMessage3(const std::string& s, size_t i) + { + if (i + 2 < s.size()) + { + char c1 = s[i]; + char c2 = s[i + 1]; + char c3 = s[i + 2]; + return ((c1 == '\x8e' && c2 == 0x1b && c3 == '\x4e') || + (c1 == '\x8f' && c2 == 0x1b && c3 == '\x4f')); + } + else + { + return false; + } + } + + /** + This function returns true if the index i in the supplied string s: + - is valid + - contains the c character + This function returns false otherwise. + **/ + static inline bool TestCharValue( + const std::string& s, size_t i, char c) + { + if (i < s.size()) + return s[i] == c; + else + return false; + } + + /** + This function returns true if the index i in the supplied string s: + - is valid + - has a c character that is >= cMin and <= cMax (included) + This function returns false otherwise. + **/ + static inline bool TestCharRange( + const std::string& s, size_t i, char cMin, char cMax) + { + if (i < s.size()) + return (s[i] >= cMin) && (s[i] <= cMax); + else + return false; + } + + /** + This function returns the total length in bytes of the escape sequence + located in string s at index i, if there is one, or 0 otherwise. + **/ + static inline size_t GetEscapeSequenceLength(const std::string& s, size_t i) + { + if (TestCharValue(s, i, 0x1b)) + { + size_t j = i+1; + + // advance reading cursor while we are in a sequence + while (TestCharRange(s, j, '\x20', '\x2f')) + ++j; + + // check there is a valid termination byte AND we're long enough (there + // must be at least one byte between 0x20 and 0x2f + if (TestCharRange(s, j, '\x30', '\x7f') && (j - i) >= 2) + return j - i + 1; + else + return 0; + } + else + return 0; + } + } + + + + /** + This function will strip all ISO/IEC 2022 control codes and escape + sequences. + Please see https://en.wikipedia.org/wiki/ISO/IEC_2022 (as of 2019-02) + for a list of those. + + Please note that this operation is potentially destructive, because + it removes the character set information from the byte stream. + + However, in the case where the encoding is unique, then suppressing + the escape sequences allows one to provide us with a clean string after + conversion to utf-8 with boost. + **/ + void Toolbox::RemoveIso2022EscapeSequences(std::string& dest, const std::string& src) + { + // we need AT MOST the same size as the source string in the output + dest.clear(); + if (dest.capacity() < src.size()) + dest.reserve(src.size()); + + size_t i = 0; + + // uint8_t view to the string + while (i < src.size()) + { + size_t j = i; + + // The i index will only be incremented if a message is detected + // in that case, the message is skipped and the index is set to the + // next position to read + if (Iso2022::IsControlMessage1(src, i)) + i += 1; + else if (Iso2022::IsControlMessage2(src, i)) + i += 2; + else if (Iso2022::IsControlMessage3(src, i)) + i += 3; + else + i += Iso2022::GetEscapeSequenceLength(src, i); + + // if the index was NOT incremented, this means there was no message at + // this location: we then may copy the character at this index and + // increment the index to point to the next read position + if (j == i) + { + dest.push_back(src[i]); + i++; + } + } + } + + + void Toolbox::Utf8ToUnicodeCharacter(uint32_t& unicode, + size_t& length, + const std::string& utf8, + size_t position) + { + // https://en.wikipedia.org/wiki/UTF-8 + + static const uint8_t MASK_IS_1_BYTE = 0x80; // printf '0x%x\n' "$((2#10000000))" + static const uint8_t TEST_IS_1_BYTE = 0x00; + + static const uint8_t MASK_IS_2_BYTES = 0xe0; // printf '0x%x\n' "$((2#11100000))" + static const uint8_t TEST_IS_2_BYTES = 0xc0; // printf '0x%x\n' "$((2#11000000))" + + static const uint8_t MASK_IS_3_BYTES = 0xf0; // printf '0x%x\n' "$((2#11110000))" + static const uint8_t TEST_IS_3_BYTES = 0xe0; // printf '0x%x\n' "$((2#11100000))" + + static const uint8_t MASK_IS_4_BYTES = 0xf8; // printf '0x%x\n' "$((2#11111000))" + static const uint8_t TEST_IS_4_BYTES = 0xf0; // printf '0x%x\n' "$((2#11110000))" + + static const uint8_t MASK_CONTINUATION = 0xc0; // printf '0x%x\n' "$((2#11000000))" + static const uint8_t TEST_CONTINUATION = 0x80; // printf '0x%x\n' "$((2#10000000))" + + if (position >= utf8.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(sizeof(uint8_t) == sizeof(char)); + const uint8_t* buffer = reinterpret_cast(utf8.c_str()) + position; + + if ((buffer[0] & MASK_IS_1_BYTE) == TEST_IS_1_BYTE) + { + length = 1; + unicode = buffer[0] & ~MASK_IS_1_BYTE; + } + else if ((buffer[0] & MASK_IS_2_BYTES) == TEST_IS_2_BYTES && + position + 1 < utf8.size() && + (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION) + { + length = 2; + uint32_t a = buffer[0] & ~MASK_IS_2_BYTES; + uint32_t b = buffer[1] & ~MASK_CONTINUATION; + unicode = (a << 6) | b; + } + else if ((buffer[0] & MASK_IS_3_BYTES) == TEST_IS_3_BYTES && + position + 2 < utf8.size() && + (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION && + (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION) + { + length = 3; + uint32_t a = buffer[0] & ~MASK_IS_3_BYTES; + uint32_t b = buffer[1] & ~MASK_CONTINUATION; + uint32_t c = buffer[2] & ~MASK_CONTINUATION; + unicode = (a << 12) | (b << 6) | c; + } + else if ((buffer[0] & MASK_IS_4_BYTES) == TEST_IS_4_BYTES && + position + 3 < utf8.size() && + (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION && + (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION && + (buffer[3] & MASK_CONTINUATION) == TEST_CONTINUATION) + { + length = 4; + uint32_t a = buffer[0] & ~MASK_IS_4_BYTES; + uint32_t b = buffer[1] & ~MASK_CONTINUATION; + uint32_t c = buffer[2] & ~MASK_CONTINUATION; + uint32_t d = buffer[3] & ~MASK_CONTINUATION; + unicode = (a << 18) | (b << 12) | (c << 6) | d; + } + else + { + // This is not a valid UTF-8 encoding + throw OrthancException(ErrorCode_BadFileFormat, "Invalid UTF-8 string"); + } + } + + + std::string Toolbox::LargeHexadecimalToDecimal(const std::string& hex) + { + /** + * NB: Focus of the code below is *not* efficiency, but + * readability! + **/ + + for (size_t i = 0; i < hex.size(); i++) + { + const char c = hex[i]; + if (!((c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f') || + (c >= '0' && c <= '9'))) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Not an hexadecimal number"); + } + } + + std::vector decimal; + decimal.push_back(0); + + for (size_t i = 0; i < hex.size(); i++) + { + uint8_t hexDigit = static_cast(Hex2Dec(hex[i])); + assert(hexDigit <= 15); + + for (size_t j = 0; j < decimal.size(); j++) + { + uint8_t val = static_cast(decimal[j]) * 16 + hexDigit; // Maximum: 9 * 16 + 15 + assert(val <= 159 /* == 9 * 16 + 15 */); + + decimal[j] = val % 10; + hexDigit = val / 10; + assert(hexDigit <= 15 /* == 159 / 10 */); + } + + while (hexDigit > 0) + { + decimal.push_back(hexDigit % 10); + hexDigit /= 10; + } + } + + size_t start = 0; + while (start < decimal.size() && + decimal[start] == '0') + { + start++; + } + + std::string s; + s.reserve(decimal.size() - start); + + for (size_t i = decimal.size(); i > start; i--) + { + s.push_back(decimal[i - 1] + '0'); + } + + return s; + } + + + std::string Toolbox::GenerateDicomPrivateUniqueIdentifier() + { + /** + * REFERENCE: "Creating a Privately Defined Unique Identifier + * (Informative)" / "UUID Derived UID" + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html + * https://stackoverflow.com/a/46316162/881731 + **/ + + std::string uuid = GenerateUuid(); + assert(IsUuid(uuid) && uuid.size() == 36); + + /** + * After removing the four dashes ("-") out of the 36-character + * UUID, we get a large hexadecimal number with 32 characters, + * each of those characters lying in the range [0,16[. The large + * number is thus in the [0,16^32[ = [0,256^16[ range. This number + * has a maximum of 39 decimal digits, as can be seen in Python: + * + * # python -c 'import math; print(math.log(16**32))/math.log(10))' + * 38.531839445 + * + * We now to convert the large hexadecimal number to a decimal + * number with up to 39 digits, remove the leading zeros, then + * prefix it with "2.25." + **/ + + // Remove the dashes + std::string hex = (uuid.substr(0, 8) + + uuid.substr(9, 4) + + uuid.substr(14, 4) + + uuid.substr(19, 4) + + uuid.substr(24, 12)); + assert(hex.size() == 32); + + return "2.25." + LargeHexadecimalToDecimal(hex); + } + + + void Toolbox::SimplifyDicomAsJson(Json::Value& target, + const Json::Value& source, + DicomToJsonFormat format) + { + if (!source.isObject()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + target = Json::objectValue; + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& v = source[members[i]]; + const std::string& type = v["Type"].asString(); + + std::string name; + switch (format) + { + case DicomToJsonFormat_Human: + name = v["Name"].asString(); + break; + + case DicomToJsonFormat_Short: + name = members[i]; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (type == "String") + { + target[name] = v["Value"].asString(); + } + else if (type == "TooLong" || + type == "Null" || + type == "Binary") + { + target[name] = Json::nullValue; + } + else if (type == "Sequence") + { + const Json::Value& array = v["Value"]; + assert(array.isArray()); + + Json::Value children = Json::arrayValue; + for (Json::Value::ArrayIndex j = 0; j < array.size(); j++) + { + Json::Value c; + SimplifyDicomAsJson(c, array[j], format); + children.append(c); + } + + target[name] = children; + } + else + { + assert(0); + } + } + } + + + static bool ReadJsonInternal(Json::Value& target, + const void* buffer, + size_t size, + bool collectComments) + { +#if JSONCPP_USE_DEPRECATED == 1 + Json::Reader reader; + return reader.parse(reinterpret_cast(buffer), + reinterpret_cast(buffer) + size, target, collectComments); +#else + Json::CharReaderBuilder builder; + builder.settings_["collectComments"] = collectComments; + + const std::unique_ptr reader(builder.newCharReader()); + assert(reader.get() != NULL); + + JSONCPP_STRING err; + if (reader->parse(reinterpret_cast(buffer), + reinterpret_cast(buffer) + size, &target, &err)) + { + return true; + } + else + { + LOG(ERROR) << "Cannot parse JSON: " << err; + return false; + } +#endif + } + + + bool Toolbox::ReadJson(Json::Value& target, + const std::string& source) + { + return ReadJson(target, source.empty() ? NULL : source.c_str(), source.size()); + } + + + bool Toolbox::ReadJson(Json::Value& target, + const void* buffer, + size_t size) + { + return ReadJsonInternal(target, buffer, size, true); + } + + + bool Toolbox::ReadJsonWithoutComments(Json::Value& target, + const std::string& source) + { + return ReadJsonWithoutComments(target, source.empty() ? NULL : source.c_str(), source.size()); + } + + + bool Toolbox::ReadJsonWithoutComments(Json::Value& target, + const void* buffer, + size_t size) + { + return ReadJsonInternal(target, buffer, size, false); + } + + + void Toolbox::WriteFastJson(std::string& target, + const Json::Value& source) + { +#if JSONCPP_USE_DEPRECATED == 1 + Json::FastWriter writer; + target = writer.write(source); +#else + Json::StreamWriterBuilder builder; + builder.settings_["indentation"] = ""; + target = Json::writeString(builder, source); +#endif + } + + + void Toolbox::WriteStyledJson(std::string& target, + const Json::Value& source) + { +#if JSONCPP_USE_DEPRECATED == 1 + Json::StyledWriter writer; + target = writer.write(source); +#else + Json::StreamWriterBuilder builder; + builder.settings_["indentation"] = " "; + target = Json::writeString(builder, source); +#endif + } + + + void Toolbox::RemoveSurroundingQuotes(std::string& value) + { + if (!value.empty() && + value[0] == '\"' && + value[value.size() - 1] == '\"') + { + value = value.substr(1, value.size() - 2); + } + } + + Toolbox::ElapsedTimer::ElapsedTimer() + { + Restart(); + } + + void Toolbox::ElapsedTimer::Restart() + { + start_ = boost::posix_time::microsec_clock::universal_time(); + } + + uint64_t Toolbox::ElapsedTimer::GetElapsedMilliseconds() + { + return GetElapsedNanoseconds() / 1000000; + } + + uint64_t Toolbox::ElapsedTimer::GetElapsedMicroseconds() + { + return GetElapsedNanoseconds() / 1000; + } + + uint64_t Toolbox::ElapsedTimer::GetElapsedNanoseconds() + { + boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + boost::posix_time::time_duration diff = now - start_; + return static_cast(diff.total_nanoseconds()); + } + + std::string Toolbox::ElapsedTimer::GetHumanElapsedDuration() + { + return Toolbox::GetHumanDuration(GetElapsedNanoseconds()); + } + + // in "full" mode, returns " 26.45MB in 2.25s = 94.04Mbps" + // else, returns "94.04Mbps" + std::string Toolbox::ElapsedTimer::GetHumanTransferSpeed(bool full, uint64_t sizeInBytes) + { + return Toolbox::GetHumanTransferSpeed(full, sizeInBytes, GetElapsedNanoseconds()); + } + + Toolbox::DebugElapsedTimeLogger::DebugElapsedTimeLogger(const std::string& message) + : message_(message), + logged_(false) + { + Restart(); + } + + Toolbox::DebugElapsedTimeLogger::~DebugElapsedTimeLogger() + { + if (!logged_) + { + StopAndLog(); + } + } + + void Toolbox::DebugElapsedTimeLogger::Restart() + { + timer_.Restart(); + } + + void Toolbox::DebugElapsedTimeLogger::StopAndLog() + { + LOG(WARNING) << "ELAPSED TIMER: " << message_ << " (" << timer_.GetElapsedMicroseconds() << " us)"; + logged_ = true; + } + + Toolbox::ApiElapsedTimeLogger::ApiElapsedTimeLogger(const std::string& message) : + message_(message) + { + timer_.Restart(); + CLOG(INFO, HTTP) << message_; + } + + Toolbox::ApiElapsedTimeLogger::~ApiElapsedTimeLogger() + { + CLOG(INFO, HTTP) << message_ << " (elapsed: " << timer_.GetElapsedMicroseconds() << " us)"; + } + + std::string Toolbox::GetHumanFileSize(uint64_t sizeInBytes) + { + if (sizeInBytes < 1024) + { + std::ostringstream oss; + oss << sizeInBytes << "bytes"; + return oss.str(); + } + else + { + static const char* suffixes[] = {"KB", "MB", "GB", "TB"}; + static const int suffixesCount = sizeof(suffixes) / sizeof(suffixes[0]); + + int i = 0; + double size = static_cast(sizeInBytes)/1024.0; + + while (size >= 1024.0 && i < suffixesCount - 1) + { + size /= 1024.0; + i++; + } + + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << size << suffixes[i]; + return oss.str(); + } + } + + std::string Toolbox::GetHumanDuration(uint64_t durationInNanoseconds) + { + if (durationInNanoseconds < 1024) + { + std::ostringstream oss; + oss << durationInNanoseconds << "ns"; + return oss.str(); + } + else + { + static const char* suffixes[] = {"ns", "us", "ms", "s"}; + static const int suffixesCount = sizeof(suffixes) / sizeof(suffixes[0]); + + int i = 0; + double duration = static_cast(durationInNanoseconds); + + while (duration >= 1000.0 && i < suffixesCount - 1) + { + duration /= 1000.0; + i++; + } + + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << duration << suffixes[i]; + return oss.str(); + } + } + + std::string Toolbox::GetHumanTransferSpeed(bool full, uint64_t sizeInBytes, uint64_t durationInNanoseconds) + { + // in "full" mode, returns " 26.45MB in 2.25s = 94.04Mbps" + // else, return "94.04Mbps" + + if (full) + { + std::ostringstream oss; + oss << Toolbox::GetHumanFileSize(sizeInBytes) << " in " << Toolbox::GetHumanDuration(durationInNanoseconds) << " = " << GetHumanTransferSpeed(false, sizeInBytes, durationInNanoseconds); + return oss.str(); + } + + double throughputInBps = 8.0 * 1000000000.0 * static_cast(sizeInBytes) / static_cast(durationInNanoseconds); + + if (throughputInBps < 1000.0) + { + std::ostringstream oss; + oss << throughputInBps << "bps"; + return oss.str(); + } + else + { + throughputInBps /= 1000.0; + static const char* suffixes[] = {"kbps", "Mbps", "Gbps"}; + static const int suffixesCount = sizeof(suffixes) / sizeof(suffixes[0]); + + int i = 0; + + while (throughputInBps >= 1000.0 && i < suffixesCount - 1) + { + throughputInBps /= 1000.0; + i++; + } + + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << throughputInBps << suffixes[i]; + return oss.str(); + } + } + + + bool Toolbox::ParseVersion(unsigned int& major, + unsigned int& minor, + unsigned int& revision, + const char* version) + { + if (version == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + +#ifdef _MSC_VER +#define ORTHANC_SCANF sscanf_s +#else +#define ORTHANC_SCANF sscanf +#endif + + int a, b, c; + if (ORTHANC_SCANF(version, "%4d.%4d.%4d", &a, &b, &c) == 3) + { + if (a >= 0 && + b >= 0 && + c >= 0) + { + major = static_cast(a); + minor = static_cast(b); + revision = static_cast(c); + return true; + } + else + { + return false; + } + } + else if (ORTHANC_SCANF(version, "%4d.%4d", &a, &b) == 2) + { + if (a >= 0 && + b >= 0) + { + major = static_cast(a); + minor = static_cast(b); + revision = 0; + return true; + } + else + { + return false; + } + } + else if (ORTHANC_SCANF(version, "%4d", &a) == 1 && + a >= 0) + { + if (a >= 0) + { + major = static_cast(a); + minor = 0; + revision = 0; + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + + + bool Toolbox::IsVersionAbove(const char* version, + unsigned int major, + unsigned int minor, + unsigned int revision) + { + /** + * Note: Similar standalone functions are implemented in + * "OrthancCPlugin.h" and "OrthancPluginCppWrapper.cpp". + **/ + + unsigned int actualMajor, actualMinor, actualRevision; + + if (version == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (!strcmp(version, "mainline")) + { + // Assume compatibility with the mainline + return true; + } + else if (ParseVersion(actualMajor, actualMinor, actualRevision, version)) + { + if (actualMajor > major) + { + return true; + } + + if (actualMajor < major) + { + return false; + } + + // Check the minor version number + assert(actualMajor == major); + + if (actualMinor > minor) + { + return true; + } + + if (actualMinor < minor) + { + return false; + } + + // Check the patch level version number + assert(actualMajor == major); + + if (actualRevision >= revision) + { + return true; + } + else + { + return false; + } + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid version: " + std::string(version)); + } + } +} + + + +OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content) +{ + return reinterpret_cast(new Orthanc::Toolbox::LinesIterator(content)); +} + + +bool OrthancLinesIterator_GetLine(std::string& target, + const OrthancLinesIterator* iterator) +{ + if (iterator != NULL) + { + return reinterpret_cast(iterator)->GetLine(target); + } + else + { + return false; + } +} + + +void OrthancLinesIterator_Next(OrthancLinesIterator* iterator) +{ + if (iterator != NULL) + { + reinterpret_cast(iterator)->Next(); + } +} + + +void OrthancLinesIterator_Free(OrthancLinesIterator* iterator) +{ + if (iterator != NULL) + { + delete reinterpret_cast(iterator); + } +} diff --git a/OrthancFramework/Sources/Toolbox.h b/OrthancFramework/Sources/Toolbox.h new file mode 100644 index 0000000..5337619 --- /dev/null +++ b/OrthancFramework/Sources/Toolbox.h @@ -0,0 +1,487 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "Enumerations.h" +#include "OrthancFramework.h" + +#include +#include +#include +#include + + +#if !defined(ORTHANC_ENABLE_BASE64) +# error The macro ORTHANC_ENABLE_BASE64 must be defined +#endif + +#if !defined(ORTHANC_ENABLE_LOCALE) +# error The macro ORTHANC_ENABLE_LOCALE must be defined +#endif + +#if !defined(ORTHANC_ENABLE_MD5) +# error The macro ORTHANC_ENABLE_MD5 must be defined +#endif + +#if !defined(ORTHANC_ENABLE_PUGIXML) +# error The macro ORTHANC_ENABLE_PUGIXML must be defined +#endif + +#if !defined(ORTHANC_ENABLE_SSL) +# error The macro ORTHANC_ENABLE_SSL must be defined +#endif + + +/** + * NOTE: GUID vs. UUID + * The simple answer is: no difference, they are the same thing. Treat + * them as a 16 byte (128 bits) value that is used as a unique + * value. In Microsoft-speak they are called GUIDs, but call them + * UUIDs when not using Microsoft-speak. + * http://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid + **/ + + +#if ORTHANC_ENABLE_PUGIXML == 1 +# include +#endif + +#include + + +namespace Orthanc +{ + typedef std::vector UriComponents; + + class NullType + { + }; + + class ORTHANC_PUBLIC Toolbox + { + public: +#if ORTHANC_ENABLE_MD5 == 1 + class ORTHANC_PUBLIC MD5Context : public boost::noncopyable + { + private: + class PImpl; + boost::shared_ptr pimpl_; + + public: + MD5Context(); + + void Append(const void* data, + size_t size); + + void Append(const std::string& source); + + void Export(std::string& target); + }; +#endif + + class ORTHANC_PUBLIC LinesIterator : public boost::noncopyable + { + private: + const std::string& content_; + size_t lineStart_; + size_t lineEnd_; + + void FindEndOfLine(); + + public: + explicit LinesIterator(const std::string& content); + + bool GetLine(std::string& target) const; + + void Next(); + }; + + static void ToUpperCase(std::string& s); // Inplace version + + static void ToLowerCase(std::string& s); // Inplace version + + static void ToUpperCase(std::string& result, + const std::string& source); + + static void ToLowerCase(std::string& result, + const std::string& source); + + static void SplitUriComponents(UriComponents& components, + const std::string& uri); + + static void TruncateUri(UriComponents& target, + const UriComponents& source, + size_t fromLevel); + + static bool IsChildUri(const UriComponents& baseUri, + const UriComponents& testedUri); + + static std::string FlattenUri(const UriComponents& components, + size_t fromLevel = 0); + + static std::string JoinUri(const std::string& base, const std::string& uri); + +#if ORTHANC_ENABLE_MD5 == 1 + static void ComputeMD5(std::string& result, + const std::string& data); + + static void ComputeMD5(std::string& result, + const void* data, + size_t size); + + static void ComputeMD5(std::string& result, + const std::set& data); +#endif + + static void ComputeSHA1(std::string& result, + const std::string& data); + + static void ComputeSHA1(std::string& result, + const void* data, + size_t size); + + static bool IsSHA1(const void* str, + size_t size); + + static bool IsSHA1(const std::string& s); + +#if ORTHANC_ENABLE_BASE64 == 1 + static void DecodeBase64(std::string& result, + const std::string& data); + + static void EncodeBase64(std::string& result, + const std::string& data); + + static bool DecodeDataUriScheme(std::string& mime, + std::string& content, + const std::string& source); + + static void EncodeDataUriScheme(std::string& result, + const std::string& mime, + const std::string& content); +#endif + +#if ORTHANC_ENABLE_LOCALE == 1 + static std::string ConvertToUtf8(const std::string& source, + Encoding sourceEncoding, + bool hasCodeExtensions); + + static std::string ConvertFromUtf8(const std::string& source, + Encoding targetEncoding); +#endif + + static bool IsAsciiString(const void* data, + size_t size); + + static bool IsAsciiString(const std::string& s); + + static std::string ConvertToAscii(const std::string& source); + + static std::string StripSpaces(const std::string& source); + + // In-place percent-decoding for URL + static void UrlDecode(std::string& s); + + static Endianness DetectEndianness(); + + static std::string WildcardToRegularExpression(const std::string& s); + + // TokenizeString result might contain empty strings (not SplitString) + static void TokenizeString(std::vector& result, + const std::string& source, + char separator); + + // SplitString result won't contain empty strings (compared to TokenizeString) + static void SplitString(std::vector& result, + const std::string& source, + char separator); + + // SplitString result won't contain empty strings (compared to TokenizeString) + static void SplitString(std::set& result, + const std::string& source, + char separator); + + static void JoinStrings(std::string& result, + const std::set& source, + const char* separator); + + static void JoinStrings(std::string& result, + const std::vector& source, + const char* separator); + + // returns true if all element of 'needles' are found in 'haystack' + template static bool IsSetInSet(const std::set& needles, const std::set& haystack) + { + for (typename std::set::const_iterator it = needles.begin(); + it != needles.end(); ++it) + { + if (haystack.count(*it) == 0) + { + return false; + } + } + + return true; + } + + // returns the set of elements from 'needles' that are not in 'haystack' + template static size_t GetMissingsFromSet(std::set& missings, const std::set& needles, const std::set& haystack) + { + missings.clear(); + + for (typename std::set::const_iterator it = needles.begin(); + it != needles.end(); ++it) + { + if (haystack.count(*it) == 0) + { + missings.insert(*it); + } + } + + return missings.size(); + } + + template static void AppendSets(std::set& target, const std::set& toAppend) + { + for (typename std::set::const_iterator it = toAppend.begin(); + it != toAppend.end(); ++it) + { + target.insert(*it); + } + } + + template static void RemoveSets(std::set& target, const std::set& toRemove) + { + for (typename std::set::const_iterator it = toRemove.begin(); + it != toRemove.end(); ++it) + { + target.erase(*it); + } + } + + // returns the elements that are both in a and b + template static void GetIntersection(std::set& target, const std::set& a, const std::set& b) + { + target.clear(); + + for (typename std::set::const_iterator it = a.begin(); + it != a.end(); ++it) + { + if (b.count(*it) > 0) + { + target.insert(*it); + } + } + } + + +#if ORTHANC_ENABLE_PUGIXML == 1 + static void JsonToXml(std::string& target, + const Json::Value& source, + const std::string& rootElement = "root", + const std::string& arrayElement = "item"); +#endif + +#if ORTHANC_ENABLE_PUGIXML == 1 + static void XmlToString(std::string& target, + const pugi::xml_document& source); +#endif + + static bool IsInteger(const std::string& str); + + static void CopyJsonWithoutComments(Json::Value& target, + const Json::Value& source); + + static bool StartsWith(const std::string& str, + const std::string& prefix); + + static void UriEncode(std::string& target, + const std::string& source); + + static std::string GetJsonStringField(const ::Json::Value& json, + const std::string& key, + const std::string& defaultValue); + + static bool GetJsonBooleanField(const ::Json::Value& json, + const std::string& key, + bool defaultValue); + + static int GetJsonIntegerField(const ::Json::Value& json, + const std::string& key, + int defaultValue); + + static unsigned int GetJsonUnsignedIntegerField(const ::Json::Value& json, + const std::string& key, + unsigned int defaultValue); + + static bool IsUuid(const std::string& str); + + static bool StartsWithUuid(const std::string& str); + +#if ORTHANC_ENABLE_LOCALE == 1 + static void InitializeGlobalLocale(const char* locale); + + static void FinalizeGlobalLocale(); + + static std::string ToUpperCaseWithAccents(const std::string& source); +#endif + + static void InitializeOpenSsl(); + + static void FinalizeOpenSsl(); + + static std::string GenerateUuid(); + + static std::string SubstituteVariables(const std::string& source, + const std::map& dictionary); + + static void RemoveIso2022EscapeSequences(std::string& dest, + const std::string& src); + + static void Utf8ToUnicodeCharacter(uint32_t& unicode, + size_t& utf8Length, + const std::string& utf8, + size_t position); + + static std::string LargeHexadecimalToDecimal(const std::string& hex); + + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html + static std::string GenerateDicomPrivateUniqueIdentifier(); + + static void SimplifyDicomAsJson(Json::Value& target, + const Json::Value& source, + DicomToJsonFormat format); + + static bool ReadJson(Json::Value& target, + const std::string& source); + + static bool ReadJson(Json::Value& target, + const void* buffer, + size_t size); + + static bool ReadJsonWithoutComments(Json::Value& target, + const std::string& source); + + static bool ReadJsonWithoutComments(Json::Value& target, + const void* buffer, + size_t size); + + static void WriteFastJson(std::string& target, + const Json::Value& source); + + static void WriteStyledJson(std::string& target, + const Json::Value& source); + + static void RemoveSurroundingQuotes(std::string& value); + + class ORTHANC_PUBLIC ElapsedTimer : public boost::noncopyable + { + private: + boost::posix_time::ptime start_; + + public: + ElapsedTimer(); + + uint64_t GetElapsedMilliseconds(); + uint64_t GetElapsedMicroseconds(); + uint64_t GetElapsedNanoseconds(); + + std::string GetHumanElapsedDuration(); + std::string GetHumanTransferSpeed(bool full, uint64_t sizeInBytes); + + void Restart(); + }; + + // This is a helper class to measure and log time spend e.g in a method. + // This should be used only during debugging and should likely not ever be used in a release. + // By default, you should use it as a RAII but you may force Restart/StopAndLog manually if needed. + class ORTHANC_PUBLIC DebugElapsedTimeLogger : public boost::noncopyable + { + private: + ElapsedTimer timer_; + const std::string message_; + bool logged_; + + public: + explicit DebugElapsedTimeLogger(const std::string& message); + + ~DebugElapsedTimeLogger(); + + void Restart(); + void StopAndLog(); + }; + + // This variant logs the same message when entering the method and when exiting (with the elapsed time). + // Logs goes to verbose-http. + class ORTHANC_PUBLIC ApiElapsedTimeLogger : public boost::noncopyable + { + private: + ElapsedTimer timer_; + const std::string message_; + + public: + explicit ApiElapsedTimeLogger(const std::string& message); + + ~ApiElapsedTimeLogger(); + }; + + static std::string GetHumanFileSize(uint64_t sizeInBytes); + + static std::string GetHumanDuration(uint64_t durationInNanoseconds); + + static std::string GetHumanTransferSpeed(bool full, uint64_t sizeInBytes, uint64_t durationInNanoseconds); + + static bool ParseVersion(unsigned int& major, + unsigned int& minor, + unsigned int& revision, + const char* version); + + static bool IsVersionAbove(const char* version, + unsigned int major, + unsigned int minor, + unsigned int revision); + }; +} + + + + +/** + * The plain C, opaque data structure "OrthancLinesIterator" is a thin + * wrapper around Orthanc::Toolbox::LinesIterator, and is only used by + * "../Resources/Patches/dcmtk-dcdict_orthanc.cc", in order to avoid + * code duplication + **/ + +struct OrthancLinesIterator; + +OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content); + +bool OrthancLinesIterator_GetLine(std::string& target, + const OrthancLinesIterator* iterator); + +void OrthancLinesIterator_Next(OrthancLinesIterator* iterator); + +void OrthancLinesIterator_Free(OrthancLinesIterator* iterator); diff --git a/OrthancFramework/Sources/WebServiceParameters.cpp b/OrthancFramework/Sources/WebServiceParameters.cpp new file mode 100644 index 0000000..9de8c07 --- /dev/null +++ b/OrthancFramework/Sources/WebServiceParameters.cpp @@ -0,0 +1,685 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "PrecompiledHeaders.h" +#include "WebServiceParameters.h" + +#include "Logging.h" +#include "OrthancException.h" +#include "SerializationToolbox.h" +#include "Toolbox.h" + +#if ORTHANC_SANDBOXED == 0 +# include "SystemToolbox.h" +#endif + +#include +#include + +namespace Orthanc +{ + static const char* KEY_CERTIFICATE_FILE = "CertificateFile"; + static const char* KEY_CERTIFICATE_KEY_FILE = "CertificateKeyFile"; + static const char* KEY_CERTIFICATE_KEY_PASSWORD = "CertificateKeyPassword"; + static const char* KEY_HTTP_HEADERS = "HttpHeaders"; + static const char* KEY_PASSWORD = "Password"; + static const char* KEY_PKCS11 = "Pkcs11"; + static const char* KEY_URL = "Url"; + static const char* KEY_URL_2 = "URL"; + static const char* KEY_USERNAME = "Username"; + static const char* KEY_TIMEOUT = "Timeout"; + + + static bool IsReservedKey(const std::string& key) + { + return (key == KEY_CERTIFICATE_FILE || + key == KEY_CERTIFICATE_KEY_FILE || + key == KEY_CERTIFICATE_KEY_PASSWORD || + key == KEY_HTTP_HEADERS || + key == KEY_PASSWORD || + key == KEY_PKCS11 || + key == KEY_URL || + key == KEY_URL_2 || + key == KEY_USERNAME || + key == KEY_TIMEOUT); + } + + + WebServiceParameters::WebServiceParameters() : + pkcs11Enabled_(false), + timeout_(0) + { + SetUrl("http://127.0.0.1:8042/"); + } + + WebServiceParameters::WebServiceParameters(const Json::Value &serialized) + { + Unserialize(serialized); + } + + const std::string &WebServiceParameters::GetUrl() const + { + return url_; + } + + + void WebServiceParameters::ClearClientCertificate() + { + certificateFile_.clear(); + certificateKeyFile_.clear(); + certificateKeyPassword_.clear(); + } + + + void WebServiceParameters::SetUrl(const std::string& url) + { + if (boost::find_first(url, "://")) + { + // Only allow the HTTP and HTTPS protocols + if (!Toolbox::StartsWith(url, "http://") && + !Toolbox::StartsWith(url, "https://")) + { + throw OrthancException(ErrorCode_BadFileFormat, "Bad URL: " + url); + } + } + + if (url.empty()) + { + throw OrthancException(ErrorCode_BadFileFormat, "Empty URL"); + } + + // Add trailing slash if needed + if (url[url.size() - 1] == '/') + { + url_ = url; + } + else + { + url_ = url + '/'; + } + } + + + void WebServiceParameters::ClearCredentials() + { + username_.clear(); + password_.clear(); + } + + + void WebServiceParameters::SetCredentials(const std::string& username, + const std::string& password) + { + if (username.empty() && + !password.empty()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + username_ = username; + password_ = password; + } + } + + const std::string &WebServiceParameters::GetUsername() const + { + return username_; + } + + const std::string &WebServiceParameters::GetPassword() const + { + return password_; + } + + + void WebServiceParameters::SetClientCertificate(const std::string& certificateFile, + const std::string& certificateKeyFile, + const std::string& certificateKeyPassword) + { + if (certificateFile.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (certificateKeyPassword.empty()) + { + LOG(WARNING) << "No password specified for certificate key file: " << certificateKeyFile; + } + + certificateFile_ = certificateFile; + certificateKeyFile_ = certificateKeyFile; + certificateKeyPassword_ = certificateKeyPassword; + } + + const std::string &WebServiceParameters::GetCertificateFile() const + { + return certificateFile_; + } + + const std::string &WebServiceParameters::GetCertificateKeyFile() const + { + return certificateKeyFile_; + } + + const std::string &WebServiceParameters::GetCertificateKeyPassword() const + { + return certificateKeyPassword_; + } + + void WebServiceParameters::SetPkcs11Enabled(bool enabled) + { + pkcs11Enabled_ = enabled; + } + + bool WebServiceParameters::IsPkcs11Enabled() const + { + return pkcs11Enabled_; + } + + void WebServiceParameters::AddHttpHeader(const std::string &key, const std::string &value) + { + headers_[key] = value; + } + + void WebServiceParameters::ClearHttpHeaders() + { + headers_.clear(); + } + + const WebServiceParameters::Dictionary &WebServiceParameters::GetHttpHeaders() const + { + return headers_; + } + + + void WebServiceParameters::FromSimpleFormat(const Json::Value& peer) + { + assert(peer.isArray()); + + pkcs11Enabled_ = false; + timeout_ = 0; + ClearClientCertificate(); + + if (peer.size() != 1 && + peer.size() != 3) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + SetUrl(peer.get(0u, "").asString()); + + if (peer.size() == 1) + { + ClearCredentials(); + } + else if (peer.size() == 2) + { + throw OrthancException(ErrorCode_BadFileFormat, + "The HTTP password is not provided"); + } + else if (peer.size() == 3) + { + SetCredentials(peer.get(1u, "").asString(), + peer.get(2u, "").asString()); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + static std::string GetStringMember(const Json::Value& peer, + const std::string& key, + const std::string& defaultValue) + { + if (!peer.isMember(key)) + { + return defaultValue; + } + else if (peer[key].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return peer[key].asString(); + } + } + + + void WebServiceParameters::FromAdvancedFormat(const Json::Value& peer) + { + assert(peer.isObject()); + + std::string url = GetStringMember(peer, KEY_URL, ""); + if (url.empty()) + { + SetUrl(GetStringMember(peer, KEY_URL_2, "")); + } + else + { + SetUrl(url); + } + + SetCredentials(GetStringMember(peer, KEY_USERNAME, ""), + GetStringMember(peer, KEY_PASSWORD, "")); + + std::string file = GetStringMember(peer, KEY_CERTIFICATE_FILE, ""); + if (!file.empty()) + { + SetClientCertificate(file, GetStringMember(peer, KEY_CERTIFICATE_KEY_FILE, ""), + GetStringMember(peer, KEY_CERTIFICATE_KEY_PASSWORD, "")); + } + else + { + ClearClientCertificate(); + } + + if (peer.isMember(KEY_PKCS11)) + { + if (peer[KEY_PKCS11].type() == Json::booleanValue) + { + pkcs11Enabled_ = peer[KEY_PKCS11].asBool(); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + else + { + pkcs11Enabled_ = false; + } + + + headers_.clear(); + + if (peer.isMember(KEY_HTTP_HEADERS)) + { + const Json::Value& h = peer[KEY_HTTP_HEADERS]; + if (h.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + Json::Value::Members keys = h.getMemberNames(); + for (size_t i = 0; i < keys.size(); i++) + { + const Json::Value& value = h[keys[i]]; + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + headers_[keys[i]] = value.asString(); + } + } + } + } + + + userProperties_.clear(); + + const Json::Value::Members members = peer.getMemberNames(); + + for (Json::Value::Members::const_iterator it = members.begin(); + it != members.end(); ++it) + { + if (!IsReservedKey(*it)) + { + switch (peer[*it].type()) + { + case Json::stringValue: + userProperties_[*it] = peer[*it].asString(); + break; + + case Json::booleanValue: + userProperties_[*it] = peer[*it].asBool() ? "1" : "0"; + break; + + case Json::intValue: + userProperties_[*it] = boost::lexical_cast(peer[*it].asInt()); + break; + + default: + throw OrthancException(ErrorCode_BadFileFormat, + "User-defined properties associated with a Web service must be strings: " + *it); + } + } + } + + + if (peer.isMember(KEY_TIMEOUT)) + { + timeout_ = SerializationToolbox::ReadUnsignedInteger(peer, KEY_TIMEOUT); + } + else + { + timeout_ = 0; + } + } + + + void WebServiceParameters::Unserialize(const Json::Value& peer) + { + try + { + if (peer.isArray()) + { + FromSimpleFormat(peer); + } + else if (peer.isObject()) + { + FromAdvancedFormat(peer); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + catch (OrthancException&) + { + throw; + } + catch (...) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void WebServiceParameters::ListHttpHeaders(std::set& target) const + { + target.clear(); + + for (Dictionary::const_iterator it = headers_.begin(); + it != headers_.end(); ++it) + { + target.insert(it->first); + } + } + + + bool WebServiceParameters::LookupHttpHeader(std::string& value, + const std::string& key) const + { + Dictionary::const_iterator found = headers_.find(key); + + if (found == headers_.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + void WebServiceParameters::AddUserProperty(const std::string& key, + const std::string& value) + { + if (IsReservedKey(key)) + { + throw OrthancException( + ErrorCode_ParameterOutOfRange, + "Cannot use this reserved key to name an user property: " + key); + } + else + { + userProperties_[key] = value; + } + } + + void WebServiceParameters::ClearUserProperties() + { + userProperties_.clear(); + } + + const WebServiceParameters::Dictionary &WebServiceParameters::GetUserProperties() const + { + return userProperties_; + } + + + void WebServiceParameters::ListUserProperties(std::set& target) const + { + target.clear(); + + for (Dictionary::const_iterator it = userProperties_.begin(); + it != userProperties_.end(); ++it) + { + target.insert(it->first); + } + } + + + bool WebServiceParameters::LookupUserProperty(std::string& value, + const std::string& key) const + { + Dictionary::const_iterator found = userProperties_.find(key); + + if (found == userProperties_.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + bool WebServiceParameters::GetBooleanUserProperty(const std::string& key, + bool defaultValue) const + { + Dictionary::const_iterator found = userProperties_.find(key); + + if (found == userProperties_.end()) + { + return defaultValue; + } + else + { + bool value; + if (SerializationToolbox::ParseBoolean(value, found->second)) + { + return value; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Bad value for a Boolean user property in the parameters " + "of a Web service: Property \"" + key + "\" equals: " + found->second); + } + } + } + + + bool WebServiceParameters::IsAdvancedFormatNeeded() const + { + return (!certificateFile_.empty() || + !certificateKeyFile_.empty() || + !certificateKeyPassword_.empty() || + pkcs11Enabled_ || + !headers_.empty() || + !userProperties_.empty() || + timeout_ != 0); + } + + + void WebServiceParameters::Serialize(Json::Value& value, + bool forceAdvancedFormat, + bool includePasswords) const + { + if (forceAdvancedFormat || + IsAdvancedFormatNeeded()) + { + value = Json::objectValue; + value[KEY_URL] = url_; + + if (!username_.empty() || + !password_.empty()) + { + value[KEY_USERNAME] = username_; + + if (includePasswords) + { + value[KEY_PASSWORD] = password_; + } + } + + if (!certificateFile_.empty()) + { + value[KEY_CERTIFICATE_FILE] = certificateFile_; + } + + if (!certificateKeyFile_.empty()) + { + value[KEY_CERTIFICATE_KEY_FILE] = certificateKeyFile_; + } + + if (!certificateKeyPassword_.empty() && + includePasswords) + { + value[KEY_CERTIFICATE_KEY_PASSWORD] = certificateKeyPassword_; + } + + value[KEY_PKCS11] = pkcs11Enabled_; + value[KEY_TIMEOUT] = timeout_; + + value[KEY_HTTP_HEADERS] = Json::objectValue; + for (Dictionary::const_iterator it = headers_.begin(); + it != headers_.end(); ++it) + { + value[KEY_HTTP_HEADERS][it->first] = it->second; + } + + for (Dictionary::const_iterator it = userProperties_.begin(); + it != userProperties_.end(); ++it) + { + value[it->first] = it->second; + } + } + else + { + value = Json::arrayValue; + value.append(url_); + + if (!username_.empty() || + !password_.empty()) + { + value.append(username_); + value.append(includePasswords ? password_ : ""); + } + } + } + + +#if ORTHANC_SANDBOXED == 0 + void WebServiceParameters::CheckClientCertificate() const + { + if (!certificateFile_.empty()) + { + if (!SystemToolbox::IsRegularFile(certificateFile_)) + { + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open certificate file: " + certificateFile_); + } + + if (!certificateKeyFile_.empty() && + !SystemToolbox::IsRegularFile(certificateKeyFile_)) + { + throw OrthancException(ErrorCode_InexistentFile, + "Cannot open key file: " + certificateKeyFile_); + } + } + } +#endif + + + void WebServiceParameters::FormatPublic(Json::Value& target) const + { + target = Json::objectValue; + + // Only return the public information identifying the destination. + // "Security"-related information such as passwords and HTTP + // headers are shown as "null" values. + target[KEY_URL] = url_; + + if (!username_.empty()) + { + target[KEY_USERNAME] = username_; + target[KEY_PASSWORD] = Json::nullValue; + } + + if (!certificateFile_.empty()) + { + target[KEY_CERTIFICATE_FILE] = certificateFile_; + target[KEY_CERTIFICATE_KEY_FILE] = Json::nullValue; + target[KEY_CERTIFICATE_KEY_PASSWORD] = Json::nullValue; + } + + target[KEY_PKCS11] = pkcs11Enabled_; + target[KEY_TIMEOUT] = timeout_; + + Json::Value headers = Json::arrayValue; + + for (Dictionary::const_iterator it = headers_.begin(); + it != headers_.end(); ++it) + { + // Only list the HTTP headers, not their value + headers.append(it->first); + } + + target[KEY_HTTP_HEADERS] = headers; + + for (Dictionary::const_iterator it = userProperties_.begin(); + it != userProperties_.end(); ++it) + { + target[it->first] = it->second; + } + } + + + void WebServiceParameters::SetTimeout(uint32_t seconds) + { + timeout_ = seconds; + } + + uint32_t WebServiceParameters::GetTimeout() const + { + return timeout_; + } + + bool WebServiceParameters::HasTimeout() const + { + return (timeout_ != 0); + } +} diff --git a/OrthancFramework/Sources/WebServiceParameters.h b/OrthancFramework/Sources/WebServiceParameters.h new file mode 100644 index 0000000..ef885bf --- /dev/null +++ b/OrthancFramework/Sources/WebServiceParameters.h @@ -0,0 +1,144 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "OrthancFramework.h" + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class ORTHANC_PUBLIC WebServiceParameters + { + public: + typedef std::map Dictionary; + + private: + std::string url_; + std::string username_; + std::string password_; + std::string certificateFile_; + std::string certificateKeyFile_; + std::string certificateKeyPassword_; + bool pkcs11Enabled_; + Dictionary headers_; + Dictionary userProperties_; + unsigned int timeout_; + + void FromSimpleFormat(const Json::Value& peer); + + void FromAdvancedFormat(const Json::Value& peer); + + public: + WebServiceParameters(); + + explicit WebServiceParameters(const Json::Value& serialized); + + const std::string& GetUrl() const; + + void SetUrl(const std::string& url); + + void ClearCredentials(); + + void SetCredentials(const std::string& username, + const std::string& password); + + const std::string& GetUsername() const; + + const std::string& GetPassword() const; + + void ClearClientCertificate(); + + void SetClientCertificate(const std::string& certificateFile, + const std::string& certificateKeyFile, + const std::string& certificateKeyPassword); + + const std::string& GetCertificateFile() const; + + const std::string& GetCertificateKeyFile() const; + + const std::string& GetCertificateKeyPassword() const; + + void SetPkcs11Enabled(bool enabled); + + bool IsPkcs11Enabled() const; + + void AddHttpHeader(const std::string& key, + const std::string& value); + + void ClearHttpHeaders(); + + const Dictionary& GetHttpHeaders() const; + + void ListHttpHeaders(std::set& target) const; + + bool LookupHttpHeader(std::string& value, + const std::string& key) const; + + void AddUserProperty(const std::string& key, + const std::string& value); + + void ClearUserProperties(); + + const Dictionary& GetUserProperties() const; + + void ListUserProperties(std::set& target) const; + + bool LookupUserProperty(std::string& value, + const std::string& key) const; + + bool GetBooleanUserProperty(const std::string& key, + bool defaultValue) const; + + bool IsAdvancedFormatNeeded() const; + + void Unserialize(const Json::Value& peer); + + void Serialize(Json::Value& value, + bool forceAdvancedFormat, + bool includePasswords) const; + +#if ORTHANC_SANDBOXED == 0 + void CheckClientCertificate() const; +#endif + + void FormatPublic(Json::Value& target) const; + + // Setting it to "0" will use "HttpClient::SetDefaultTimeout()" + void SetTimeout(uint32_t seconds); + + uint32_t GetTimeout() const; + + bool HasTimeout() const; + }; +} diff --git a/OrthancFramework/UnitTestsSources/CMakeLists.txt b/OrthancFramework/UnitTestsSources/CMakeLists.txt new file mode 100644 index 0000000..06c300a --- /dev/null +++ b/OrthancFramework/UnitTestsSources/CMakeLists.txt @@ -0,0 +1,119 @@ +# 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 Lesser 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + + +## +## This file is meant to be used only by ../SharedLibrary/CMakeLists.txt +## + +cmake_minimum_required(VERSION 2.8...4.0) +project(UnitTestsProject) + +set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +set(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") + +set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)") +set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test") + +if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) +else() + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) +endif() + +if (ORTHANC_FRAMEWORK_STATIC) + include_directories(${ORTHANC_FRAMEWORK_ROOT}/..) +else() + include(CheckIncludeFile) + include(CheckIncludeFileCXX) + + link_libraries(jsoncpp) + + include(FindLua) + if (NOT LUA_FOUND) + message(FATAL_ERROR "Please install the liblua-dev package") + endif() + include_directories(${LUA_INCLUDE_DIR}) + link_libraries(${LUA_LIBRARIES}) + + check_include_file(sqlite3.h HAVE_SQLITE_H) + if (NOT HAVE_SQLITE_H) + message(FATAL_ERROR "Please install the libsqlite3-dev package") + endif() + link_libraries(sqlite3) + + check_include_file_cxx(pugixml.hpp HAVE_PUGIXML_H) + if (NOT HAVE_PUGIXML_H) + message(FATAL_ERROR "Please install the libpugixml-dev package") + endif() + link_libraries(pugixml) + + find_package(Boost COMPONENTS filesystem thread system date_time iostreams locale regex) + if (NOT Boost_FOUND) + message(FATAL_ERROR "Unable to locate Boost on this system") + endif() + link_libraries(${Boost_LIBRARIES}) + + include(FindDCMTK NO_MODULE) + link_libraries(${DCMTK_LIBRARIES}) +endif() + +include(${CMAKE_SOURCE_DIR}/../Resources/CMake/DownloadOrthancFramework.cmake) +include(${CMAKE_SOURCE_DIR}/../Resources/CMake/GoogleTestConfiguration.cmake) + +add_definitions( + -DORTHANC_UNIT_TESTS_LINK_FRAMEWORK=1 + -DORTHANC_BUILD_UNIT_TESTS=1 # For "HierarchicalZipWriter" tests + ) + +add_executable(UnitTests + ${CMAKE_SOURCE_DIR}/SharedLibraryUnitTests.cpp + + ${CMAKE_SOURCE_DIR}/DicomMapTests.cpp + ${CMAKE_SOURCE_DIR}/FileStorageTests.cpp + ${CMAKE_SOURCE_DIR}/FrameworkTests.cpp + ${CMAKE_SOURCE_DIR}/FromDcmtkTests.cpp + ${CMAKE_SOURCE_DIR}/ImageProcessingTests.cpp + ${CMAKE_SOURCE_DIR}/ImageTests.cpp + ${CMAKE_SOURCE_DIR}/JobsTests.cpp + ${CMAKE_SOURCE_DIR}/JpegLosslessTests.cpp + ${CMAKE_SOURCE_DIR}/LoggingTests.cpp + ${CMAKE_SOURCE_DIR}/LuaTests.cpp + ${CMAKE_SOURCE_DIR}/MemoryCacheTests.cpp + ${CMAKE_SOURCE_DIR}/RestApiTests.cpp + ${CMAKE_SOURCE_DIR}/SQLiteChromiumTests.cpp + ${CMAKE_SOURCE_DIR}/SQLiteTests.cpp + ${CMAKE_SOURCE_DIR}/StreamTests.cpp + ${CMAKE_SOURCE_DIR}/ToolboxTests.cpp + ${CMAKE_SOURCE_DIR}/ZipTests.cpp + + ${AUTOGENERATED_SOURCES} + ${BOOST_SOURCES} + ${GOOGLE_TEST_SOURCES} + ) + +DefineSourceBasenameForTarget(UnitTests) + +target_link_libraries(UnitTests ${ORTHANC_FRAMEWORK_LIBRARIES}) + +install(TARGETS UnitTests + DESTINATION ${ORTHANC_FRAMEWORK_LIBDIR} + ) diff --git a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp new file mode 100644 index 0000000..10389ab --- /dev/null +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp @@ -0,0 +1,1525 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#if !defined(DCMTK_VERSION_NUMBER) +# error DCMTK_VERSION_NUMBER is not defined +#endif + +#include + +#include "../Sources/Compatibility.h" +#include "../Sources/OrthancException.h" +#include "../Sources/DicomFormat/DicomMap.h" +#include "../Sources/DicomFormat/DicomStreamReader.h" +#include "../Sources/DicomParsing/FromDcmtkBridge.h" +#include "../Sources/DicomParsing/ToDcmtkBridge.h" +#include "../Sources/DicomParsing/ParsedDicomFile.h" +#include "../Sources/DicomParsing/DicomWebJsonVisitor.h" + +#include +#include + +using namespace Orthanc; + + +namespace Orthanc +{ + // The namespace is necessary because of FRIEND_TEST + // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members + + class DicomMapMainTagsTests : public ::testing::Test + { + public: + DicomMapMainTagsTests() + { + } + + virtual void SetUp() ORTHANC_OVERRIDE + { + DicomMap::ResetDefaultMainDicomTags(); + } + + virtual void TearDown() ORTHANC_OVERRIDE + { + DicomMap::ResetDefaultMainDicomTags(); + } + }; + + TEST_F(DicomMapMainTagsTests, MainTags) + { + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study)); + + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID)); + + { + std::set s; + DicomMap::GetAllMainDicomTags(s); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + } + + { + std::set s; + DicomMap::GetMainDicomTags(s, ResourceType_Patient); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + } + + { + std::set s; + DicomMap::GetMainDicomTags(s, ResourceType_Study); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + } + + { + std::set s; + DicomMap::GetMainDicomTags(s, ResourceType_Series); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + } + + { + std::set s; + DicomMap::GetMainDicomTags(s, ResourceType_Instance); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + } + } + + TEST_F(DicomMapMainTagsTests, AddMainTags) + { + DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance); + + { + std::set s; + DicomMap::GetMainDicomTags(s, ResourceType_Instance); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_BITS_ALLOCATED)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + } + { + std::set s; + DicomMap::GetMainDicomTags(s, ResourceType_Series); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_BITS_ALLOCATED)); + } + + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_BITS_ALLOCATED)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance)); + + // adding the same tag should throw + ASSERT_THROW(DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance), OrthancException); + + } + + TEST_F(DicomMapMainTagsTests, Signatures) + { + std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Patient); + std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Study); + std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Series); + std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Instance); + + ASSERT_NE(defaultInstanceSignature, defaultPatientSignature); + ASSERT_NE(defaultSeriesSignature, defaultStudySignature); + ASSERT_NE(defaultSeriesSignature, defaultPatientSignature); + + std::string patientSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Patient); + std::string studySignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Study); + std::string seriesSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Series); + std::string instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance); + + // // at start, default and current signature should be equal !! This is not true anymore since we have added new MainDicomTags in 1.12.5 + // ASSERT_EQ(defaultPatientSignature, patientSignature); + // ASSERT_EQ(defaultStudySignature, studySignature); + // ASSERT_EQ(defaultSeriesSignature, seriesSignature); + // ASSERT_EQ(defaultInstanceSignature, instanceSignature); + + DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance); + instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance); + + ASSERT_NE(defaultInstanceSignature, instanceSignature); + } + +} + + +TEST(DicomMap, Tags) +{ + std::set s; + + DicomMap m; + m.GetTags(s); + ASSERT_EQ(0u, s.size()); + + ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME)); + ASSERT_FALSE(m.HasTag(0x0010, 0x0010)); + m.SetValue(0x0010, 0x0010, "PatientName", false); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(m.HasTag(0x0010, 0x0010)); + + m.GetTags(s); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin()); + + ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID)); + m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID", false); + ASSERT_TRUE(m.HasTag(0x0010, 0x0020)); + m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2", false); + ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).GetContent()); + + m.GetTags(s); + ASSERT_EQ(2u, s.size()); + + m.Remove(DICOM_TAG_PATIENT_ID); + ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException); + + m.GetTags(s); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin()); + + std::unique_ptr mm(m.Clone()); + ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).GetContent()); + + m.SetValue(DICOM_TAG_PATIENT_ID, "Hello", false); + ASSERT_THROW(mm->GetValue(DICOM_TAG_PATIENT_ID), OrthancException); + mm->CopyTagIfExists(m, DICOM_TAG_PATIENT_ID); + ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).GetContent()); + + DicomValue v; + ASSERT_TRUE(v.IsNull()); +} + + +TEST(DicomMap, FindTemplates) +{ + DicomMap m; + + DicomMap::SetupFindPatientTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + + DicomMap::SetupFindStudyTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_ACCESSION_NUMBER)); + + DicomMap::SetupFindSeriesTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + + DicomMap::SetupFindInstanceTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); +} + + + + +static void TestModule(ResourceType level, + DicomModule module) +{ + // REFERENCE: DICOM PS3.3 2015c - Information Object Definitions + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html + + std::set main; + DicomMap::GetMainDicomTags(main, level); + + std::set moduleTags; + DicomTag::AddTagsForModule(moduleTags, module); + + // The main dicom tags are a subset of the module + for (std::set::const_iterator it = main.begin(); it != main.end(); ++it) + { + bool ok = moduleTags.find(*it) != moduleTags.end(); + + // Exceptions for the Study level + if (level == ResourceType_Study && + (*it == DicomTag(0x0008, 0x0080) || /* InstitutionName, from Visit identification module, related to Visit */ + *it == DicomTag(0x0032, 0x1032) || /* RequestingPhysician, from Imaging Service Request module, related to Study */ + *it == DicomTag(0x0008, 0x0201) || /* TimezoneOffsetFromUTC */ + *it == DicomTag(0x0032, 0x1060))) /* RequestedProcedureDescription, from Requested Procedure module, related to Study */ + { + ok = true; + } + + // Exceptions for the Series level + if (level == ResourceType_Series && + (*it == DicomTag(0x0008, 0x0070) || /* Manufacturer, from General Equipment Module */ + *it == DicomTag(0x0008, 0x1010) || /* StationName, from General Equipment Module */ + *it == DicomTag(0x0018, 0x0024) || /* SequenceName, from MR Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0018, 0x1090) || /* CardiacNumberOfImages, from MR Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0020, 0x0037) || /* ImageOrientationPatient, from Image Plane Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0020, 0x0105) || /* NumberOfTemporalPositions, from MR Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0020, 0x1002) || /* ImagesInAcquisition, from General Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0054, 0x0081) || /* NumberOfSlices, from PET Series module */ + *it == DicomTag(0x0054, 0x0101) || /* NumberOfTimeSlices, from PET Series module */ + *it == DicomTag(0x0054, 0x1000) || /* SeriesType, from PET Series module */ + *it == DicomTag(0x0018, 0x1400) || /* AcquisitionDeviceProcessingDescription, from CR/X-Ray/DX/WholeSlideMicro Image (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0008, 0x0201) || /* TimezoneOffsetFromUTC */ + *it == DicomTag(0x0018, 0x0010))) /* ContrastBolusAgent, from Contrast/Bolus module (SIMPLIFICATION => Series) */ + { + ok = true; + } + + // Exceptions for the Instance level + if (level == ResourceType_Instance && + (*it == DicomTag(0x0020, 0x0012) || /* AccessionNumber, from General Image module */ + *it == DicomTag(0x0054, 0x1330) || /* ImageIndex, from PET Image module */ + *it == DicomTag(0x0020, 0x0100) || /* TemporalPositionIdentifier, from MR Image module */ + *it == DicomTag(0x0028, 0x0008) || /* NumberOfFrames, from Multi-frame module attributes, related to Image */ + *it == DicomTag(0x0020, 0x0032) || /* ImagePositionPatient, from Image Plan module, related to Image */ + *it == DicomTag(0x0020, 0x0037) || /* ImageOrientationPatient, from Image Plane Module (Orthanc 1.4.2) */ + *it == DicomTag(0x0020, 0x4000))) /* ImageComments, from General Image module */ + { + ok = true; + } + + if (!ok) + { + std::cout << it->Format() << ": " << FromDcmtkBridge::GetTagName(*it, "") + << " not expected at level " << EnumerationToString(level) << std::endl; + } + + EXPECT_TRUE(ok); + } +} + + +TEST(DicomMap, Modules) +{ + TestModule(ResourceType_Patient, DicomModule_Patient); + TestModule(ResourceType_Study, DicomModule_Study); + TestModule(ResourceType_Series, DicomModule_Series); // TODO + TestModule(ResourceType_Instance, DicomModule_Instance); +} + + +TEST(DicomMap, Parse) +{ + DicomMap m; + float f; + double d; + int32_t i; + int64_t j; + uint32_t k; + uint64_t l; + unsigned int ui; + std::string s; + + m.SetValue(DICOM_TAG_PATIENT_NAME, " ", false); // Empty value + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + + m.SetValue(DICOM_TAG_PATIENT_NAME, "0", true); // Binary value + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + + ASSERT_FALSE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, true)); + ASSERT_EQ("0", s); + + + // 2**31-1 + m.SetValue(DICOM_TAG_PATIENT_NAME, "2147483647", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + ASSERT_FLOAT_EQ(2147483647.0f, f); + ASSERT_DOUBLE_EQ(2147483647.0, d); + ASSERT_EQ(2147483647, i); + ASSERT_EQ(2147483647ll, j); + ASSERT_EQ(2147483647u, k); + ASSERT_EQ(2147483647ull, l); + + // Test shortcuts + m.SetValue(DICOM_TAG_PATIENT_NAME, "42", false); + ASSERT_TRUE(m.ParseFloat(f, DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(m.ParseDouble(d, DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(m.ParseInteger32(i, DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(m.ParseInteger64(j, DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(m.ParseUnsignedInteger32(k, DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(m.ParseUnsignedInteger64(l, DICOM_TAG_PATIENT_NAME)); + ASSERT_FLOAT_EQ(42.0f, f); + ASSERT_DOUBLE_EQ(42.0, d); + ASSERT_EQ(42, i); + ASSERT_EQ(42ll, j); + ASSERT_EQ(42u, k); + ASSERT_EQ(42ull, l); + + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); + ASSERT_EQ("42", s); + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, true)); + ASSERT_EQ("42", s); + + + // 2**31 + m.SetValue(DICOM_TAG_PATIENT_NAME, "2147483648", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + ASSERT_FLOAT_EQ(2147483648.0f, f); + ASSERT_DOUBLE_EQ(2147483648.0, d); + ASSERT_EQ(2147483648ll, j); + ASSERT_EQ(2147483648u, k); + ASSERT_EQ(2147483648ull, l); + + // 2**32-1 + m.SetValue(DICOM_TAG_PATIENT_NAME, "4294967295", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + ASSERT_FLOAT_EQ(4294967295.0f, f); + ASSERT_DOUBLE_EQ(4294967295.0, d); + ASSERT_EQ(4294967295ll, j); + ASSERT_EQ(4294967295u, k); + ASSERT_EQ(4294967295ull, l); + + // 2**32 + m.SetValue(DICOM_TAG_PATIENT_NAME, "4294967296", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + ASSERT_FLOAT_EQ(4294967296.0f, f); + ASSERT_DOUBLE_EQ(4294967296.0, d); + ASSERT_EQ(4294967296ll, j); + ASSERT_EQ(4294967296ull, l); + + m.SetValue(DICOM_TAG_PATIENT_NAME, "-1", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + ASSERT_FLOAT_EQ(-1.0f, f); + ASSERT_DOUBLE_EQ(-1.0, d); + ASSERT_EQ(-1, i); + ASSERT_EQ(-1ll, j); + + // -2**31 + m.SetValue(DICOM_TAG_PATIENT_NAME, "-2147483648", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + ASSERT_FLOAT_EQ(-2147483648.0f, f); + ASSERT_DOUBLE_EQ(-2147483648.0, d); + ASSERT_EQ(static_cast(-2147483648ll), i); + ASSERT_EQ(-2147483648ll, j); + + // -2**31 - 1 + m.SetValue(DICOM_TAG_PATIENT_NAME, "-2147483649", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseFloat(f)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseDouble(d)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger32(i)); + ASSERT_TRUE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseInteger64(j)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger32(k)); + ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).ParseUnsignedInteger64(l)); + ASSERT_FLOAT_EQ(-2147483649.0f, f); + ASSERT_DOUBLE_EQ(-2147483649.0, d); + ASSERT_EQ(-2147483649ll, j); + + + // "800\0" in US COLMUNS tag + m.SetValue(DICOM_TAG_COLUMNS, "800\0", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(ui)); + ASSERT_EQ(800u, ui); + m.SetValue(DICOM_TAG_COLUMNS, "800", false); + ASSERT_TRUE(m.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(ui)); + ASSERT_EQ(800u, ui); +} + + +TEST(DicomMap, Serialize) +{ + Json::Value s; + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_NAME, "Hello", false); + m.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "Binary", true); + m.SetNullValue(DICOM_TAG_SERIES_DESCRIPTION); + m.Serialize(s); + } + + { + DicomMap m; + m.Unserialize(s); + + const DicomValue* v = m.TestAndGetValue(DICOM_TAG_ACCESSION_NUMBER); + ASSERT_TRUE(v == NULL); + + v = m.TestAndGetValue(DICOM_TAG_PATIENT_NAME); + ASSERT_TRUE(v != NULL); + ASSERT_FALSE(v->IsNull()); + ASSERT_FALSE(v->IsBinary()); + ASSERT_EQ("Hello", v->GetContent()); + + v = m.TestAndGetValue(DICOM_TAG_STUDY_DESCRIPTION); + ASSERT_TRUE(v != NULL); + ASSERT_FALSE(v->IsNull()); + ASSERT_TRUE(v->IsBinary()); + ASSERT_EQ("Binary", v->GetContent()); + + v = m.TestAndGetValue(DICOM_TAG_SERIES_DESCRIPTION); + ASSERT_TRUE(v != NULL); + ASSERT_TRUE(v->IsNull()); + ASSERT_FALSE(v->IsBinary()); + ASSERT_THROW(v->GetContent(), OrthancException); + } +} + + + +TEST(DicomMap, ExtractMainDicomTags) +{ + DicomMap b; + b.SetValue(DICOM_TAG_PATIENT_NAME, "E", false); + ASSERT_TRUE(b.HasOnlyMainDicomTags()); + + { + DicomMap a; + a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false); + a.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "B", false); + a.SetValue(DICOM_TAG_SERIES_DESCRIPTION, "C", false); + a.SetValue(DICOM_TAG_NUMBER_OF_FRAMES, "D", false); + a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false); + ASSERT_FALSE(a.HasOnlyMainDicomTags()); + b.ExtractMainDicomTags(a); + } + + ASSERT_EQ(4u, b.GetSize()); + ASSERT_EQ("A", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent()); + ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent()); + ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent()); + ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent()); + ASSERT_FALSE(b.HasTag(DICOM_TAG_SLICE_THICKNESS)); + ASSERT_TRUE(b.HasOnlyMainDicomTags()); + + b.SetValue(DICOM_TAG_PATIENT_NAME, "G", false); + + { + DicomMap a; + a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false); + a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false); + ASSERT_FALSE(a.HasOnlyMainDicomTags()); + b.Merge(a); + } + + ASSERT_EQ(5u, b.GetSize()); + ASSERT_EQ("G", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent()); + ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent()); + ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent()); + ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent()); + ASSERT_EQ("F", b.GetValue(DICOM_TAG_SLICE_THICKNESS).GetContent()); + ASSERT_FALSE(b.HasOnlyMainDicomTags()); +} + + +TEST(DicomMap, ComputedTags) +{ + { + std::set tags; + + ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + } + + { + std::set tags; + tags.insert(DICOM_TAG_ACCESSION_NUMBER); + + ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + } + + { + std::set tags; + tags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + tags.insert(DICOM_TAG_RETRIEVE_URL); + + ASSERT_TRUE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + } + + { + std::set tags; + tags.insert(DICOM_TAG_ACCESSION_NUMBER); + tags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + + ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + } + +} + +TEST(DicomMap, RemoveBinary) +{ + DicomMap b; + b.SetValue(DICOM_TAG_PATIENT_NAME, "A", false); + b.SetValue(DICOM_TAG_PATIENT_ID, "B", true); + b.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, DicomValue()); // NULL + b.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, DicomValue("C", false)); + b.SetValue(DICOM_TAG_SOP_INSTANCE_UID, DicomValue("D", true)); + + b.RemoveBinaryTags(); + + std::string s; + ASSERT_EQ(2u, b.GetSize()); + ASSERT_TRUE(b.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); ASSERT_EQ("A", s); + ASSERT_TRUE(b.LookupStringValue(s, DICOM_TAG_SERIES_INSTANCE_UID, false)); ASSERT_EQ("C", s); +} + + +TEST(DicomMap, FromDicomAsJsonAndSequences) +{ + DicomMap m; + std::string jsonFullString = "{" + "\"0008,1090\" : " + "{" + "\"Name\" : \"ManufacturerModelName\"," + "\"Type\" : \"String\"," + "\"Value\" : \"MyModel\"" + "}," + "\"0008,1111\" : " + "{" + "\"Name\" : \"ReferencedPerformedProcedureStepSequence\"," + "\"Type\" : \"Sequence\"," + "\"Value\" : " + "[" + "{" + "\"0008,1150\" : " + "{" + "\"Name\" : \"ReferencedSOPClassUID\"," + "\"Type\" : \"String\"," + "\"Value\" : \"1.2.4\"" + "}," + "\"0008,1155\" : " + "{" + "\"Name\" : \"ReferencedSOPInstanceUID\"," + "\"Type\" : \"String\"," + "\"Value\" : \"1.2.3\"" + "}" + "}" + "]" + "}}"; + + Json::Value parsedJson; + bool ret = Toolbox::ReadJson(parsedJson, jsonFullString); + + m.FromDicomAsJson(parsedJson, false /* append */, true /* parseSequences*/); + ASSERT_TRUE(ret); + + ASSERT_TRUE(m.HasTag(DicomTag(0x0008, 0x1090))); + ASSERT_EQ("MyModel", m.GetValue(0x0008,0x1090).GetContent()); + + ASSERT_TRUE(m.HasTag(DicomTag(0x0008, 0x1111))); + const Json::Value& jsonSequence = m.GetValue(0x0008, 0x1111).GetSequenceContent(); + ASSERT_EQ("ReferencedSOPClassUID", jsonSequence[0]["0008,1150"]["Name"].asString()); + + {// serialize to human dicomAsJson + Json::Value dicomAsJson = Json::objectValue; + FromDcmtkBridge::ToJson(dicomAsJson, m, DicomToJsonFormat_Human); + // printf("%s", dicomAsJson.toStyledString().c_str()); + + ASSERT_TRUE(dicomAsJson.isMember("ManufacturerModelName")); + ASSERT_TRUE(dicomAsJson.isMember("ReferencedPerformedProcedureStepSequence")); + ASSERT_TRUE(dicomAsJson["ReferencedPerformedProcedureStepSequence"][0].isMember("ReferencedSOPClassUID")); + ASSERT_EQ("1.2.4", dicomAsJson["ReferencedPerformedProcedureStepSequence"][0]["ReferencedSOPClassUID"].asString()); + } + + {// serialize to full dicomAsJson + Json::Value dicomAsJson = Json::objectValue; + FromDcmtkBridge::ToJson(dicomAsJson, m, DicomToJsonFormat_Full); + // printf("%s", dicomAsJson.toStyledString().c_str()); + + ASSERT_TRUE(dicomAsJson.isMember("0008,1090")); + ASSERT_TRUE(dicomAsJson.isMember("0008,1111")); + ASSERT_TRUE(dicomAsJson["0008,1111"]["Value"][0].isMember("0008,1150")); + ASSERT_EQ("1.2.4", dicomAsJson["0008,1111"]["Value"][0]["0008,1150"]["Value"].asString()); + ASSERT_EQ("MyModel", dicomAsJson["0008,1090"]["Value"].asString()); + } + + {// serialize to short dicomAsJson + Json::Value dicomAsJson = Json::objectValue; + FromDcmtkBridge::ToJson(dicomAsJson, m, DicomToJsonFormat_Short); + // printf("%s", dicomAsJson.toStyledString().c_str()); + + ASSERT_TRUE(dicomAsJson.isMember("0008,1090")); + ASSERT_TRUE(dicomAsJson.isMember("0008,1111")); + ASSERT_TRUE(dicomAsJson["0008,1111"][0].isMember("0008,1150")); + ASSERT_EQ("1.2.4", dicomAsJson["0008,1111"][0]["0008,1150"].asString()); + ASSERT_EQ("MyModel", dicomAsJson["0008,1090"].asString()); + } + + {// extract sequence + DicomMap sequencesOnly; + m.ExtractSequences(sequencesOnly); + + ASSERT_EQ(1u, sequencesOnly.GetSize()); + ASSERT_TRUE(sequencesOnly.HasTag(0x0008, 0x1111)); + ASSERT_TRUE(sequencesOnly.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150")); + + // copy sequence + DicomMap sequencesCopy; + sequencesCopy.SetValue(0x0008, 0x1111, sequencesOnly.GetValue(0x0008, 0x1111)); + + ASSERT_EQ(1u, sequencesCopy.GetSize()); + ASSERT_TRUE(sequencesCopy.HasTag(0x0008, 0x1111)); + ASSERT_TRUE(sequencesCopy.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150")); + } +} + +TEST(DicomMap, ExtractSummary) +{ + Json::Value v = Json::objectValue; + v["PatientName"] = "Hello"; + v["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + + { + Json::Value a = Json::arrayValue; + + { + Json::Value item = Json::objectValue; + item["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + item["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.719"; + a.append(item); + } + + { + Json::Value item = Json::objectValue; + item["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; // ReferencedSOPClassUID + item["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.726"; + a.append(item); + } + + v["ReferencedImageSequence"] = a; + } + + { + Json::Value a = Json::arrayValue; + + { + Json::Value item = Json::objectValue; + item["StudyInstanceUID"] = "1.2.840.113704.1.111.7016.1342451220.40"; + + { + Json::Value b = Json::arrayValue; + + { + Json::Value c = Json::objectValue; + c["CodeValue"] = "122403"; + c["0008,103e"] = "WORLD"; // Series description + b.append(c); + } + + item["PurposeOfReferenceCodeSequence"] = b; + } + + a.append(item); + } + + v["RelatedSeriesSequence"] = a; + } + + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + DicomMap summary; + std::set ignoreTagLength; + dicom->ExtractDicomSummary(summary, 256, ignoreTagLength); + + ASSERT_TRUE(summary.HasTag(0x0008, 0x1140)); + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", summary.GetValue(0x0008, 0x1140).GetSequenceContent()[0]["0008,1150"]["Value"].asString()); +} + + + +TEST(DicomWebJson, Multiplicity) +{ + // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.4.html + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "SB1^SB2^SB3^SB4^SB5"); + dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1\\2.3\\4"); + dicom.ReplacePlainString(DICOM_TAG_IMAGE_POSITION_PATIENT, ""); + dicom.ReplacePlainString(DICOM_TAG_PIXEL_SPACING, "0,143\\0,143"); // seen in https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195 + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + { + const Json::Value& tag = visitor.GetResult() ["00200037"]; // ImageOrientationPatient + const Json::Value& value = tag["Value"]; + + ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString()); + ASSERT_EQ(2u, tag.getMemberNames().size()); + ASSERT_EQ(3u, value.size()); + ASSERT_EQ(Json::stringValue, value[1].type()); // since Orthanc 1.12.5, this is now stored as a string + ASSERT_EQ("1", value[0].asString()); + ASSERT_EQ("2.3", value[1].asString()); + ASSERT_EQ("4", value[2].asString()); + } + + { + const Json::Value& tag = visitor.GetResult() ["00200032"]; // ImagePositionPatient + ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString()); + ASSERT_EQ(1u, tag.getMemberNames().size()); + } + + { + const Json::Value& tag = visitor.GetResult() ["00280030"]; // PixelSpacing + const Json::Value& value = tag["Value"]; + + ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString()); + ASSERT_EQ(2u, value.size()); + ASSERT_EQ("0.143", value[0].asString()); + ASSERT_EQ("0.143", value[1].asString()); + } + + std::string xml; + visitor.FormatXml(xml); + + { + DicomMap m; + m.FromDicomWeb(visitor.GetResult()); + ASSERT_EQ(4u, m.GetSize()); + + std::string s; + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); + ASSERT_EQ("SB1^SB2^SB3^SB4^SB5", s); + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_POSITION_PATIENT, false)); + ASSERT_TRUE(s.empty()); + + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)); + + std::vector v; + Toolbox::TokenizeString(v, s, '\\'); + ASSERT_FLOAT_EQ(1.0f, boost::lexical_cast(v[0])); + ASSERT_FLOAT_EQ(2.3f, boost::lexical_cast(v[1])); + ASSERT_FLOAT_EQ(4.0f, boost::lexical_cast(v[2])); + } +} + + +TEST(DicomWebJson, NullValue) +{ + // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.5.html + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1.5\\\\\\2.5"); + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + { + const Json::Value& tag = visitor.GetResult() ["00200037"]; + const Json::Value& value = tag["Value"]; + + ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString()); + ASSERT_EQ(2u, tag.getMemberNames().size()); + ASSERT_EQ(4u, value.size()); + ASSERT_EQ(Json::stringValue, value[0].type()); + ASSERT_EQ(Json::nullValue, value[1].type()); + ASSERT_EQ(Json::nullValue, value[2].type()); + ASSERT_EQ(Json::stringValue, value[3].type()); + ASSERT_EQ("1.5", value[0].asString()); + ASSERT_EQ("2.5", value[3].asString()); + } + + std::string xml; + visitor.FormatXml(xml); + + { + DicomMap m; + m.FromDicomWeb(visitor.GetResult()); + ASSERT_EQ(1u, m.GetSize()); + + std::string s; + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)); + + std::vector v; + Toolbox::TokenizeString(v, s, '\\'); + ASSERT_FLOAT_EQ(1.5f, boost::lexical_cast(v[0])); + ASSERT_TRUE(v[1].empty()); + ASSERT_TRUE(v[2].empty()); + ASSERT_FLOAT_EQ(2.5f, boost::lexical_cast(v[3])); + } +} + + +TEST(DicomWebJson, PixelSpacing) +{ + // Test related to locales: Make sure that decimal separator is + // correctly handled (dot "." vs comma ",") + ParsedDicomFile source(false); + source.ReplacePlainString(DICOM_TAG_PIXEL_SPACING, "1.5\\1.3"); + + DicomWebJsonVisitor visitor; + source.Apply(visitor); + + DicomMap target; + target.FromDicomWeb(visitor.GetResult()); + + ASSERT_EQ("DS", visitor.GetResult() ["00280030"]["vr"].asString()); + ASSERT_EQ("1.5", visitor.GetResult() ["00280030"]["Value"][0].asString()); + ASSERT_EQ("1.3", visitor.GetResult() ["00280030"]["Value"][1].asString()); + + std::string s; + ASSERT_TRUE(target.LookupStringValue(s, DICOM_TAG_PIXEL_SPACING, false)); + ASSERT_EQ(s, "1.5\\1.3"); +} + + +TEST(DicomMap, MainTagNames) +{ + ASSERT_EQ(3, ResourceType_Instance - ResourceType_Patient); + + for (int i = ResourceType_Patient; i <= ResourceType_Instance; i++) + { + ResourceType level = static_cast(i); + + std::set tags; + DicomMap::GetMainDicomTags(tags, level); + + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + DicomMap a; + a.SetValue(*it, "TEST", false); + + Json::Value json; + a.DumpMainDicomTags(json, level); + + ASSERT_EQ(Json::objectValue, json.type()); + ASSERT_EQ(1u, json.getMemberNames().size()); + + std::string name = json.getMemberNames() [0]; + EXPECT_EQ(name, FromDcmtkBridge::GetTagName(*it, "")); + + std::string main = FromDcmtkBridge::GetTagName(*it, ""); + if (!main.empty()) + { + ASSERT_EQ(main, name); + } + } + } +} + + + +TEST(DicomTag, Comparisons) +{ + DicomTag a(0x0000, 0x0000); + DicomTag b(0x0010, 0x0010); + DicomTag c(0x0010, 0x0020); + DicomTag d(0x0020, 0x0000); + + // operator==() + ASSERT_TRUE(a == a); + ASSERT_FALSE(a == b); + + // operator!=() + ASSERT_FALSE(a != a); + ASSERT_TRUE(a != b); + + // operator<=() + ASSERT_TRUE(a <= a); + ASSERT_TRUE(a <= b); + ASSERT_TRUE(a <= c); + ASSERT_TRUE(a <= d); + + ASSERT_FALSE(b <= a); + ASSERT_TRUE(b <= b); + ASSERT_TRUE(b <= c); + ASSERT_TRUE(b <= d); + + ASSERT_FALSE(c <= a); + ASSERT_FALSE(c <= b); + ASSERT_TRUE(c <= c); + ASSERT_TRUE(c <= d); + + ASSERT_FALSE(d <= a); + ASSERT_FALSE(d <= b); + ASSERT_FALSE(d <= c); + ASSERT_TRUE(d <= d); + + // operator<() + ASSERT_FALSE(a < a); + ASSERT_TRUE(a < b); + ASSERT_TRUE(a < c); + ASSERT_TRUE(a < d); + + ASSERT_FALSE(b < a); + ASSERT_FALSE(b < b); + ASSERT_TRUE(b < c); + ASSERT_TRUE(b < d); + + ASSERT_FALSE(c < a); + ASSERT_FALSE(c < b); + ASSERT_FALSE(c < c); + ASSERT_TRUE(c < d); + + ASSERT_FALSE(d < a); + ASSERT_FALSE(d < b); + ASSERT_FALSE(d < c); + ASSERT_FALSE(d < d); + + // operator>=() + ASSERT_TRUE(a >= a); + ASSERT_FALSE(a >= b); + ASSERT_FALSE(a >= c); + ASSERT_FALSE(a >= d); + + ASSERT_TRUE(b >= a); + ASSERT_TRUE(b >= b); + ASSERT_FALSE(b >= c); + ASSERT_FALSE(b >= d); + + ASSERT_TRUE(c >= a); + ASSERT_TRUE(c >= b); + ASSERT_TRUE(c >= c); + ASSERT_FALSE(c >= d); + + ASSERT_TRUE(d >= a); + ASSERT_TRUE(d >= b); + ASSERT_TRUE(d >= c); + ASSERT_TRUE(d >= d); + + // operator>() + ASSERT_FALSE(a > a); + ASSERT_FALSE(a > b); + ASSERT_FALSE(a > c); + ASSERT_FALSE(a > d); + + ASSERT_TRUE(b > a); + ASSERT_FALSE(b > b); + ASSERT_FALSE(b > c); + ASSERT_FALSE(b > d); + + ASSERT_TRUE(c > a); + ASSERT_TRUE(c > b); + ASSERT_FALSE(c > c); + ASSERT_FALSE(c > d); + + ASSERT_TRUE(d > a); + ASSERT_TRUE(d > b); + ASSERT_TRUE(d > c); + ASSERT_FALSE(d > d); +} + +TEST(ParsedDicomFile, canIncludeXsVrTags) +{ + Json::Value tags; + tags["0028,0034"] = "1\\1"; // PixelAspectRatio + tags["0028,1101"] = "256\\0\\16"; // RedPaletteColorLookupTableDescriptor which is declared as xs VR in dicom.dic + + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(tags, DicomFromJsonFlags_DecodeDataUriScheme, "")); + // simply make sure it does not throw ! +} + + +TEST(DicomMap, SetupFindTemplates) +{ + /** + * The templates for C-FIND must be common to all the Orthanc + * servers, and must not be altered by the "ExtraMainDicomTags" + * configuration option that was introduced in Orthanc 1.11.0. + **/ + + { + DicomMap m; + m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false); + m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false); + + DicomMap::SetupFindPatientTemplate(m); + std::set tags; + m.GetTags(tags); + + // This corresponds to the values of DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS + ASSERT_EQ(5u, tags.size()); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false)); + + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_OTHER_PATIENT_IDS, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_BIRTH_DATE, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_NAME, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_SEX, "nope", false)); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false); + m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false); + + DicomMap::SetupFindStudyTemplate(m); + std::set tags; + m.GetTags(tags); + + // This corresponds to the values of DEFAULT_STUDY_MAIN_DICOM_TAGS + ASSERT_EQ(8u, tags.size()); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACCESSION_NUMBER, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_INSTANCE_UID, "nope", false)); + + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_REFERRING_PHYSICIAN_NAME, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_DATE, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_DESCRIPTION, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_ID, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_TIME, "nope", false)); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false); + m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false); + + DicomMap::SetupFindSeriesTemplate(m); + std::set tags; + m.GetTags(tags); + + // This corresponds to the values of DEFAULT_SERIES_MAIN_DICOM_TAGS + ASSERT_EQ(13u, tags.size()); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACCESSION_NUMBER, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_INSTANCE_UID, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_INSTANCE_UID, "nope", false)); + + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_BODY_PART_EXAMINED, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_MODALITY, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_OPERATOR_NAME, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PROTOCOL_NAME, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_DATE, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_DESCRIPTION, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_NUMBER, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_TIME, "nope", false)); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_ENCAPSULATED_DOCUMENT, "nope", false); + m.SetValue(DICOM_TAG_PATIENT_ID, "patient_id", false); + + DicomMap::SetupFindInstanceTemplate(m); + std::set tags; + m.GetTags(tags); + + // This corresponds to the values of DEFAULT_INSTANCE_MAIN_DICOM_TAGS + ASSERT_EQ(15u, tags.size()); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACCESSION_NUMBER, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_STUDY_INSTANCE_UID, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SERIES_INSTANCE_UID, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_SOP_INSTANCE_UID, "nope", false)); + + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_ACQUISITION_NUMBER, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_COMMENTS, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_INDEX, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_IMAGE_POSITION_PATIENT, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_INSTANCE_CREATION_DATE, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_INSTANCE_CREATION_TIME, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_INSTANCE_NUMBER, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_NUMBER_OF_FRAMES, "nope", false)); + ASSERT_EQ("", m.GetStringValue(DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, "nope", false)); + } +} + + +#if ORTHANC_SANDBOXED != 1 + +#include "../Sources/SystemToolbox.h" + +TEST(DicomMap, DISABLED_ParseDicomMetaInformation) +{ + static const std::string PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/"; + + std::map f; + f.insert(std::make_pair(PATH + "../ColorTestMalaterre.dcm", DicomTransferSyntax_LittleEndianImplicit)); // 1.2.840.10008.1.2 + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.1.dcm", DicomTransferSyntax_LittleEndianExplicit)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.2.dcm", DicomTransferSyntax_BigEndianExplicit)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.50.dcm", DicomTransferSyntax_JPEGProcess1)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.51.dcm", DicomTransferSyntax_JPEGProcess2_4)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.57.dcm", DicomTransferSyntax_JPEGProcess14)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.70.dcm", DicomTransferSyntax_JPEGProcess14SV1)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.80.dcm", DicomTransferSyntax_JPEGLSLossless)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.81.dcm", DicomTransferSyntax_JPEGLSLossy)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.90.dcm", DicomTransferSyntax_JPEG2000LosslessOnly)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.4.91.dcm", DicomTransferSyntax_JPEG2000)); + f.insert(std::make_pair(PATH + "1.2.840.10008.1.2.5.dcm", DicomTransferSyntax_RLELossless)); + + for (std::map::const_iterator it = f.begin(); it != f.end(); ++it) + { + printf("\n== %s ==\n\n", it->first.c_str()); + + std::string dicom; + SystemToolbox::ReadFile(dicom, it->first, false); + + DicomMap d; + ASSERT_TRUE(DicomMap::ParseDicomMetaInformation(d, dicom.c_str(), dicom.size())); + d.Print(stdout); + + std::string s; + ASSERT_TRUE(d.LookupStringValue(s, DICOM_TAG_TRANSFER_SYNTAX_UID, false)); + + DicomTransferSyntax ts; + ASSERT_TRUE(LookupTransferSyntax(ts, s)); + ASSERT_EQ(ts, it->second); + } +} + + +namespace +{ + class V : public DicomStreamReader::IVisitor + { + private: + DicomMap map_; + uint64_t pixelDataOffset_; + + public: + V() : + pixelDataOffset_(0) + { + } + + const DicomMap& GetDicomMap() const + { + return map_; + } + + virtual void VisitMetaHeaderTag(const DicomTag& tag, + const ValueRepresentation& vr, + const std::string& value) ORTHANC_OVERRIDE + { + std::cout << "Header: " << tag.Format() << " [" << Toolbox::ConvertToAscii(value).c_str() << "] (" << value.size() << ")" << std::endl; + } + + virtual void VisitTransferSyntax(DicomTransferSyntax transferSyntax) ORTHANC_OVERRIDE + { + printf("TRANSFER SYNTAX: %s\n", GetTransferSyntaxUid(transferSyntax)); + } + + virtual bool VisitDatasetTag(const DicomTag& tag, + const ValueRepresentation& vr, + const std::string& value, + bool isLittleEndian, + uint64_t fileOffset) ORTHANC_OVERRIDE + { + if (!isLittleEndian) + printf("** "); + + if (tag == DICOM_TAG_PIXEL_DATA) + { + std::cout << "Dataset: " << tag.Format() << " " << EnumerationToString(vr) + << " [PIXEL] (" << value.size() << "), offset: " << std::hex << fileOffset << std::dec << std::endl; + pixelDataOffset_ = fileOffset; + return false; + } + else + { + std::cout << "Dataset: " << tag.Format() << " " << EnumerationToString(vr) + << " [" << Toolbox::ConvertToAscii(value).c_str() << "] (" << value.size() + << "), offset: " << std::hex << fileOffset << std::dec << std::endl; + } + + map_.SetValue(tag, value, Toolbox::IsAsciiString(value)); + + return true; + } + + uint64_t GetPixelDataOffset() const + { + return pixelDataOffset_; + } + }; +} + + + +TEST(DicomStreamReader, DISABLED_Tutu) +{ + static const std::string PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/"; + + typedef boost::tuple Source; + typedef std::list Sources; + + // $ ~/Subversion/orthanc-tests/Tests/GetPixelDataVR.py ~/Subversion/orthanc-tests/Database/ColorTestMalaterre.dcm ~/Subversion/orthanc-tests/Database/ColorTestImageJ.dcm ~/Subversion/orthanc-tests/Database/Knee/T1/IM-0001-0001.dcm ~/Subversion/orthanc-tests/Database/TransferSyntaxes/*.dcm + + Sources sources; + sources.push_back(Source(PATH + "../ColorTestMalaterre.dcm", 0x03a0u, ValueRepresentation_Unknown)); // This file has strange VR + sources.push_back(Source(PATH + "../ColorTestImageJ.dcm", 0x00924, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "../Knee/T1/IM-0001-0001.dcm", 0x00c78, ValueRepresentation_OtherWord)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.1.dcm", 0x037cu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.2.dcm", 0x03e8u, ValueRepresentation_OtherByte)); // Big Endian + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.50.dcm", 0x04acu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.51.dcm", 0x072cu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.57.dcm", 0x0620u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.70.dcm", 0x065au, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.80.dcm", 0x0b46u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.81.dcm", 0x073eu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.90.dcm", 0x0b66u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.91.dcm", 0x19b8u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.5.dcm", 0x0b0au, ValueRepresentation_OtherByte)); + + { + std::string dicom; + + uint64_t offset; + ValueRepresentation vr; + + // Not a DICOM image + SystemToolbox::ReadFile(dicom, PATH + "1.2.840.10008.1.2.4.50.png", false); + ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom)); + + // Image without valid DICOM preamble + SystemToolbox::ReadFile(dicom, PATH + "1.2.840.10008.1.2.dcm", false); + ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom)); + } + + for (Sources::const_iterator it = sources.begin(); it != sources.end(); ++it) + { + std::string dicom; + SystemToolbox::ReadFile(dicom, it->get<0>(), false); + + { + uint64_t offset; + ValueRepresentation vr; + ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom)); + ASSERT_EQ(it->get<1>(), offset); + ASSERT_EQ(it->get<2>(), vr); + } + + { + uint64_t offset; + ValueRepresentation vr; + ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom.c_str(), dicom.size())); + ASSERT_EQ(it->get<1>(), offset); + ASSERT_EQ(it->get<2>(), vr); + } + + ParsedDicomFile a(dicom); + Json::Value aa; + a.DatasetToJson(aa, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0); + + std::stringstream stream; + size_t pos = 0; + + DicomStreamReader r(stream); + V visitor; + + // Test reading byte per byte + while (pos < dicom.size() && + !r.IsDone()) + { + r.Consume(visitor); + stream.clear(); + stream.put(dicom[pos++]); + } + + r.Consume(visitor); + + ASSERT_EQ(it->get<1>(), visitor.GetPixelDataOffset()); + + // Truncate the original DICOM up to pixel data + dicom.resize(visitor.GetPixelDataOffset()); + + ParsedDicomFile b(dicom); + Json::Value bb; + b.DatasetToJson(bb, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0); + + aa.removeMember("7fe0,0010"); + aa.removeMember("fffc,fffc"); // For "1.2.840.10008.1.2.5.dcm" + ASSERT_EQ(aa.toStyledString(), bb.toStyledString()); + } +} + +TEST(DicomStreamReader, DISABLED_Tutu2) +{ + //static const std::string PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/"; + + //const std::string path = PATH + "1.2.840.10008.1.2.4.50.dcm"; + //const std::string path = PATH + "1.2.840.10008.1.2.2.dcm"; + const std::string path = "/home/jodogne/Subversion/orthanc-tests/Database/HierarchicalAnonymization/RTH/RT.dcm"; + + std::ifstream stream(path.c_str()); + + DicomStreamReader r(stream); + V visitor; + + r.Consume(visitor); + + printf(">> %d\n", static_cast(r.GetProcessedBytes())); +} + + +#include + +TEST(DicomStreamReader, DISABLED_Tutu3) +{ + static const std::string PATH = "/home/jodogne/Subversion/orthanc-tests/Database/"; + + std::set errors; + unsigned int success = 0; + + for (boost::filesystem::recursive_directory_iterator current(PATH), end; + current != end ; ++current) + { + if (SystemToolbox::IsRegularFile(current->path().string())) + { + try + { + if (current->path().extension() == ".dcm") + { + const std::string path = current->path().string(); + printf("[%s]\n", path.c_str()); + + DicomMap m1; + + { + std::ifstream stream(path.c_str()); + + DicomStreamReader r(stream); + V visitor; + + try + { + r.Consume(visitor, DICOM_TAG_PIXEL_DATA); + //r.Consume(visitor); + success++; + } + catch (OrthancException& e) + { + errors.insert(path); + continue; + } + + m1.Assign(visitor.GetDicomMap()); + } + + m1.SetValue(DICOM_TAG_PIXEL_DATA, "", true); + + + DicomMap m2; + + { + std::string dicom; + SystemToolbox::ReadFile(dicom, path); + + ParsedDicomFile f(dicom); + f.ExtractDicomSummary(m2, 256); + } + + std::set tags; + m2.GetTags(tags); + + bool first = true; + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + if (!m1.HasTag(*it)) + { + if (first) + { + fprintf(stderr, "[%s]\n", path.c_str()); + first = false; + } + + std::cerr << "ERROR: " << it->Format() << std::endl; + } + else if (!m2.GetValue(*it).IsNull() && + !m2.GetValue(*it).IsBinary() && + Toolbox::IsAsciiString(m1.GetValue(*it).GetContent())) + { + const std::string& v1 = m1.GetValue(*it).GetContent(); + const std::string& v2 = m2.GetValue(*it).GetContent(); + + if (v1 != v2 && + (v1.size() != v2.size() + 1 || + v1.substr(0, v2.size()) != v2)) + { + std::cerr << "ERROR: [" << v1 << "] [" << v2 << "]" << std::endl; + } + } + } + } + } + catch (boost::filesystem::filesystem_error&) + { + } + } + } + + + printf("\n== ERRORS ==\n"); + for (std::set::const_iterator + it = errors.begin(); it != errors.end(); ++it) + { + printf("[%s]\n", it->c_str()); + } + + printf("\n== SUCCESSES: %u ==\n\n", success); +} + +#endif diff --git a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp new file mode 100644 index 0000000..f5d3aa9 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp @@ -0,0 +1,342 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/FileStorage/FilesystemStorage.h" +#include "../Sources/FileStorage/PluginStorageAreaAdapter.h" +#include "../Sources/FileStorage/StorageAccessor.h" +#include "../Sources/FileStorage/StorageCache.h" +#include "../Sources/Logging.h" +#include "../Sources/OrthancException.h" +#include "../Sources/Toolbox.h" +#include "../Sources/SystemToolbox.h" + +#include + + +using namespace Orthanc; + + +static void StringToVector(std::vector& v, + const std::string& s) +{ + v.resize(s.size()); + for (size_t i = 0; i < s.size(); i++) + v[i] = s[i]; +} + + +TEST(FilesystemStorage, Basic) +{ + FilesystemStorage s("UnitTestsStorage"); + + std::string data = Toolbox::GenerateUuid(); + std::string uid = Toolbox::GenerateUuid(); + s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); + std::string d; + { + std::unique_ptr buffer(s.ReadWhole(uid, FileContentType_Unknown)); + buffer->MoveToString(d); + } + ASSERT_EQ(d.size(), data.size()); + ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); + ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } +} + +TEST(FilesystemStorage, Basic2) +{ + FilesystemStorage s("UnitTestsStorage"); + + std::vector data; + StringToVector(data, Toolbox::GenerateUuid()); + std::string uid = Toolbox::GenerateUuid(); + s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); + std::string d; + { + std::unique_ptr buffer(s.ReadWhole(uid, FileContentType_Unknown)); + buffer->MoveToString(d); + } + ASSERT_EQ(d.size(), data.size()); + ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); + ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } +} + +TEST(FilesystemStorage, FileWithSameNameAsTopDirectory) +{ + FilesystemStorage s("UnitTestsStorageTop"); + s.Clear(); + + std::vector data; + StringToVector(data, Toolbox::GenerateUuid()); + + SystemToolbox::WriteFile("toto", "UnitTestsStorageTop/12"); + ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException); + s.Clear(); +} + +TEST(FilesystemStorage, FileWithSameNameAsChildDirectory) +{ + FilesystemStorage s("UnitTestsStorageChild"); + s.Clear(); + + std::vector data; + StringToVector(data, Toolbox::GenerateUuid()); + + SystemToolbox::MakeDirectory("UnitTestsStorageChild/12"); + SystemToolbox::WriteFile("toto", "UnitTestsStorageChild/12/34"); + ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException); + s.Clear(); +} + +TEST(FilesystemStorage, FileAlreadyExists) +{ + FilesystemStorage s("UnitTestsStorageFileAlreadyExists"); + s.Clear(); + + std::vector data; + StringToVector(data, Toolbox::GenerateUuid()); + + SystemToolbox::MakeDirectory("UnitTestsStorageFileAlreadyExists/12/34"); + SystemToolbox::WriteFile("toto", "UnitTestsStorageFileAlreadyExists/12/34/12345678-1234-1234-1234-1234567890ab"); + ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException); + s.Clear(); +} + + +TEST(FilesystemStorage, EndToEnd) +{ + FilesystemStorage s("UnitTestsStorage"); + s.Clear(); + + std::list u; + for (unsigned int i = 0; i < 10; i++) + { + std::string t = Toolbox::GenerateUuid(); + std::string uid = Toolbox::GenerateUuid(); + s.Create(uid.c_str(), &t[0], t.size(), FileContentType_Unknown); + u.push_back(uid); + } + + std::set ss; + s.ListAllFiles(ss); + ASSERT_EQ(10u, ss.size()); + + unsigned int c = 0; + for (std::list::const_iterator + i = u.begin(); i != u.end(); ++i, c++) + { + ASSERT_TRUE(ss.find(*i) != ss.end()); + if (c < 5) + s.Remove(*i, FileContentType_Unknown); + } + + s.ListAllFiles(ss); + ASSERT_EQ(5u, ss.size()); + + s.Clear(); + s.ListAllFiles(ss); + ASSERT_EQ(0u, ss.size()); +} + + +TEST(StorageAccessor, NoCompression) +{ + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); + StorageCache cache; + StorageAccessor accessor(s, cache); + + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL); + + std::string r; + accessor.Read(r, info); + + ASSERT_EQ(data, r); + ASSERT_EQ(CompressionType_None, info.GetCompressionType()); + ASSERT_EQ(11u, info.GetUncompressedSize()); + ASSERT_EQ(11u, info.GetCompressedSize()); + ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); + ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5()); + ASSERT_EQ(info.GetUncompressedMD5(), info.GetCompressedMD5()); +} + + +TEST(StorageAccessor, Compression) +{ + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); + StorageCache cache; + StorageAccessor accessor(s, cache); + + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL); + + std::string r; + accessor.Read(r, info); + + ASSERT_EQ(data, r); + ASSERT_EQ(CompressionType_ZlibWithSize, info.GetCompressionType()); + ASSERT_EQ(11u, info.GetUncompressedSize()); + ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); + ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5()); + ASSERT_NE(info.GetUncompressedMD5(), info.GetCompressedMD5()); +} + + +TEST(StorageAccessor, Mix) +{ + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); + StorageCache cache; + StorageAccessor accessor(s, cache); + + const std::string compressedData = "Hello"; + const std::string uncompressedData = "HelloWorld"; + + FileInfo compressedInfo; + accessor.Write(compressedInfo, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, NULL); + + std::string r; + accessor.Read(r, compressedInfo); + ASSERT_EQ(compressedData, r); + + FileInfo uncompressedInfo; + accessor.Write(uncompressedInfo, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, NULL); + accessor.Read(r, uncompressedInfo); + ASSERT_EQ(uncompressedData, r); + ASSERT_NE(compressedData, r); + + /* + // This test is too slow on Windows + accessor.SetCompressionForNextOperations(CompressionType_ZlibWithSize); + ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid(), FileContentType_Unknown), OrthancException); + */ +} + + +TEST(StorageAccessor, Range) +{ + { + StorageAccessor::Range range; + ASSERT_FALSE(range.HasStart()); + ASSERT_FALSE(range.HasEnd()); + ASSERT_THROW(range.GetStartInclusive(), OrthancException); + ASSERT_THROW(range.GetEndInclusive(), OrthancException); + ASSERT_EQ("bytes 0-99/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 0-0/1", range.FormatHttpContentRange(1)); + ASSERT_THROW(range.FormatHttpContentRange(0), OrthancException); + ASSERT_EQ(100u, range.GetContentLength(100)); + ASSERT_EQ(1u, range.GetContentLength(1)); + ASSERT_THROW(range.GetContentLength(0), OrthancException); + + range.SetStartInclusive(10); + ASSERT_TRUE(range.HasStart()); + ASSERT_FALSE(range.HasEnd()); + ASSERT_EQ(10u, range.GetStartInclusive()); + ASSERT_THROW(range.GetEndInclusive(), OrthancException); + ASSERT_EQ("bytes 10-99/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 10-10/11", range.FormatHttpContentRange(11)); + ASSERT_THROW(range.FormatHttpContentRange(10), OrthancException); + ASSERT_EQ(90u, range.GetContentLength(100)); + ASSERT_EQ(1u, range.GetContentLength(11)); + ASSERT_THROW(range.GetContentLength(10), OrthancException); + + range.SetEndInclusive(30); + ASSERT_TRUE(range.HasStart()); + ASSERT_TRUE(range.HasEnd()); + ASSERT_EQ(10u, range.GetStartInclusive()); + ASSERT_EQ(30u, range.GetEndInclusive()); + ASSERT_EQ("bytes 10-30/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 10-30/31", range.FormatHttpContentRange(31)); + ASSERT_THROW(range.FormatHttpContentRange(30), OrthancException); + ASSERT_EQ(21u, range.GetContentLength(100)); + ASSERT_EQ(21u, range.GetContentLength(31)); + ASSERT_THROW(range.GetContentLength(30), OrthancException); + } + + { + StorageAccessor::Range range; + range.SetEndInclusive(20); + ASSERT_FALSE(range.HasStart()); + ASSERT_TRUE(range.HasEnd()); + ASSERT_THROW(range.GetStartInclusive(), OrthancException); + ASSERT_EQ(20u, range.GetEndInclusive()); + ASSERT_EQ("bytes 0-20/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 0-20/21", range.FormatHttpContentRange(21)); + ASSERT_THROW(range.FormatHttpContentRange(20), OrthancException); + ASSERT_EQ(21u, range.GetContentLength(100)); + ASSERT_EQ(21u, range.GetContentLength(21)); + ASSERT_THROW(range.GetContentLength(20), OrthancException); + } + + { + StorageAccessor::Range range = StorageAccessor::Range::ParseHttpRange("bytes=1-30"); + ASSERT_TRUE(range.HasStart()); + ASSERT_TRUE(range.HasEnd()); + ASSERT_EQ(1u, range.GetStartInclusive()); + ASSERT_EQ(30u, range.GetEndInclusive()); + ASSERT_EQ("bytes 1-30/100", range.FormatHttpContentRange(100)); + } + + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes="), OrthancException); + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=-1-30"), OrthancException); + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=100-30"), OrthancException); + + ASSERT_EQ("bytes 0-99/100", StorageAccessor::Range::ParseHttpRange("bytes=-").FormatHttpContentRange(100)); + ASSERT_EQ("bytes 0-10/100", StorageAccessor::Range::ParseHttpRange("bytes=-10").FormatHttpContentRange(100)); + ASSERT_EQ("bytes 10-99/100", StorageAccessor::Range::ParseHttpRange("bytes=10-").FormatHttpContentRange(100)); + + { + std::string s; + StorageAccessor::Range::ParseHttpRange("bytes=1-2").Extract(s, "Hello"); + ASSERT_EQ("el", s); + StorageAccessor::Range::ParseHttpRange("bytes=-2").Extract(s, "Hello"); + ASSERT_EQ("Hel", s); + StorageAccessor::Range::ParseHttpRange("bytes=3-").Extract(s, "Hello"); + ASSERT_EQ("lo", s); + StorageAccessor::Range::ParseHttpRange("bytes=-").Extract(s, "Hello"); + ASSERT_EQ("Hello", s); + StorageAccessor::Range::ParseHttpRange("bytes=4-").Extract(s, "Hello"); + ASSERT_EQ("o", s); + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=5-").Extract(s, "Hello"), OrthancException); + } +} \ No newline at end of file diff --git a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp new file mode 100644 index 0000000..64c0665 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp @@ -0,0 +1,1715 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#if !defined(ORTHANC_ENABLE_PUGIXML) +# error ORTHANC_ENABLE_PUGIXML is not defined +#endif + +#include "../Sources/EnumerationDictionary.h" + +#include + +#include "../Sources/DicomFormat/DicomTag.h" +#include "../Sources/HttpServer/HttpToolbox.h" +#include "../Sources/Logging.h" +#include "../Sources/OrthancException.h" +#include "../Sources/Toolbox.h" + +#if ORTHANC_SANDBOXED != 1 +# include "../Sources/FileBuffer.h" +# include "../Sources/MetricsRegistry.h" +# include "../Sources/SystemToolbox.h" +# include "../Sources/TemporaryFile.h" +#endif + +#include + + +using namespace Orthanc; + + +TEST(Uuid, Generation) +{ + for (int i = 0; i < 10; i++) + { + std::string s = Toolbox::GenerateUuid(); + ASSERT_TRUE(Toolbox::IsUuid(s)); + } +} + +TEST(Uuid, Test) +{ + ASSERT_FALSE(Toolbox::IsUuid("")); + ASSERT_FALSE(Toolbox::IsUuid("012345678901234567890123456789012345")); + ASSERT_TRUE(Toolbox::IsUuid("550e8400-e29b-41d4-a716-446655440000")); + ASSERT_FALSE(Toolbox::IsUuid("550e8400-e29b-41d4-a716-44665544000_")); + ASSERT_FALSE(Toolbox::IsUuid("01234567890123456789012345678901234_")); + ASSERT_FALSE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-44665544000")); + ASSERT_TRUE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000")); + ASSERT_TRUE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000 ok")); + ASSERT_FALSE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000ok")); +} + +TEST(Toolbox, IsSHA1) +{ + ASSERT_FALSE(Toolbox::IsSHA1("")); + ASSERT_FALSE(Toolbox::IsSHA1("01234567890123456789012345678901234567890123")); + ASSERT_FALSE(Toolbox::IsSHA1("012345678901234567890123456789012345678901234")); + ASSERT_TRUE(Toolbox::IsSHA1("b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b9")); + + std::string sha = " b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b9 "; + ASSERT_TRUE(Toolbox::IsSHA1(sha)); + sha[3] = '\0'; + sha[53] = '\0'; + ASSERT_TRUE(Toolbox::IsSHA1(sha)); + sha[40] = '\0'; + ASSERT_FALSE(Toolbox::IsSHA1(sha)); + ASSERT_FALSE(Toolbox::IsSHA1(" ")); + + ASSERT_TRUE(Toolbox::IsSHA1("16738bc3-e47ed42a-43ce044c-a3414a45-cb069bd0")); + + std::string s; + Toolbox::ComputeSHA1(s, "The quick brown fox jumps over the lazy dog"); + ASSERT_TRUE(Toolbox::IsSHA1(s)); + ASSERT_EQ("2fd4e1c6-7a2d28fc-ed849ee1-bb76e739-1b93eb12", s); + + ASSERT_FALSE(Toolbox::IsSHA1("b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b_")); +} + + +TEST(ParseGetArguments, Basic) +{ + HttpToolbox::GetArguments b; + HttpToolbox::ParseGetArguments(b, "aaa=baaa&bb=a&aa=c"); + + HttpToolbox::Arguments a; + HttpToolbox::CompileGetArguments(a, b); + + ASSERT_EQ(3u, a.size()); + ASSERT_EQ(a["aaa"], "baaa"); + ASSERT_EQ(a["bb"], "a"); + ASSERT_EQ(a["aa"], "c"); +} + + +TEST(ParseGetArguments, BasicEmpty) +{ + HttpToolbox::GetArguments b; + HttpToolbox::ParseGetArguments(b, "aaa&bb=aa&aa"); + + HttpToolbox::Arguments a; + HttpToolbox::CompileGetArguments(a, b); + + ASSERT_EQ(3u, a.size()); + ASSERT_EQ(a["aaa"], ""); + ASSERT_EQ(a["bb"], "aa"); + ASSERT_EQ(a["aa"], ""); +} + + +TEST(ParseGetArguments, Single) +{ + HttpToolbox::GetArguments b; + HttpToolbox::ParseGetArguments(b, "aaa=baaa"); + + HttpToolbox::Arguments a; + HttpToolbox::CompileGetArguments(a, b); + + ASSERT_EQ(1u, a.size()); + ASSERT_EQ(a["aaa"], "baaa"); +} + + +TEST(ParseGetArguments, SingleEmpty) +{ + HttpToolbox::GetArguments b; + HttpToolbox::ParseGetArguments(b, "aaa"); + + HttpToolbox::Arguments a; + HttpToolbox::CompileGetArguments(a, b); + + ASSERT_EQ(1u, a.size()); + ASSERT_EQ(a["aaa"], ""); +} + + +TEST(ParseGetQuery, Test1) +{ + UriComponents uri; + HttpToolbox::GetArguments b; + HttpToolbox::ParseGetQuery(uri, b, "/instances/test/world?aaa=baaa&bb=a&aa=c"); + + HttpToolbox::Arguments a; + HttpToolbox::CompileGetArguments(a, b); + + ASSERT_EQ(3u, uri.size()); + ASSERT_EQ("instances", uri[0]); + ASSERT_EQ("test", uri[1]); + ASSERT_EQ("world", uri[2]); + ASSERT_EQ(3u, a.size()); + ASSERT_EQ(a["aaa"], "baaa"); + ASSERT_EQ(a["bb"], "a"); + ASSERT_EQ(a["aa"], "c"); +} + + +TEST(ParseGetQuery, Test2) +{ + UriComponents uri; + HttpToolbox::GetArguments b; + HttpToolbox::ParseGetQuery(uri, b, "/instances/test/world"); + + HttpToolbox::Arguments a; + HttpToolbox::CompileGetArguments(a, b); + + ASSERT_EQ(3u, uri.size()); + ASSERT_EQ("instances", uri[0]); + ASSERT_EQ("test", uri[1]); + ASSERT_EQ("world", uri[2]); + ASSERT_EQ(0u, a.size()); +} + + +TEST(Uri, SplitUriComponents) +{ + UriComponents c, d; + Toolbox::SplitUriComponents(c, "/cou/hello/world"); + ASSERT_EQ(3u, c.size()); + ASSERT_EQ("cou", c[0]); + ASSERT_EQ("hello", c[1]); + ASSERT_EQ("world", c[2]); + + Toolbox::SplitUriComponents(c, "/cou/hello/world/"); + ASSERT_EQ(3u, c.size()); + ASSERT_EQ("cou", c[0]); + ASSERT_EQ("hello", c[1]); + ASSERT_EQ("world", c[2]); + + Toolbox::SplitUriComponents(c, "/cou/hello/world/a"); + ASSERT_EQ(4u, c.size()); + ASSERT_EQ("cou", c[0]); + ASSERT_EQ("hello", c[1]); + ASSERT_EQ("world", c[2]); + ASSERT_EQ("a", c[3]); + + Toolbox::SplitUriComponents(c, "/"); + ASSERT_EQ(0u, c.size()); + + Toolbox::SplitUriComponents(c, "/hello"); + ASSERT_EQ(1u, c.size()); + ASSERT_EQ("hello", c[0]); + + Toolbox::SplitUriComponents(c, "/hello/"); + ASSERT_EQ(1u, c.size()); + ASSERT_EQ("hello", c[0]); + + ASSERT_THROW(Toolbox::SplitUriComponents(c, ""), OrthancException); + ASSERT_THROW(Toolbox::SplitUriComponents(c, "a"), OrthancException); + ASSERT_THROW(Toolbox::SplitUriComponents(c, "/coucou//coucou"), OrthancException); + + c.clear(); + c.push_back("test"); + ASSERT_EQ("/", Toolbox::FlattenUri(c, 10)); +} + + +TEST(Uri, Truncate) +{ + UriComponents c, d; + Toolbox::SplitUriComponents(c, "/cou/hello/world"); + + Toolbox::TruncateUri(d, c, 0); + ASSERT_EQ(3u, d.size()); + ASSERT_EQ("cou", d[0]); + ASSERT_EQ("hello", d[1]); + ASSERT_EQ("world", d[2]); + + Toolbox::TruncateUri(d, c, 1); + ASSERT_EQ(2u, d.size()); + ASSERT_EQ("hello", d[0]); + ASSERT_EQ("world", d[1]); + + Toolbox::TruncateUri(d, c, 2); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("world", d[0]); + + Toolbox::TruncateUri(d, c, 3); + ASSERT_EQ(0u, d.size()); + + Toolbox::TruncateUri(d, c, 4); + ASSERT_EQ(0u, d.size()); + + Toolbox::TruncateUri(d, c, 5); + ASSERT_EQ(0u, d.size()); +} + + +TEST(Uri, Child) +{ + UriComponents c1; Toolbox::SplitUriComponents(c1, "/hello/world"); + UriComponents c2; Toolbox::SplitUriComponents(c2, "/hello/hello"); + UriComponents c3; Toolbox::SplitUriComponents(c3, "/hello"); + UriComponents c4; Toolbox::SplitUriComponents(c4, "/world"); + UriComponents c5; Toolbox::SplitUriComponents(c5, "/"); + + ASSERT_TRUE(Toolbox::IsChildUri(c1, c1)); + ASSERT_FALSE(Toolbox::IsChildUri(c1, c2)); + ASSERT_FALSE(Toolbox::IsChildUri(c1, c3)); + ASSERT_FALSE(Toolbox::IsChildUri(c1, c4)); + ASSERT_FALSE(Toolbox::IsChildUri(c1, c5)); + + ASSERT_FALSE(Toolbox::IsChildUri(c2, c1)); + ASSERT_TRUE(Toolbox::IsChildUri(c2, c2)); + ASSERT_FALSE(Toolbox::IsChildUri(c2, c3)); + ASSERT_FALSE(Toolbox::IsChildUri(c2, c4)); + ASSERT_FALSE(Toolbox::IsChildUri(c2, c5)); + + ASSERT_TRUE(Toolbox::IsChildUri(c3, c1)); + ASSERT_TRUE(Toolbox::IsChildUri(c3, c2)); + ASSERT_TRUE(Toolbox::IsChildUri(c3, c3)); + ASSERT_FALSE(Toolbox::IsChildUri(c3, c4)); + ASSERT_FALSE(Toolbox::IsChildUri(c3, c5)); + + ASSERT_FALSE(Toolbox::IsChildUri(c4, c1)); + ASSERT_FALSE(Toolbox::IsChildUri(c4, c2)); + ASSERT_FALSE(Toolbox::IsChildUri(c4, c3)); + ASSERT_TRUE(Toolbox::IsChildUri(c4, c4)); + ASSERT_FALSE(Toolbox::IsChildUri(c4, c5)); + + ASSERT_TRUE(Toolbox::IsChildUri(c5, c1)); + ASSERT_TRUE(Toolbox::IsChildUri(c5, c2)); + ASSERT_TRUE(Toolbox::IsChildUri(c5, c3)); + ASSERT_TRUE(Toolbox::IsChildUri(c5, c4)); + ASSERT_TRUE(Toolbox::IsChildUri(c5, c5)); +} + + +#if ORTHANC_SANDBOXED != 1 +TEST(Uri, AutodetectMimeType) +{ + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("../NOTES")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("/")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("a/a")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\")); + ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\a")); + + ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../NOTES.txt")); + ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../coucou.xml/NOTES.txt")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("..\\coucou.\\NOTES.xml")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.xml")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.XmL")); + + ASSERT_EQ(MimeType_JavaScript, SystemToolbox::AutodetectMimeType("NOTES.js")); + ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.json")); + ASSERT_EQ(MimeType_Pdf, SystemToolbox::AutodetectMimeType("NOTES.pdf")); + ASSERT_EQ(MimeType_Css, SystemToolbox::AutodetectMimeType("NOTES.css")); + ASSERT_EQ(MimeType_Html, SystemToolbox::AutodetectMimeType("NOTES.html")); + ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("NOTES.txt")); + ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("NOTES.xml")); + ASSERT_EQ(MimeType_Gif, SystemToolbox::AutodetectMimeType("NOTES.gif")); + ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpg")); + ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpeg")); + ASSERT_EQ(MimeType_Png, SystemToolbox::AutodetectMimeType("NOTES.png")); + ASSERT_EQ(MimeType_NaCl, SystemToolbox::AutodetectMimeType("NOTES.nexe")); + ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.nmf")); + ASSERT_EQ(MimeType_PNaCl, SystemToolbox::AutodetectMimeType("NOTES.pexe")); + ASSERT_EQ(MimeType_Svg, SystemToolbox::AutodetectMimeType("NOTES.svg")); + ASSERT_EQ(MimeType_Woff, SystemToolbox::AutodetectMimeType("NOTES.woff")); + ASSERT_EQ(MimeType_Woff2, SystemToolbox::AutodetectMimeType("NOTES.woff2")); + ASSERT_EQ(MimeType_Ico, SystemToolbox::AutodetectMimeType("NOTES.ico")); + + // Test primitives from the "RegisterDefaultExtensions()" that was + // present in the sample "Serve Folders plugin" of Orthanc 1.4.2 + ASSERT_STREQ("application/javascript", EnumerationToString(SystemToolbox::AutodetectMimeType(".js"))); + ASSERT_STREQ("application/json", EnumerationToString(SystemToolbox::AutodetectMimeType(".json"))); + ASSERT_STREQ("application/json", EnumerationToString(SystemToolbox::AutodetectMimeType(".nmf"))); + ASSERT_STREQ("application/octet-stream", EnumerationToString(SystemToolbox::AutodetectMimeType(""))); + ASSERT_STREQ("application/wasm", EnumerationToString(SystemToolbox::AutodetectMimeType(".wasm"))); + ASSERT_STREQ("application/x-font-woff", EnumerationToString(SystemToolbox::AutodetectMimeType(".woff"))); + ASSERT_STREQ("application/x-nacl", EnumerationToString(SystemToolbox::AutodetectMimeType(".nexe"))); + ASSERT_STREQ("application/x-pnacl", EnumerationToString(SystemToolbox::AutodetectMimeType(".pexe"))); + ASSERT_STREQ("application/xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".xml"))); + ASSERT_STREQ("font/woff2", EnumerationToString(SystemToolbox::AutodetectMimeType(".woff2"))); + ASSERT_STREQ("image/gif", EnumerationToString(SystemToolbox::AutodetectMimeType(".gif"))); + ASSERT_STREQ("image/jpeg", EnumerationToString(SystemToolbox::AutodetectMimeType(".jpeg"))); + ASSERT_STREQ("image/jpeg", EnumerationToString(SystemToolbox::AutodetectMimeType(".jpg"))); + ASSERT_STREQ("image/png", EnumerationToString(SystemToolbox::AutodetectMimeType(".png"))); + ASSERT_STREQ("image/svg+xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".svg"))); + ASSERT_STREQ("text/css", EnumerationToString(SystemToolbox::AutodetectMimeType(".css"))); + ASSERT_STREQ("text/html", EnumerationToString(SystemToolbox::AutodetectMimeType(".html"))); + + ASSERT_STREQ("model/obj", EnumerationToString(SystemToolbox::AutodetectMimeType(".obj"))); + ASSERT_STREQ("model/mtl", EnumerationToString(SystemToolbox::AutodetectMimeType(".mtl"))); + ASSERT_STREQ("model/stl", EnumerationToString(SystemToolbox::AutodetectMimeType(".stl"))); +} +#endif + + +TEST(Toolbox, ComputeMD5) +{ + std::string s; + + // # echo -n "Hello" | md5sum + + Toolbox::ComputeMD5(s, "Hello"); + ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s); + Toolbox::ComputeMD5(s, ""); + ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s); + + Toolbox::ComputeMD5(s, "aaabbbccc"); + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); + + std::set set; + + Toolbox::ComputeMD5(s, set); + ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s); // empty set same as empty string + + set.insert("bbb"); + set.insert("ccc"); + set.insert("aaa"); + + Toolbox::ComputeMD5(s, set); + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); // set md5 same as string with the values sorted + + { + Toolbox::MD5Context context; + context.Append(""); + context.Append(NULL, 0); + context.Append("Hello"); + context.Export(s); + ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s); + ASSERT_THROW(context.Append("World"), OrthancException); + ASSERT_THROW(context.Export(s), OrthancException); + } + +#if ORTHANC_SANDBOXED != 1 + { + std::istringstream iss(std::string("aaabbbccc")); + SystemToolbox::ComputeStreamMD5(s, iss); + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); + } +#endif +} + +TEST(Toolbox, ComputeSHA1) +{ + std::string s; + + Toolbox::ComputeSHA1(s, "The quick brown fox jumps over the lazy dog"); + ASSERT_EQ("2fd4e1c6-7a2d28fc-ed849ee1-bb76e739-1b93eb12", s); + Toolbox::ComputeSHA1(s, ""); + ASSERT_EQ("da39a3ee-5e6b4b0d-3255bfef-95601890-afd80709", s); +} + +#if ORTHANC_SANDBOXED != 1 +TEST(Toolbox, PathToExecutable) +{ + printf("[%s]\n", SystemToolbox::GetPathToExecutable().c_str()); + printf("[%s]\n", SystemToolbox::GetDirectoryOfExecutable().c_str()); +} +#endif + +TEST(Toolbox, StripSpaces) +{ + ASSERT_EQ("", Toolbox::StripSpaces(" \t \r \n ")); + ASSERT_EQ("coucou", Toolbox::StripSpaces(" coucou \t \r \n ")); + ASSERT_EQ("cou cou", Toolbox::StripSpaces(" cou cou \n ")); + ASSERT_EQ("c", Toolbox::StripSpaces(" \n\t c\r \n ")); + + std::string s = "\" abd \""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ(" abd ", s); + + s = " \" abd \" "; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ(" \" abd \" ", s); + + s = Toolbox::StripSpaces(s); + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ(" abd ", s); + + s = "\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("", s); + + s = "\"\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("", s); + + s = "\"_\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("_", s); + + s = "\"\"\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("\"", s); +} + +TEST(Toolbox, Case) +{ + std::string s = "CoU"; + std::string ss; + + Toolbox::ToUpperCase(ss, s); + ASSERT_EQ("COU", ss); + Toolbox::ToLowerCase(ss, s); + ASSERT_EQ("cou", ss); + + s = "CoU"; + Toolbox::ToUpperCase(s); + ASSERT_EQ("COU", s); + + s = "CoU"; + Toolbox::ToLowerCase(s); + ASSERT_EQ("cou", s); +} + + +TEST(Logger, Basic) +{ + LOG(INFO) << "I say hello"; +} + +TEST(Toolbox, ConvertFromLatin1) +{ + // This is a Latin-1 test string + const unsigned char data[10] = { 0xe0, 0xe9, 0xea, 0xe7, 0x26, 0xc6, 0x61, 0x62, 0x63, 0x00 }; + + std::string s((char*) &data[0], 10); + ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s)); + + // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C" + std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false); + ASSERT_EQ(15u, utf8.size()); + ASSERT_EQ(0xc3, static_cast(utf8[0])); + ASSERT_EQ(0xa0, static_cast(utf8[1])); + ASSERT_EQ(0xc3, static_cast(utf8[2])); + ASSERT_EQ(0xa9, static_cast(utf8[3])); + ASSERT_EQ(0xc3, static_cast(utf8[4])); + ASSERT_EQ(0xaa, static_cast(utf8[5])); + ASSERT_EQ(0xc3, static_cast(utf8[6])); + ASSERT_EQ(0xa7, static_cast(utf8[7])); + ASSERT_EQ(0x26, static_cast(utf8[8])); + ASSERT_EQ(0xc3, static_cast(utf8[9])); + ASSERT_EQ(0x86, static_cast(utf8[10])); + ASSERT_EQ(0x61, static_cast(utf8[11])); + ASSERT_EQ(0x62, static_cast(utf8[12])); + ASSERT_EQ(0x63, static_cast(utf8[13])); + ASSERT_EQ(0x00, static_cast(utf8[14])); // Null-terminated string +} + + +TEST(Toolbox, FixUtf8) +{ + // This is a Latin-1 test string: "crane" with a circumflex accent + const unsigned char latin1[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 }; + + std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char)); + + ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false), Encoding_Latin1)); + ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false)); +} + + +static int32_t GetUnicode(const uint8_t* data, + size_t size, + size_t expectedLength) +{ + std::string s((char*) &data[0], size); + uint32_t unicode; + size_t length; + Toolbox::Utf8ToUnicodeCharacter(unicode, length, s, 0); + if (length != expectedLength) + { + return -1; // Error case + } + else + { + return unicode; + } +} + + +TEST(Toolbox, Utf8ToUnicode) +{ + // https://en.wikipedia.org/wiki/UTF-8 + + ASSERT_EQ(1u, sizeof(char)); + ASSERT_EQ(1u, sizeof(uint8_t)); + + { + const uint8_t data[] = { 0x24 }; + ASSERT_EQ(0x24, GetUnicode(data, 1, 1)); + ASSERT_THROW(GetUnicode(data, 0, 1), OrthancException); + } + + { + const uint8_t data[] = { 0xc2, 0xa2 }; + ASSERT_EQ(0xa2, GetUnicode(data, 2, 2)); + ASSERT_THROW(GetUnicode(data, 1, 2), OrthancException); + } + + { + const uint8_t data[] = { 0xe0, 0xa4, 0xb9 }; + ASSERT_EQ(0x0939, GetUnicode(data, 3, 3)); + ASSERT_THROW(GetUnicode(data, 2, 3), OrthancException); + } + + { + const uint8_t data[] = { 0xe2, 0x82, 0xac }; + ASSERT_EQ(0x20ac, GetUnicode(data, 3, 3)); + ASSERT_THROW(GetUnicode(data, 2, 3), OrthancException); + } + + { + const uint8_t data[] = { 0xf0, 0x90, 0x8d, 0x88 }; + ASSERT_EQ(0x010348, GetUnicode(data, 4, 4)); + ASSERT_THROW(GetUnicode(data, 3, 4), OrthancException); + } + + { + const uint8_t data[] = { 0xe0 }; + ASSERT_THROW(GetUnicode(data, 1, 1), OrthancException); + } +} + + +TEST(Toolbox, UrlDecode) +{ + std::string s; + + s = "Hello%20World"; + Toolbox::UrlDecode(s); + ASSERT_EQ("Hello World", s); + + s = "%21%23%24%26%27%28%29%2A%2B%2c%2f%3A%3b%3d%3f%40%5B%5D%90%ff"; + Toolbox::UrlDecode(s); + std::string ss = "!#$&'()*+,/:;=?@[]"; + ss.push_back((char) 144); + ss.push_back((char) 255); + ASSERT_EQ(ss, s); + + s = "(2000%2C00A4)+Other"; + Toolbox::UrlDecode(s); + ASSERT_EQ("(2000,00A4) Other", s); +} + + +TEST(Toolbox, IsAsciiString) +{ + std::string s = "Hello 12 /"; + ASSERT_EQ(10u, s.size()); + ASSERT_TRUE(Toolbox::IsAsciiString(s)); + ASSERT_TRUE(Toolbox::IsAsciiString(s.c_str(), 10)); + ASSERT_FALSE(Toolbox::IsAsciiString(s.c_str(), 11)); // Taking the trailing hidden '\0' + + s[2] = '\0'; + ASSERT_EQ(10u, s.size()); + ASSERT_FALSE(Toolbox::IsAsciiString(s)); + + ASSERT_TRUE(Toolbox::IsAsciiString("Hello\nworld")); + ASSERT_FALSE(Toolbox::IsAsciiString("Hello\rworld")); + + ASSERT_EQ("Hello\nworld", Toolbox::ConvertToAscii("Hello\nworld")); + ASSERT_EQ("Helloworld", Toolbox::ConvertToAscii("Hello\r\tworld")); +} + + +#if defined(__linux__) +TEST(Toolbox, AbsoluteDirectory) +{ + ASSERT_EQ("/tmp/hello", SystemToolbox::InterpretRelativePath("/tmp", "hello")); + ASSERT_EQ("/tmp", SystemToolbox::InterpretRelativePath("/tmp", "/tmp")); +} +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(Toolbox, WriteFile) +{ + std::string path; + + { + TemporaryFile tmp; + path = tmp.GetPath(); + + std::string s; + s.append("Hello"); + s.push_back('\0'); + s.append("World"); + ASSERT_EQ(11u, s.size()); + + SystemToolbox::WriteFile(s, path.c_str()); + + std::string t; + SystemToolbox::ReadFile(t, path.c_str()); + + ASSERT_EQ(11u, t.size()); + ASSERT_EQ(0, t[5]); + ASSERT_EQ(0, memcmp(s.c_str(), t.c_str(), s.size())); + + std::string h; + ASSERT_EQ(true, SystemToolbox::ReadHeader(h, path.c_str(), 1)); + ASSERT_EQ(1u, h.size()); + ASSERT_EQ('H', h[0]); + ASSERT_TRUE(SystemToolbox::ReadHeader(h, path.c_str(), 0)); + ASSERT_EQ(0u, h.size()); + ASSERT_FALSE(SystemToolbox::ReadHeader(h, path.c_str(), 32)); + ASSERT_EQ(11u, h.size()); + ASSERT_EQ(0, memcmp(s.c_str(), h.c_str(), s.size())); + } + + std::string u; + ASSERT_THROW(SystemToolbox::ReadFile(u, path.c_str()), OrthancException); + + { + TemporaryFile tmp; + std::string s = "Hello"; + SystemToolbox::WriteFile(s, tmp.GetPath(), true /* call fsync() */); + std::string t; + SystemToolbox::ReadFile(t, tmp.GetPath()); + ASSERT_EQ(s, t); + } +} +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(Toolbox, FileBuffer) +{ + FileBuffer f; + f.Append("a", 1); + f.Append("", 0); + f.Append("bc", 2); + + std::string s; + f.Read(s); + ASSERT_EQ("abc", s); + + ASSERT_THROW(f.Append("d", 1), OrthancException); // File is closed +} +#endif + + +TEST(Toolbox, Wildcard) +{ + ASSERT_EQ("abcd", Toolbox::WildcardToRegularExpression("abcd")); + ASSERT_EQ("ab.*cd", Toolbox::WildcardToRegularExpression("ab*cd")); + ASSERT_EQ("ab..cd", Toolbox::WildcardToRegularExpression("ab??cd")); + ASSERT_EQ("a.*b.c.*d", Toolbox::WildcardToRegularExpression("a*b?c*d")); + ASSERT_EQ("a\\{b\\]", Toolbox::WildcardToRegularExpression("a{b]")); +} + + +TEST(Toolbox, Tokenize) +{ + std::vector t; + + Toolbox::TokenizeString(t, "", ','); + ASSERT_EQ(1u, t.size()); + ASSERT_EQ("", t[0]); + + Toolbox::TokenizeString(t, "abc", ','); + ASSERT_EQ(1u, t.size()); + ASSERT_EQ("abc", t[0]); + + Toolbox::TokenizeString(t, "ab,cd,ef,", ','); + ASSERT_EQ(4u, t.size()); + ASSERT_EQ("ab", t[0]); + ASSERT_EQ("cd", t[1]); + ASSERT_EQ("ef", t[2]); + ASSERT_EQ("", t[3]); +} + +TEST(Toolbox, SplitString) +{ + { + std::set result; + Toolbox::SplitString(result, "", ';'); + ASSERT_EQ(0u, result.size()); + } + + { + std::set result; + Toolbox::SplitString(result, "a", ';'); + ASSERT_EQ(1u, result.size()); + ASSERT_TRUE(result.end() != result.find("a")); + } + + { + std::set result; + Toolbox::SplitString(result, "a;b", ';'); + ASSERT_EQ(2u, result.size()); + ASSERT_TRUE(result.end() != result.find("a")); + ASSERT_TRUE(result.end() != result.find("b")); + } + + { + std::set result; + Toolbox::SplitString(result, "a;b;", ';'); + ASSERT_EQ(2u, result.size()); + ASSERT_TRUE(result.end() != result.find("a")); + ASSERT_TRUE(result.end() != result.find("b")); + } + + { + std::set result; + Toolbox::SplitString(result, "a;a", ';'); + ASSERT_EQ(1u, result.size()); + ASSERT_TRUE(result.end() != result.find("a")); + } + + { + std::vector result; + Toolbox::SplitString(result, "", ';'); + ASSERT_EQ(0u, result.size()); + } + + { + std::vector result; + Toolbox::SplitString(result, "a", ';'); + ASSERT_EQ(1u, result.size()); + ASSERT_EQ("a", result[0]); + } + + { + std::vector result; + Toolbox::SplitString(result, "a;b", ';'); + ASSERT_EQ(2u, result.size()); + ASSERT_EQ("a", result[0]); + ASSERT_EQ("b", result[1]); + } + + { + std::vector result; + Toolbox::SplitString(result, "a;b;", ';'); + ASSERT_EQ(2u, result.size()); + ASSERT_EQ("a", result[0]); + ASSERT_EQ("b", result[1]); + } + + { + std::vector result; + Toolbox::TokenizeString(result, "a;a", ';'); + ASSERT_EQ(2u, result.size()); + ASSERT_EQ("a", result[0]); + ASSERT_EQ("a", result[1]); + } +} + +TEST(Toolbox, Enumerations) +{ + ASSERT_EQ(Encoding_Utf8, StringToEncoding(EnumerationToString(Encoding_Utf8))); + ASSERT_EQ(Encoding_Ascii, StringToEncoding(EnumerationToString(Encoding_Ascii))); + ASSERT_EQ(Encoding_Latin1, StringToEncoding(EnumerationToString(Encoding_Latin1))); + ASSERT_EQ(Encoding_Latin2, StringToEncoding(EnumerationToString(Encoding_Latin2))); + ASSERT_EQ(Encoding_Latin3, StringToEncoding(EnumerationToString(Encoding_Latin3))); + ASSERT_EQ(Encoding_Latin4, StringToEncoding(EnumerationToString(Encoding_Latin4))); + ASSERT_EQ(Encoding_Latin5, StringToEncoding(EnumerationToString(Encoding_Latin5))); + ASSERT_EQ(Encoding_Cyrillic, StringToEncoding(EnumerationToString(Encoding_Cyrillic))); + ASSERT_EQ(Encoding_Arabic, StringToEncoding(EnumerationToString(Encoding_Arabic))); + ASSERT_EQ(Encoding_Greek, StringToEncoding(EnumerationToString(Encoding_Greek))); + ASSERT_EQ(Encoding_Hebrew, StringToEncoding(EnumerationToString(Encoding_Hebrew))); + ASSERT_EQ(Encoding_Japanese, StringToEncoding(EnumerationToString(Encoding_Japanese))); + ASSERT_EQ(Encoding_Chinese, StringToEncoding(EnumerationToString(Encoding_Chinese))); + ASSERT_EQ(Encoding_Thai, StringToEncoding(EnumerationToString(Encoding_Thai))); + ASSERT_EQ(Encoding_Korean, StringToEncoding(EnumerationToString(Encoding_Korean))); + ASSERT_EQ(Encoding_JapaneseKanji, StringToEncoding(EnumerationToString(Encoding_JapaneseKanji))); + ASSERT_EQ(Encoding_SimplifiedChinese, StringToEncoding(EnumerationToString(Encoding_SimplifiedChinese))); + + ASSERT_EQ(ResourceType_Patient, StringToResourceType(EnumerationToString(ResourceType_Patient))); + ASSERT_EQ(ResourceType_Study, StringToResourceType(EnumerationToString(ResourceType_Study))); + ASSERT_EQ(ResourceType_Series, StringToResourceType(EnumerationToString(ResourceType_Series))); + ASSERT_EQ(ResourceType_Instance, StringToResourceType(EnumerationToString(ResourceType_Instance))); + + ASSERT_EQ(ImageFormat_Png, StringToImageFormat(EnumerationToString(ImageFormat_Png))); + + ASSERT_EQ(PhotometricInterpretation_ARGB, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_ARGB))); + ASSERT_EQ(PhotometricInterpretation_CMYK, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_CMYK))); + ASSERT_EQ(PhotometricInterpretation_HSV, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_HSV))); + ASSERT_EQ(PhotometricInterpretation_Monochrome1, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Monochrome1))); + ASSERT_EQ(PhotometricInterpretation_Monochrome2, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Monochrome2))); + ASSERT_EQ(PhotometricInterpretation_Palette, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Palette))); + ASSERT_EQ(PhotometricInterpretation_RGB, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_RGB))); + ASSERT_EQ(PhotometricInterpretation_YBRFull, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRFull))); + ASSERT_EQ(PhotometricInterpretation_YBRFull422, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRFull422))); + ASSERT_EQ(PhotometricInterpretation_YBRPartial420, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRPartial420))); + ASSERT_EQ(PhotometricInterpretation_YBRPartial422, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRPartial422))); + ASSERT_EQ(PhotometricInterpretation_YBR_ICT, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBR_ICT))); + ASSERT_EQ(PhotometricInterpretation_YBR_RCT, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBR_RCT))); + + ASSERT_STREQ("Unknown", EnumerationToString(PhotometricInterpretation_Unknown)); + ASSERT_THROW(StringToPhotometricInterpretation("Unknown"), OrthancException); + + ASSERT_EQ(DicomVersion_2008, StringToDicomVersion(EnumerationToString(DicomVersion_2008))); + ASSERT_EQ(DicomVersion_2017c, StringToDicomVersion(EnumerationToString(DicomVersion_2017c))); + ASSERT_EQ(DicomVersion_2021b, StringToDicomVersion(EnumerationToString(DicomVersion_2021b))); + ASSERT_EQ(DicomVersion_2023b, StringToDicomVersion(EnumerationToString(DicomVersion_2023b))); + + for (int i = static_cast(ValueRepresentation_ApplicationEntity); + i < static_cast(ValueRepresentation_NotSupported); i += 1) + { + ValueRepresentation vr = static_cast(i); + ASSERT_EQ(vr, StringToValueRepresentation(EnumerationToString(vr), true)); + } + + ASSERT_THROW(StringToValueRepresentation("nope", true), OrthancException); + + ASSERT_EQ(JobState_Pending, StringToJobState(EnumerationToString(JobState_Pending))); + ASSERT_EQ(JobState_Running, StringToJobState(EnumerationToString(JobState_Running))); + ASSERT_EQ(JobState_Success, StringToJobState(EnumerationToString(JobState_Success))); + ASSERT_EQ(JobState_Failure, StringToJobState(EnumerationToString(JobState_Failure))); + ASSERT_EQ(JobState_Paused, StringToJobState(EnumerationToString(JobState_Paused))); + ASSERT_EQ(JobState_Retry, StringToJobState(EnumerationToString(JobState_Retry))); + ASSERT_THROW(StringToJobState("nope"), OrthancException); + + ASSERT_EQ(MimeType_Binary, StringToMimeType(EnumerationToString(MimeType_Binary))); + ASSERT_EQ(MimeType_Css, StringToMimeType(EnumerationToString(MimeType_Css))); + ASSERT_EQ(MimeType_Dicom, StringToMimeType(EnumerationToString(MimeType_Dicom))); + ASSERT_EQ(MimeType_Gif, StringToMimeType(EnumerationToString(MimeType_Gif))); + ASSERT_EQ(MimeType_Gzip, StringToMimeType(EnumerationToString(MimeType_Gzip))); + ASSERT_EQ(MimeType_Html, StringToMimeType(EnumerationToString(MimeType_Html))); + ASSERT_EQ(MimeType_JavaScript, StringToMimeType(EnumerationToString(MimeType_JavaScript))); + ASSERT_EQ(MimeType_Jpeg, StringToMimeType(EnumerationToString(MimeType_Jpeg))); + ASSERT_EQ(MimeType_Jpeg2000, StringToMimeType(EnumerationToString(MimeType_Jpeg2000))); + ASSERT_EQ(MimeType_Json, StringToMimeType(EnumerationToString(MimeType_Json))); + ASSERT_EQ(MimeType_NaCl, StringToMimeType(EnumerationToString(MimeType_NaCl))); + ASSERT_EQ(MimeType_PNaCl, StringToMimeType(EnumerationToString(MimeType_PNaCl))); + ASSERT_EQ(MimeType_Pam, StringToMimeType(EnumerationToString(MimeType_Pam))); + ASSERT_EQ(MimeType_Pdf, StringToMimeType(EnumerationToString(MimeType_Pdf))); + ASSERT_EQ(MimeType_PlainText, StringToMimeType(EnumerationToString(MimeType_PlainText))); + ASSERT_EQ(MimeType_Png, StringToMimeType(EnumerationToString(MimeType_Png))); + ASSERT_EQ(MimeType_Svg, StringToMimeType(EnumerationToString(MimeType_Svg))); + ASSERT_EQ(MimeType_WebAssembly, StringToMimeType(EnumerationToString(MimeType_WebAssembly))); + ASSERT_EQ(MimeType_Xml, StringToMimeType("application/xml")); + ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml")); + ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml))); + ASSERT_EQ(MimeType_DicomWebJson, StringToMimeType(EnumerationToString(MimeType_DicomWebJson))); + ASSERT_EQ(MimeType_DicomWebXml, StringToMimeType(EnumerationToString(MimeType_DicomWebXml))); + ASSERT_EQ(MimeType_Mtl, StringToMimeType(EnumerationToString(MimeType_Mtl))); + ASSERT_EQ(MimeType_Obj, StringToMimeType(EnumerationToString(MimeType_Obj))); + ASSERT_EQ(MimeType_Stl, StringToMimeType(EnumerationToString(MimeType_Stl))); + ASSERT_THROW(StringToMimeType("nope"), OrthancException); + + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Patient)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Patient)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Patient)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance)); + + ASSERT_STREQ("Patients", GetResourceTypeText(ResourceType_Patient, true /* plural */, true /* upper case */)); + ASSERT_STREQ("patients", GetResourceTypeText(ResourceType_Patient, true, false)); + ASSERT_STREQ("Patient", GetResourceTypeText(ResourceType_Patient, false, true)); + ASSERT_STREQ("patient", GetResourceTypeText(ResourceType_Patient, false, false)); + ASSERT_STREQ("Studies", GetResourceTypeText(ResourceType_Study, true, true)); + ASSERT_STREQ("studies", GetResourceTypeText(ResourceType_Study, true, false)); + ASSERT_STREQ("Study", GetResourceTypeText(ResourceType_Study, false, true)); + ASSERT_STREQ("study", GetResourceTypeText(ResourceType_Study, false, false)); + ASSERT_STREQ("Series", GetResourceTypeText(ResourceType_Series, true, true)); + ASSERT_STREQ("series", GetResourceTypeText(ResourceType_Series, true, false)); + ASSERT_STREQ("Series", GetResourceTypeText(ResourceType_Series, false, true)); + ASSERT_STREQ("series", GetResourceTypeText(ResourceType_Series, false, false)); + ASSERT_STREQ("Instances", GetResourceTypeText(ResourceType_Instance, true, true)); + ASSERT_STREQ("instances", GetResourceTypeText(ResourceType_Instance, true, false)); + ASSERT_STREQ("Instance", GetResourceTypeText(ResourceType_Instance, false, true)); + ASSERT_STREQ("instance", GetResourceTypeText(ResourceType_Instance, false, false)); + + DicomTransferSyntax ts; + ASSERT_FALSE(LookupTransferSyntax(ts, "nope")); + ASSERT_TRUE(LookupTransferSyntax(ts, "1.2.840.10008.1.2")); ASSERT_EQ(DicomTransferSyntax_LittleEndianImplicit, ts); + ASSERT_STREQ("1.2.840.10008.1.2", GetTransferSyntaxUid(ts)); +} + + +#if defined(__linux__) || defined(__OpenBSD__) +#include +#elif defined(__FreeBSD__) +#include +#endif + + +TEST(Toolbox, Endianness) +{ + // Parts of this test come from Adam Conrad + // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728822#5 + + + /** + * Windows and OS X are assumed to always little-endian. + **/ + +#if defined(_WIN32) || defined(__APPLE__) + ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); + + + /** + * FreeBSD. + **/ + +#elif defined(__FreeBSD__) || defined(__OpenBSD__) +# if _BYTE_ORDER == _BIG_ENDIAN + ASSERT_EQ(Endianness_Big, Toolbox::DetectEndianness()); +# else // _LITTLE_ENDIAN + ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); +# endif + + + /** + * Linux. + **/ + +#elif defined(__linux__) || defined(__FreeBSD_kernel__) + +#if !defined(__BYTE_ORDER) +# error Support your platform here +#endif + +# if __BYTE_ORDER == __BIG_ENDIAN + ASSERT_EQ(Endianness_Big, Toolbox::DetectEndianness()); +# else // __LITTLE_ENDIAN + ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); +# endif + + + /** + * WebAssembly is always little-endian. + **/ + +#elif defined(__EMSCRIPTEN__) + ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); +#else +# error Support your platform here +#endif +} + + +#include "../Sources/Endianness.h" + +static void ASSERT_EQ16(uint16_t a, uint16_t b) +{ +#ifdef __MINGW32__ + // This cast solves a linking problem with MinGW + ASSERT_EQ(static_cast(a), static_cast(b)); +#else + ASSERT_EQ(a, b); +#endif +} + +static void ASSERT_NE16(uint16_t a, uint16_t b) +{ +#ifdef __MINGW32__ + // This cast solves a linking problem with MinGW + ASSERT_NE(static_cast(a), static_cast(b)); +#else + ASSERT_NE(a, b); +#endif +} + +static void ASSERT_EQ32(uint32_t a, uint32_t b) +{ +#ifdef __MINGW32__ + // This cast solves a linking problem with MinGW + ASSERT_EQ(static_cast(a), static_cast(b)); +#else + ASSERT_EQ(a, b); +#endif +} + +static void ASSERT_NE32(uint32_t a, uint32_t b) +{ +#ifdef __MINGW32__ + // This cast solves a linking problem with MinGW + ASSERT_NE(static_cast(a), static_cast(b)); +#else + ASSERT_NE(a, b); +#endif +} + +static void ASSERT_EQ64(uint64_t a, uint64_t b) +{ +#ifdef __MINGW32__ + // This cast solves a linking problem with MinGW + ASSERT_EQ(static_cast(a), static_cast(b)); +#else + ASSERT_EQ(a, b); +#endif +} + +static void ASSERT_NE64(uint64_t a, uint64_t b) +{ +#ifdef __MINGW32__ + // This cast solves a linking problem with MinGW + ASSERT_NE(static_cast(a), static_cast(b)); +#else + ASSERT_NE(a, b); +#endif +} + + + +TEST(Toolbox, EndiannessConversions16) +{ + Endianness e = Toolbox::DetectEndianness(); + + for (unsigned int i = 0; i < 65536; i += 17) + { + uint16_t v = static_cast(i); + ASSERT_EQ16(v, be16toh(htobe16(v))); + ASSERT_EQ16(v, le16toh(htole16(v))); + + const uint8_t* bytes = reinterpret_cast(&v); + if (bytes[0] != bytes[1]) + { + ASSERT_NE16(v, le16toh(htobe16(v))); + ASSERT_NE16(v, be16toh(htole16(v))); + } + else + { + ASSERT_EQ16(v, le16toh(htobe16(v))); + ASSERT_EQ16(v, be16toh(htole16(v))); + } + + switch (e) + { + case Endianness_Little: + ASSERT_EQ16(v, htole16(v)); + if (bytes[0] != bytes[1]) + { + ASSERT_NE16(v, htobe16(v)); + } + else + { + ASSERT_EQ16(v, htobe16(v)); + } + break; + + case Endianness_Big: + ASSERT_EQ16(v, htobe16(v)); + if (bytes[0] != bytes[1]) + { + ASSERT_NE16(v, htole16(v)); + } + else + { + ASSERT_EQ16(v, htole16(v)); + } + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +} + + +TEST(Toolbox, EndiannessConversions32) +{ + const uint32_t v = 0xff010203u; + const uint32_t r = 0x030201ffu; + ASSERT_EQ32(v, be32toh(htobe32(v))); + ASSERT_EQ32(v, le32toh(htole32(v))); + ASSERT_NE32(v, be32toh(htole32(v))); + ASSERT_NE32(v, le32toh(htobe32(v))); + + switch (Toolbox::DetectEndianness()) + { + case Endianness_Little: + ASSERT_EQ32(r, htobe32(v)); + ASSERT_EQ32(v, htole32(v)); + ASSERT_EQ32(r, be32toh(v)); + ASSERT_EQ32(v, le32toh(v)); + break; + + case Endianness_Big: + ASSERT_EQ32(v, htobe32(v)); + ASSERT_EQ32(r, htole32(v)); + ASSERT_EQ32(v, be32toh(v)); + ASSERT_EQ32(r, le32toh(v)); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } +} + + +TEST(Toolbox, EndiannessConversions64) +{ + const uint64_t v = 0xff01020304050607LL; + const uint64_t r = 0x07060504030201ffLL; + ASSERT_EQ64(v, be64toh(htobe64(v))); + ASSERT_EQ64(v, le64toh(htole64(v))); + ASSERT_NE64(v, be64toh(htole64(v))); + ASSERT_NE64(v, le64toh(htobe64(v))); + + switch (Toolbox::DetectEndianness()) + { + case Endianness_Little: + ASSERT_EQ64(r, htobe64(v)); + ASSERT_EQ64(v, htole64(v)); + ASSERT_EQ64(r, be64toh(v)); + ASSERT_EQ64(v, le64toh(v)); + break; + + case Endianness_Big: + ASSERT_EQ64(v, htobe64(v)); + ASSERT_EQ64(r, htole64(v)); + ASSERT_EQ64(v, be64toh(v)); + ASSERT_EQ64(r, le64toh(v)); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } +} + + +#if ORTHANC_SANDBOXED != 1 +TEST(Toolbox, Now) +{ + LOG(WARNING) << "Local time: " << SystemToolbox::GetNowIsoString(false); + LOG(WARNING) << "Universal time: " << SystemToolbox::GetNowIsoString(true); + + std::string date, time; + SystemToolbox::GetNowDicom(date, time, false); + LOG(WARNING) << "Local DICOM time: [" << date << "] [" << time << "]"; + + SystemToolbox::GetNowDicom(date, time, true); + LOG(WARNING) << "Universal DICOM time: [" << date << "] [" << time << "]"; +} +#endif + + +#if ORTHANC_ENABLE_PUGIXML == 1 +TEST(Toolbox, Xml) +{ + Json::Value a; + a["hello"] = "world"; + a["42"] = 43; + a["b"] = Json::arrayValue; + a["b"].append("test"); + a["b"].append("test2"); + + std::string s; + Toolbox::JsonToXml(s, a); + + std::cout << s; +} +#endif + + +#if !defined(_WIN32) && (ORTHANC_SANDBOXED != 1) +TEST(Toolbox, ExecuteSystemCommand) +{ + std::vector args(2); + args[0] = "Hello"; + args[1] = "World"; + + SystemToolbox::ExecuteSystemCommand("echo", args); +} +#endif + + +TEST(Toolbox, IsInteger) +{ + ASSERT_TRUE(Toolbox::IsInteger("00236")); + ASSERT_TRUE(Toolbox::IsInteger("-0042")); + ASSERT_TRUE(Toolbox::IsInteger("0")); + ASSERT_TRUE(Toolbox::IsInteger("-0")); + + ASSERT_FALSE(Toolbox::IsInteger("")); + ASSERT_FALSE(Toolbox::IsInteger("42a")); + ASSERT_FALSE(Toolbox::IsInteger("42-")); +} + + +TEST(Toolbox, StartsWith) +{ + ASSERT_TRUE(Toolbox::StartsWith("hello world", "")); + ASSERT_TRUE(Toolbox::StartsWith("hello world", "hello")); + ASSERT_TRUE(Toolbox::StartsWith("hello world", "h")); + ASSERT_FALSE(Toolbox::StartsWith("hello world", "H")); + ASSERT_FALSE(Toolbox::StartsWith("h", "hello")); + ASSERT_TRUE(Toolbox::StartsWith("h", "h")); + ASSERT_FALSE(Toolbox::StartsWith("", "h")); +} + + +TEST(Toolbox, UriEncode) +{ + std::string s; + + // Unreserved characters must not be modified + std::string t = "aAzZ09.-~_"; + Toolbox::UriEncode(s, t); + ASSERT_EQ(t, s); + + Toolbox::UriEncode(s, "!#$&'()*+,/:;=?@[]"); ASSERT_EQ("%21%23%24%26%27%28%29%2A%2B%2C/%3A%3B%3D%3F%40%5B%5D", s); + Toolbox::UriEncode(s, "%"); ASSERT_EQ("%25", s); + + // Encode characters from UTF-8. This is the test string from the + // file "../Resources/EncodingTests.py" + Toolbox::UriEncode(s, "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0"); + ASSERT_EQ("Test%C3%A9%C3%A4%C3%B6%C3%B2%D0%94%CE%98%C4%9D%D7%93%D8%B5%C4%B7%D1%9B%E0%B9%9B%EF%BE%88%C4%B0", s); +} + + +TEST(Toolbox, AccessJson) +{ + Json::Value v = Json::arrayValue; + ASSERT_EQ("nope", Toolbox::GetJsonStringField(v, "hello", "nope")); + + v = Json::objectValue; + ASSERT_EQ("nope", Toolbox::GetJsonStringField(v, "hello", "nope")); + ASSERT_EQ(-10, Toolbox::GetJsonIntegerField(v, "hello", -10)); + ASSERT_EQ(10u, Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10)); + ASSERT_TRUE(Toolbox::GetJsonBooleanField(v, "hello", true)); + + v["hello"] = "world"; + ASSERT_EQ("world", Toolbox::GetJsonStringField(v, "hello", "nope")); + ASSERT_THROW(Toolbox::GetJsonIntegerField(v, "hello", -10), OrthancException); + ASSERT_THROW(Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10), OrthancException); + ASSERT_THROW(Toolbox::GetJsonBooleanField(v, "hello", true), OrthancException); + + v["hello"] = -42; + ASSERT_THROW(Toolbox::GetJsonStringField(v, "hello", "nope"), OrthancException); + ASSERT_EQ(-42, Toolbox::GetJsonIntegerField(v, "hello", -10)); + ASSERT_THROW(Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10), OrthancException); + ASSERT_THROW(Toolbox::GetJsonBooleanField(v, "hello", true), OrthancException); + + v["hello"] = 42; + ASSERT_THROW(Toolbox::GetJsonStringField(v, "hello", "nope"), OrthancException); + ASSERT_EQ(42, Toolbox::GetJsonIntegerField(v, "hello", -10)); + ASSERT_EQ(42u, Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10)); + ASSERT_THROW(Toolbox::GetJsonBooleanField(v, "hello", true), OrthancException); + + v["hello"] = false; + ASSERT_THROW(Toolbox::GetJsonStringField(v, "hello", "nope"), OrthancException); + ASSERT_THROW(Toolbox::GetJsonIntegerField(v, "hello", -10), OrthancException); + ASSERT_THROW(Toolbox::GetJsonUnsignedIntegerField(v, "hello", 10), OrthancException); + ASSERT_FALSE(Toolbox::GetJsonBooleanField(v, "hello", true)); +} + + +TEST(Toolbox, LinesIterator) +{ + std::string s; + + { + std::string content; + Toolbox::LinesIterator it(content); + ASSERT_FALSE(it.GetLine(s)); + } + + { + std::string content = "\n\r"; + Toolbox::LinesIterator it(content); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_FALSE(it.GetLine(s)); + } + + { + std::string content = "\n Hello \n\nWorld\n\n"; + Toolbox::LinesIterator it(content); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_FALSE(it.GetLine(s)); it.Next(); + ASSERT_FALSE(it.GetLine(s)); + } + + { + std::string content = "\r Hello \r\rWorld\r\r"; + Toolbox::LinesIterator it(content); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_FALSE(it.GetLine(s)); it.Next(); + ASSERT_FALSE(it.GetLine(s)); + } + + { + std::string content = "\n\r Hello \n\r\n\rWorld\n\r\n\r"; + Toolbox::LinesIterator it(content); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_FALSE(it.GetLine(s)); it.Next(); + ASSERT_FALSE(it.GetLine(s)); + } + + { + std::string content = "\r\n Hello \r\n\r\nWorld\r\n\r\n"; + Toolbox::LinesIterator it(content); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ(" Hello ", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("World", s); + ASSERT_TRUE(it.GetLine(s)); it.Next(); ASSERT_EQ("", s); + ASSERT_FALSE(it.GetLine(s)); it.Next(); + ASSERT_FALSE(it.GetLine(s)); + } +} + + +#if ORTHANC_SANDBOXED != 1 +TEST(Toolbox, SubstituteVariables) +{ + std::map env; + env["NOPE"] = "nope"; + env["WORLD"] = "world"; + + ASSERT_EQ("Hello world\r\nWorld \r\nDone world\r\n", + Toolbox::SubstituteVariables( + "Hello ${WORLD}\r\nWorld ${HELLO}\r\nDone ${WORLD}\r\n", + env)); + + ASSERT_EQ("world A a B world C 'c' D {\"a\":\"b\"} E ", + Toolbox::SubstituteVariables( + "${WORLD} A ${WORLD2:-a} B ${WORLD:-b} C ${WORLD2:-\"'c'\"} D ${WORLD2:-'{\"a\":\"b\"}'} E ${WORLD2:-}", + env)); + + SystemToolbox::GetEnvironmentVariables(env); + ASSERT_TRUE(env.find("NOPE") == env.end()); + + // The "PATH" environment variable should always be available on + // machines running the unit tests + ASSERT_TRUE(env.find("PATH") != env.end() /* Case used by UNIX */ || + env.find("Path") != env.end() /* Case used by Windows */); + + env["PATH"] = "hello"; + ASSERT_EQ("AhelloB", + Toolbox::SubstituteVariables("A${PATH}B", env)); +} +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(MetricsRegistry, Basic) +{ + { + MetricsRegistry m; + m.SetEnabled(false); + m.SetIntegerValue("hello.world", 42); + + std::string s; + m.ExportPrometheusText(s); + ASSERT_TRUE(s.empty()); + } + + { + MetricsRegistry m; + m.Register("hello.world", MetricsUpdatePolicy_Directly, MetricsDataType_Integer); + + std::string s; + m.ExportPrometheusText(s); + ASSERT_TRUE(s.empty()); + } + + { + MetricsRegistry m; + m.SetIntegerValue("hello.world", -42); + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("hello.world")); + ASSERT_THROW(m.GetUpdatePolicy("nope"), OrthancException); + + std::string s; + m.ExportPrometheusText(s); + + std::vector t; + Toolbox::TokenizeString(t, s, '\n'); + ASSERT_EQ(2u, t.size()); + ASSERT_EQ("hello.world -42 ", t[0].substr(0, 16)); + ASSERT_TRUE(t[1].empty()); + } + + { + MetricsRegistry m; + m.Register("hello.max", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + m.SetIntegerValue("hello.max", 10); + m.SetIntegerValue("hello.max", 20); + m.SetIntegerValue("hello.max", -10); + m.SetIntegerValue("hello.max", 5); + + m.Register("hello.min", MetricsUpdatePolicy_MinOver10Seconds, MetricsDataType_Integer); + m.SetIntegerValue("hello.min", 10); + m.SetIntegerValue("hello.min", 20); + m.SetIntegerValue("hello.min", -10); + m.SetIntegerValue("hello.min", 5); + + m.Register("hello.directly", MetricsUpdatePolicy_Directly, MetricsDataType_Integer); + m.SetIntegerValue("hello.directly", 10); + m.SetIntegerValue("hello.directly", 20); + m.SetIntegerValue("hello.directly", -10); + m.SetIntegerValue("hello.directly", 5); + + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("hello.max")); + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("hello.min")); + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("hello.directly")); + + std::string s; + m.ExportPrometheusText(s); + + std::vector t; + Toolbox::TokenizeString(t, s, '\n'); + ASSERT_EQ(4u, t.size()); + ASSERT_TRUE(t[3].empty()); + + std::map u; + for (size_t i = 0; i < t.size() - 1; i++) + { + std::vector v; + Toolbox::TokenizeString(v, t[i], ' '); + u[v[0]] = v[1]; + } + + ASSERT_EQ("20", u["hello.max"]); + ASSERT_EQ("-10", u["hello.min"]); + ASSERT_EQ("5", u["hello.directly"]); + } + + { + MetricsRegistry m; + + m.SetIntegerValue("a", 10); + m.SetIntegerValue("b", 10, MetricsUpdatePolicy_MinOver10Seconds); + + m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + m.SetIntegerValue("c", 10, MetricsUpdatePolicy_MinOver10Seconds); + + m.Register("d", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + ASSERT_THROW(m.Register("d", MetricsUpdatePolicy_Directly, MetricsDataType_Integer), OrthancException); + + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("a")); + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("b")); + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("d")); + } + + { + MetricsRegistry m; + + { + MetricsRegistry::Timer t1(m, "a"); + MetricsRegistry::Timer t2(m, "b", MetricsUpdatePolicy_MinOver10Seconds); + } + + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("a")); + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("b")); + } + + { + MetricsRegistry m; + m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + m.SetFloatValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); + } + + { + MetricsRegistry m; + m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Float); + m.SetIntegerValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Float, m.GetDataType("c")); + } + + { + MetricsRegistry m; + m.SetIntegerValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds); + m.SetFloatValue("c", 101, MetricsUpdatePolicy_MaxOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); + } + + { + MetricsRegistry m; + m.SetIntegerValue("c", 100); + m.SetFloatValue("c", 101, MetricsUpdatePolicy_MaxOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); + } +} +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(Toolbox, ReadFileRange) +{ + TemporaryFile tmp; + std::string s; + + tmp.Write(""); + tmp.Read(s); ASSERT_TRUE(s.empty()); + tmp.ReadRange(s, 0, 0, true); ASSERT_TRUE(s.empty()); + tmp.ReadRange(s, 0, 10, false); ASSERT_TRUE(s.empty()); + + ASSERT_THROW(tmp.ReadRange(s, 0, 1, true), OrthancException); + + tmp.Write("Hello"); + tmp.Read(s); ASSERT_EQ("Hello", s); + tmp.ReadRange(s, 0, 5, true); ASSERT_EQ("Hello", s); + tmp.ReadRange(s, 0, 1, true); ASSERT_EQ("H", s); + tmp.ReadRange(s, 1, 2, true); ASSERT_EQ("e", s); + tmp.ReadRange(s, 2, 3, true); ASSERT_EQ("l", s); + tmp.ReadRange(s, 3, 4, true); ASSERT_EQ("l", s); + tmp.ReadRange(s, 4, 5, true); ASSERT_EQ("o", s); + tmp.ReadRange(s, 2, 5, true); ASSERT_EQ("llo", s); + tmp.ReadRange(s, 2, 50, false); ASSERT_EQ("llo", s); + tmp.ReadRange(s, 2, 2, false); ASSERT_TRUE(s.empty()); + tmp.ReadRange(s, 10, 50, false); ASSERT_TRUE(s.empty()); + + ASSERT_THROW(tmp.ReadRange(s, 5, 10, true), OrthancException); + ASSERT_THROW(tmp.ReadRange(s, 10, 50, true), OrthancException); + ASSERT_THROW(tmp.ReadRange(s, 50, 10, true), OrthancException); + ASSERT_THROW(tmp.ReadRange(s, 2, 1, true), OrthancException); +} +#endif + + +#if ORTHANC_SANDBOXED != 1 && ORTHANC_ENABLE_MD5 == 1 +TEST(Toolbox, FileMD5) +{ + { + TemporaryFile tmp1, tmp2; + std::string s = "aaabbbccc"; + + SystemToolbox::WriteFile(s, tmp1.GetPath()); + SystemToolbox::WriteFile(s, tmp2.GetPath()); + + std::string md5; + SystemToolbox::ComputeFileMD5(md5, tmp1.GetPath()); + + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", md5); + ASSERT_TRUE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // different sizes + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbcccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // same sizes, different contents + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } +} +#endif + +#if ORTHANC_SANDBOXED != 1 +TEST(Toolbox, GetMacAddressess) +{ + std::set mac; + SystemToolbox::GetMacAddresses(mac); + + for (std::set::const_iterator it = mac.begin(); it != mac.end(); ++it) + { + printf("MAC address: [%s]\n", it->c_str()); + } +} +#endif + + +TEST(Toolbox, IsVersionAbove) +{ + unsigned int a, b, c; + ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "nope")); + ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "mainline")); + ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "")); + ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "-1")); + ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "1.-1")); + ASSERT_FALSE(Toolbox::ParseVersion(a, b, c, "1.1.-1")); + + ASSERT_TRUE(Toolbox::ParseVersion(a, b, c, "14.17.20")); + ASSERT_EQ(14u, a); + ASSERT_EQ(17u, b); + ASSERT_EQ(20u, c); + + ASSERT_TRUE(Toolbox::ParseVersion(a, b, c, "18.19")); + ASSERT_EQ(18u, a); + ASSERT_EQ(19u, b); + ASSERT_EQ(0u, c); + + ASSERT_TRUE(Toolbox::ParseVersion(a, b, c, "78")); + ASSERT_EQ(78u, a); + ASSERT_EQ(0u, b); + ASSERT_EQ(0u, c); + + ASSERT_TRUE(Toolbox::IsVersionAbove("mainline", 99, 99, 99)); + + ASSERT_TRUE(Toolbox::IsVersionAbove("18", 17, 99, 99)); + ASSERT_TRUE(Toolbox::IsVersionAbove("18", 18, 0, 0)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18", 18, 0, 1)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18", 18, 1, 0)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18", 19, 0, 0)); + + ASSERT_TRUE(Toolbox::IsVersionAbove("18.19", 17, 99, 99)); + ASSERT_TRUE(Toolbox::IsVersionAbove("18.19", 18, 18, 99)); + ASSERT_TRUE(Toolbox::IsVersionAbove("18.19", 18, 19, 0)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18.19", 18, 19, 1)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18.19", 18, 20, 0)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18.19", 19, 0, 0)); + + ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 17, 99, 99)); + ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 18, 18, 99)); + ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 18, 19, 19)); + ASSERT_TRUE(Toolbox::IsVersionAbove("18.19.20", 18, 19, 20)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18.19.20", 18, 19, 21)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18.19.20", 18, 20, 0)); + ASSERT_FALSE(Toolbox::IsVersionAbove("18.19.20", 19, 0, 0)); +} diff --git a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp new file mode 100644 index 0000000..a0fc714 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp @@ -0,0 +1,3666 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING) +# error ORTHANC_ENABLE_DCMTK_TRANSCODING is not defined +#endif + +#if !defined(ORTHANC_ENABLE_PUGIXML) +# error ORTHANC_ENABLE_PUGIXML is not defined +#endif + +#include + +#include "../Sources/Compatibility.h" +#include "../Sources/DicomFormat/DicomImageInformation.h" +#include "../Sources/DicomFormat/DicomPath.h" +#include "../Sources/DicomNetworking/DicomFindAnswers.h" +#include "../Sources/DicomParsing/DicomModification.h" +#include "../Sources/DicomParsing/DicomWebJsonVisitor.h" +#include "../Sources/DicomParsing/FromDcmtkBridge.h" +#include "../Sources/DicomParsing/ParsedDicomCache.h" +#include "../Sources/DicomParsing/ToDcmtkBridge.h" +#include "../Sources/Endianness.h" +#include "../Sources/Images/Image.h" +#include "../Sources/Images/ImageBuffer.h" +#include "../Sources/Images/ImageProcessing.h" +#include "../Sources/Images/PngReader.h" +#include "../Sources/Logging.h" +#include "../Sources/OrthancException.h" + +#include "../Resources/CodeGeneration/EncodingTests.h" + +#if ORTHANC_SANDBOXED != 1 +# include "../Sources/SystemToolbox.h" +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include + +#if ORTHANC_ENABLE_PUGIXML == 1 +# include +# if !defined(PUGIXML_VERSION) +# error PUGIXML_VERSION is not available +# endif +#endif + +using namespace Orthanc; + +TEST(DicomFormat, Tag) +{ + ASSERT_EQ("PatientName", FromDcmtkBridge::GetTagName(DicomTag(0x0010, 0x0010), "")); + + DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription"); + ASSERT_EQ(0x0008, t.GetGroup()); + ASSERT_EQ(0x103E, t.GetElement()); + + t = FromDcmtkBridge::ParseTag("0020-e040"); + ASSERT_EQ(0x0020, t.GetGroup()); + ASSERT_EQ(0xe040, t.GetElement()); + + // Test ==() and !=() operators + ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020)); + ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020)); +} + + +#if ORTHANC_SANDBOXED != 1 +TEST(DicomModification, Basic) +{ + DicomModification m; + m.SetupAnonymization(DicomVersion_2008); + //m.SetLevel(DicomRootLevel_Study); + //m.ReplacePlainString(DICOM_TAG_PATIENT_ID, "coucou"); + //m.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou"); + + ParsedDicomFile o(true); + o.SaveToFile("UnitTestsResults/anon.dcm"); + + for (int i = 0; i < 10; i++) + { + char b[1024]; + sprintf(b, "UnitTestsResults/anon%06d.dcm", i); + std::unique_ptr f(o.Clone(false)); + if (i > 4) + o.ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, "coucou"); + m.Apply(*f); + f->SaveToFile(b); + } +} +#endif + + +TEST(DicomModification, Anonymization) +{ + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName")); + + const DicomTag privateTag(0x0045, 0x1010); + const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020")); + ASSERT_TRUE(privateTag.IsPrivate()); + ASSERT_TRUE(privateTag2.IsPrivate()); + ASSERT_EQ(0x0031, privateTag2.GetGroup()); + ASSERT_EQ(0x1020, privateTag2.GetElement()); + + std::string s; + ParsedDicomFile o(true); + o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou"); + ASSERT_FALSE(o.GetTagValue(s, privateTag)); + o.Insert(privateTag, "private tag", false, "OrthancCreator"); + ASSERT_TRUE(o.GetTagValue(s, privateTag)); + ASSERT_STREQ("private tag", s.c_str()); + + ASSERT_FALSE(o.GetTagValue(s, privateTag2)); + ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException); + ASSERT_FALSE(o.GetTagValue(s, privateTag2)); + o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator"); + ASSERT_FALSE(o.GetTagValue(s, privateTag2)); + o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator"); + ASSERT_TRUE(o.GetTagValue(s, privateTag2)); + ASSERT_STREQ("hello", s.c_str()); + o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator"); + ASSERT_TRUE(o.GetTagValue(s, privateTag2)); + ASSERT_STREQ("hello world", s.c_str()); + + ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME)); + ASSERT_FALSE(Toolbox::IsUuid(s)); + + DicomModification m; + m.SetupAnonymization(DicomVersion_2008); + m.Keep(privateTag); + + m.Apply(o); + + ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(Toolbox::IsUuid(s)); + ASSERT_TRUE(o.GetTagValue(s, privateTag)); + ASSERT_STREQ("private tag", s.c_str()); + + m.SetupAnonymization(DicomVersion_2008); + m.Apply(o); + ASSERT_FALSE(o.GetTagValue(s, privateTag)); +} + + +#include + +TEST(DicomModification, Png) +{ + // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image) + std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + + std::string m, cc; + ASSERT_TRUE(Toolbox::DecodeDataUriScheme(m, cc, s)); + + ASSERT_EQ("image/png", m); + + PngReader reader; + reader.ReadFromMemory(cc); + + ASSERT_EQ(5u, reader.GetHeight()); + ASSERT_EQ(5u, reader.GetWidth()); + ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat()); + + ParsedDicomFile o(true); + o.EmbedContent(s); + +#if ORTHANC_SANDBOXED != 1 + o.SaveToFile("UnitTestsResults/png1.dcm"); +#endif + + // Red dot, without alpha channel + s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII="; + o.EmbedContent(s); + +#if ORTHANC_SANDBOXED != 1 + o.SaveToFile("UnitTestsResults/png2.dcm"); +#endif + + // Check box in Graylevel8 + s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="; + o.EmbedContent(s); + //o.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing); + +#if ORTHANC_SANDBOXED != 1 + o.SaveToFile("UnitTestsResults/png3.dcm"); +#endif + + + { + // Gradient in Graylevel16 + + ImageBuffer img; + img.SetWidth(256); + img.SetHeight(256); + img.SetFormat(PixelFormat_Grayscale16); + + ImageAccessor accessor; + img.GetWriteableAccessor(accessor); + + uint16_t v = 0; + for (unsigned int y = 0; y < img.GetHeight(); y++) + { + uint16_t *p = reinterpret_cast(accessor.GetRow(y)); + for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++) + { + *p = v; + } + } + + o.EmbedImage(accessor); + +#if ORTHANC_SANDBOXED != 1 + o.SaveToFile("UnitTestsResults/png4.dcm"); +#endif + + // From http://www.schaik.com/pngsuite/pngsuite_bas_png.html + // 16Bit RGBA PNG + // License http://www.schaik.com/pngsuite/PngSuite.LICENSE + s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgEAYAAAAj6qa3AAAABGdBTUEAAYagMeiWXwAADSJJREFUeJzdmV9sHNd1xn/zj7NLck0u5VqOSwSgrIcEkQDKtNvYxlKJAstNEIgWIFkuUtQyWsCQW8mKlAJecf1iLLUGWsmKDCgwUMByigC25UKh0SaIXNMpiSiJHZoERAN+kEQ0lR1LkLhLL8nd4fzrwzl3qVVVNI9BHhbfzp07d+537r3nfOeMlaZpCtB8FwCaE+3YmLh9+x/LfStNG/8hfzPfgN6x5iZ98P/B5ubfr98fWn/TD5rvZrbVRt01W/AsQGYuMwf5clqWxnRMMDH4N4LxccFI28O/F3T12tHnnW8JWj9U1PvsUjTv2aL41zr+TxT1fvT0Le97RPGQYPBrRb3fHFU013/ZIr4pc6FaguZIZhxuMkCqNhLq2VK2BL3ldFiJTynerxM7rBPSdm9SJ6SjuM8I2nrf1vvWvYpP6du0PTXj36P4RPv4kRm/T3FECU+1YzOr+KhgY8oQb5Szo7USNDdl5gCCCX8buGunJDmmU1GbCfXO4c5hyJfTfu31VTWArmD0r4rzOrFP1AC2oPNFNcBDSvwLOp8HFHUnpfp8ohj/VsdNdNw/FVz9MyX8J4rPKuHLSlOfX5k3xFcmOwvVEjTHMqMAzdHMGEDwqv9U2w5IdO1am11tJ9S7NnRtgN5yuqh3/0snWteJXtGJfqQTm1FD/LsaYlYNoe2WYqrtiV7HipHBh5W4XgerSvi6Eo6V5oLgcov48uWugVoJGlPZAqwZINjgXwZYnejY1maAeJ9ORU+52exmzYV695buLZAvpz/Vu6d1ohU1gK5EcF7Q03ZH0VaXy48Uv6Pj6P34Ax1Hr1cVAzV88w0lrO3LvxNcmjXEl2a6B6slWFno7ANoTmaGAYLf+PcDBL/2/xwg/IG3r90ApxR1U5pTbja7WXOhnjuSOwK95eTv1AA6wXDrLRP+J0FXr+29gtb7OpoeheRVHUfPcHj4lnH+Qonr9fK/CNY/N8TrR3PFWgmW7+76DKARZx2AYMovAATH/MMA4WbvAkD4Je/jNh8QbVfUI9ByP3rKzWY3ay7Ue3p6eiBfTvSsRpHgqtmqDUHPE3QcNYClBkh1dN3KYajEA8GGPr+8rDR1Fost4ouLPXdUS7Bc6SoCrOztPA3QzGXqNxsgHPHGAcJN3hxAeM7b3rYDIvUBJqAZv27cmznlZrObNRfq+Xw+D73l5EkdRb10U3FF0VW0dqoBduhoxqvr8w29XlJcVKyOGOLVar63VoJ6PZcDWKl0FgGab2T2AAT9/hWA1cmOYYBwzBsFiH7ufg0gmnDbfYBjApiJtMYg6teNezOn3Gx2s+ZCfd3b696GfDk6p4ReVAPoynlK0Nb7iXr18DUl/leC9ecEa9rvRov4jR3rxqslWDzRcxBgebmrC6BZyRQBgkW/B2B1taMDIAw8HyCaczcBRE+7rwDEE067AWxdCyNhTCQ3Ac34dePezOk0m92suVBfv339dugth3NqAPXStf/Ut9zicZpNJa5xfuFTwavvGOJXz61/pFaC2kjvOMBStTsP0GhkswDBUf8IwOpDHecBoofdXwBEl9x7AaJT7j6AuMf5HCD5Z/tv23yArafYaDcjYUwkNwHN+HXj3swpN5vdrLlQ7z/Ufwjy5aYepKoqPrdXzVUTNF78+lnBKy8Z4leO93+3WoKFT/vuAag/l3sRoDGTHQQIAt8HCCteESB8wXseIJp3BwDi3wqD5An7dYD4J8IwmbBv2QH7BY1oNdrNSBgTyU1AM37duDdzys1mN2su1De+vPFl6C0vTkvrfz+m5lLC1+4QvHjAEL+4f+PJWgmun71zJ0C9L7cA0Phl9kGA4Fn/BEAYygyiilsEiD9wHgCIv+x8DJCctx8ESMbsUYB0s3UBIHnZ3t9mAEvPvFHrLdGq2s1IGBPJTUAzft24N3PKzWY3ay7Uh4aGhiBfvnxJ71YFpz80xKenh+6rluDagbtOAiwVuicBmlszkwDha/KGqOrmAeJYZpB83z4IkKyzbwAkX7V/BZBOWcMA6SVhlG6y5gDSCat9B/BNRY37Rq0b0Wq0m5EwJpKbgGb8unFv5pSbzW7WXKjvfnz349BbfnyPIX7mzd27aiWoVvN5gODH/k6AqCYjxIGTAUhG7HGA9Kz1GECaygzSilUE4Dsyw3TeGgBgigJA+qoyeksZ/pRvtvkAVNu18jM9Ai21brSWajcjYUwkNwHN+PVWvqin3Gx2s+Zr1GWE3bvOvAmQz0sP/0BwUgwoIzin4mcA7HJSArAeS88CWJY8bx1NjwAwyyCAVUnFIJMMA1hPpSK2dvEWAP/AP94+GzSJqckFTJqiO8CIVqPdjIQxkdwENOPXjXszp1w2+5k3YfeuWmn3Lvl/5q0zb+1+HGDovulpgLveu7YNoHtwaQYgM9ncCuA9KW9wq1EewHFkBnYlKQLYM8kWAHs+GQCwptICgDWWjgJYYeoBWBNp+xFIjDTRjLyVmJr8zKQrqtZbotVoNyNhTCTXgGb8uri36WkYuq9aqlYhn5dftQpD98m96Q+nPxwaAth48uJ+gDt3ygi5BRkxW2/kAPwTkgl6nszArURFAOcBmaEzHw8A2JNJAcB+XVJs64fC0H4lebrdAForMaUIk5G3ElM1gElTWmrdiFbdAUbCmEguAe3ifth4sla6dgDuOgnBj8HfCf4BCE7CXe/BtW2w8aT0vXjg4oGNLwM05zMDAH33yIi5F+UN2cHGDIDvywy8Y+H3ALznwxcA3MvRBgAnit2bDeBsji8A2Elit9cDDiphU4MxuYBR+SYxvSU/M2rdiFaj3UTCXDkO/d+tlq6fhTt3wlIBuifFM7i98otq0D0ISzPS5/pZCZ6ZAbjy0pWX+g8BhI945wCCEX8coHt2aRAgm5UZ+JWgCNAxu7oFwJ2PBgBcWxi4+6JTAM6meA7APpEcbM8G1Qe0ik+mBmNKEUYJ3pKfmTTFqHURrVfPwfpHaqWFT6HvHjkQuQVoboXMJMQBOBmJFfEz0tbcKn3qffLMwqdS+vLOwdV3rr6zfjtANO7uAAjf874Oa5I3c7R5BMDvkRl2fLT6FQDvE2HgjkUlAPcVYehMxO0+IPq2oskF9Ay3ajAmvzMZuRrC5GeSptzYAevGq6XaCPSOy4HIvQiNX0L2QXGR3pOQjIA9DnYZkpK0ha9Btg6NnDxTfw6CEfDHIRoHdwfcGLkxsu5tgKTXrgJEkevCTUpwj/cGgH8l6AfoeF8YeOMi1t2vRT8HcP8t+nabAUI9u61yo5G2WnwyNZhWKUIzcklMq1XI99ZKiyeg5yAsVaE7D40ZyA5K6co/AVEV3DykZ8F6TH7pWXCrEOWlT/CsPNOYge5ZWBqE8D3wvg5JL9hVqNaqNdELSWLbsKYEo9PuXoCo7uYAokl3+GYDeL8LvwDgHQqPt/mA8EuKps5qyo1adTPFJ1ODkVLE4iL03FEt1euQy8mB6OoSz5DNiov0fYkVnidB03Ek9luW/NJU2uJY+oShPBMEMkajISlXR4fknq4rSbhtw+Lni5/39AAkFbsIkHxm3w0QO04MEE25BQD/cHAMIDruHoLbpMPhbiVsCsymzqrpr9H2EtfrRyFXrJWWK9BVlH3RWYRmBTJFCI6Cf0RihleUKoJbhOT7YB+EtAJWEayjkB4BuwJJEdwKREXwjkH4PfArEBQhcxSatxuvAnYR6pV6JXcEIB0UzZr02QsA8ZRTAIjLTgnA3xxcAIj3OT9oM8CqOsFWZV3jvKmzSrlxaQa6B6ul5buh6zNY2Qudp8UzZPaIi/R7YPUh6DgP4QvgPS/qwXkAknVg30D05I+AWWBQMo1ki/SJP5BnwhegYxZWt8iYwSKEe8B7A6LT4O6F5DOw74Z0UMT60uzSbPcWgHTAugyQTNrDAMnP7EcBkk32HNymHhBoXG99UtDKuhSYly9D10CttLIAnX1yILIONHOQqUPQD/6Vm7bqw+D+QupJ7gDEXwbnYymx2r8SfWkNgFWBtAj2PCQD4MxDPADuZYg2gDsP0QB0fASrX5F3BP0Q1cHNQeyAE0PSB/YCpANgXYbl+eX5rg0A6ZRVaDOAZoXJMftwmw8ItOhpvqXIJ4WVSegsVEuNKcgWoDkJmWE5IH5hDVcnoWMYwgA8H6JL4N4rMsr5IiTnwX5QBLY1DEwBBWASGAZrCtKCJOFJAZwIYlfKMVEC3icSkDvel7gUTYI7LGrFLUA8BU4Bkkmwh/U9BViZWpnqlGxwzJJ0WLPB/1UPMAUN+YjUKEN2tFZqjkFmVMySGYXgN+DfD8Ex8A9LrPDGIRwDbxSiOXA3QXQK3H2iJ+3X5WuDPQrpJUm001cl37Se0v9jkI5q3yfW0N2nY41BVNJ3jayhf1jmEpfBKUHyM7AfXcN0DKxRaIw1xrIlgPSCJP7puDUCVppmtinxCfNxNHNBPiZm5/5vbG7+/fr9ofVvbgb5NJbZ1ny3NmqZZLb5LmS2iRluxsYEZG/T/kdx/xvwP2XY7MOt27XzAAAAAElFTkSuQmCC"; + o.EmbedContent(s); + +#if ORTHANC_SANDBOXED != 1 + o.SaveToFile("UnitTestsResults/png5.dcm"); +#endif + } +} + + +TEST(FromDcmtkBridge, Encodings1) +{ + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + std::string source(testEncodingsEncoded[i]); + std::string expected(testEncodingsExpected[i]); + std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false); + //std::cout << EnumerationToString(testEncodings[i]) << std::endl; + EXPECT_EQ(expected, s); + } +} + + +TEST(FromDcmtkBridge, Enumerations) +{ + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2 + Encoding e; + + ASSERT_FALSE(GetDicomEncoding(e, "")); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 6")); ASSERT_EQ(Encoding_Ascii, e); + + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-2 + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 100")); ASSERT_EQ(Encoding_Latin1, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 101")); ASSERT_EQ(Encoding_Latin2, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 109")); ASSERT_EQ(Encoding_Latin3, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 110")); ASSERT_EQ(Encoding_Latin4, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 144")); ASSERT_EQ(Encoding_Cyrillic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 127")); ASSERT_EQ(Encoding_Arabic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 126")); ASSERT_EQ(Encoding_Greek, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 138")); ASSERT_EQ(Encoding_Hebrew, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 148")); ASSERT_EQ(Encoding_Latin5, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 13")); ASSERT_EQ(Encoding_Japanese, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 166")); ASSERT_EQ(Encoding_Thai, e); + + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-3 + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 6")); ASSERT_EQ(Encoding_Ascii, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 100")); ASSERT_EQ(Encoding_Latin1, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 101")); ASSERT_EQ(Encoding_Latin2, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 109")); ASSERT_EQ(Encoding_Latin3, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 110")); ASSERT_EQ(Encoding_Latin4, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 144")); ASSERT_EQ(Encoding_Cyrillic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 127")); ASSERT_EQ(Encoding_Arabic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 126")); ASSERT_EQ(Encoding_Greek, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 138")); ASSERT_EQ(Encoding_Hebrew, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 148")); ASSERT_EQ(Encoding_Latin5, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 13")); ASSERT_EQ(Encoding_Japanese, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 166")); ASSERT_EQ(Encoding_Thai, e); + + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-4 + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 87")); ASSERT_EQ(Encoding_JapaneseKanji, e); + ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 159")); //ASSERT_EQ(Encoding_JapaneseKanjiSupplementary, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 149")); ASSERT_EQ(Encoding_Korean, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 58")); ASSERT_EQ(Encoding_SimplifiedChinese, e); + + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-5 + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 192")); ASSERT_EQ(Encoding_Utf8, e); + ASSERT_TRUE(GetDicomEncoding(e, "GB18030")); ASSERT_EQ(Encoding_Chinese, e); + ASSERT_TRUE(GetDicomEncoding(e, "GBK")); ASSERT_EQ(Encoding_Chinese, e); + + // common spelling mistakes + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR_100")); ASSERT_EQ(Encoding_Latin1, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_2022_IR_6")); ASSERT_EQ(Encoding_Ascii, e); +} + + +TEST(FromDcmtkBridge, Encodings3) +{ + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + //std::cout << EnumerationToString(testEncodings[i]) << std::endl; + std::string dicom; + + { + ParsedDicomFile f(true); + f.SetEncoding(testEncodings[i]); + + std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); + f.Insert(DICOM_TAG_PATIENT_NAME, s, false, ""); + f.SaveToMemoryBuffer(dicom); + } + + if (testEncodings[i] != Encoding_Windows1251) + { + ParsedDicomFile g(dicom); + + if (testEncodings[i] != Encoding_Ascii) + { + bool hasCodeExtensions; + ASSERT_EQ(testEncodings[i], g.DetectEncoding(hasCodeExtensions)); + ASSERT_FALSE(hasCodeExtensions); + } + + std::string tag; + ASSERT_TRUE(g.GetTagValue(tag, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(std::string(testEncodingsExpected[i]), tag); + } + } +} + + +TEST(FromDcmtkBridge, ValueRepresentation) +{ + ASSERT_EQ(ValueRepresentation_PersonName, + FromDcmtkBridge::LookupValueRepresentation(DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(ValueRepresentation_Date, + FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0020) /* StudyDate */)); + ASSERT_EQ(ValueRepresentation_Time, + FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0030) /* StudyTime */)); + ASSERT_EQ(ValueRepresentation_DateTime, + FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x002a) /* AcquisitionDateTime */)); + ASSERT_EQ(ValueRepresentation_NotSupported, + FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0001, 0x0001) /* some private tag */)); +} + + +TEST(FromDcmtkBridge, ParseListOfTags) +{ + {// nominal test + std::string source = "0010,0010;PatientBirthDate;0020,0020"; + std::set result; + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_NAME) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_BIRTH_DATE) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ORIENTATION) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ID) == result.end()); + + // serialize to string + std::string serialized; + FromDcmtkBridge::FormatListOfTags(serialized, result); + ASSERT_EQ("0010,0010;0010,0030;0020,0020", serialized); + } + + {// no tag + std::string source = ""; + std::set result; + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_EQ(0u, result.size()); + } + + {// invalid tag + std::string source = "0010,0010;Patient-BirthDate;0020,0020"; + std::set result; + + ASSERT_THROW(FromDcmtkBridge::ParseListOfTags(result, source), OrthancException); + } + + {// duplicate tag only once + std::string source = "0010,0010;PatientName"; + std::set result; + + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_EQ(1u, result.size()); + } + + {// Json + Json::Value source = Json::arrayValue; + source.append("0010,0010"); + source.append("PatientBirthDate"); + source.append("0020,0020"); + std::set result; + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_NAME) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_BIRTH_DATE) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ORIENTATION) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ID) == result.end()); + } + + +} + +static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110); +static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120); + +static void CreateSampleJson(Json::Value& a) +{ + { + Json::Value b = Json::objectValue; + b["PatientName"] = "Hello"; + b["PatientID"] = "World"; + b["StudyDescription"] = "Toto"; + a.append(b); + } + + { + Json::Value b = Json::objectValue; + b["PatientName"] = "data:application/octet-stream;base64,SGVsbG8y"; // echo -n "Hello2" | base64 + b["PatientID"] = "World2"; + a.append(b); + } +} + + + +TEST(ParsedDicomFile, InsertReplaceStrings) +{ + ParsedDicomFile f(true); + + f.Insert(DICOM_TAG_PATIENT_NAME, "World", false, ""); + ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false, ""), OrthancException); // Already existing tag + f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto"); // (*) + f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata"); // (**) + + DicomTransferSyntax syntax; + ASSERT_TRUE(f.LookupTransferSyntax(syntax)); + // The default transfer syntax depends on the OS endianness + ASSERT_TRUE(syntax == DicomTransferSyntax_LittleEndianExplicit || + syntax == DicomTransferSyntax_BigEndianExplicit); + + ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), + false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent, ""); + + std::string s; + ASSERT_FALSE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent, ""); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_EQ(s, "Accession"); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent, ""); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_EQ(s, "Accession2"); + f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent, ""); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_EQ(s, "Accession3"); + + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(s, "World"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_EQ(s, "Toto"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); // Implicitly modified by (*) + ASSERT_EQ(s, "Toto"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID)); + ASSERT_EQ(s, "Tata"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID)); // Implicitly modified by (**) + ASSERT_EQ(s, "Tata"); +} + + + + +TEST(ParsedDicomFile, InsertReplaceJson) +{ + ParsedDicomFile f(true); + + Json::Value a; + CreateSampleJson(a); + + ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + f.Remove(REFERENCED_STUDY_SEQUENCE); // No effect + f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""); + ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""), OrthancException); + f.Remove(REFERENCED_STUDY_SEQUENCE); + ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""); + ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE)); + + ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException); + ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent, ""); + ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent, ""); + ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE)); + + { + Json::Value b; + f.DatasetToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0); + + Json::Value c; + Toolbox::SimplifyDicomAsJson(c, b, DicomToJsonFormat_Human); + + ASSERT_EQ(0, c["ReferencedPatientSequence"].compare(a)); + ASSERT_NE(0, c["ReferencedStudySequence"].compare(a)); // Because Data URI Scheme decoding was enabled + } + + a = "data:application/octet-stream;base64,VGF0YQ=="; // echo -n "Tata" | base64 + f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent, ""); // (*) + f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent, ""); // (**) + + std::string s; + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_EQ(s, a.asString()); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); // Implicitly modified by (*) + ASSERT_EQ(s, a.asString()); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID)); + ASSERT_EQ(s, "Tata"); + ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID)); // Implicitly modified by (**) + ASSERT_EQ(s, "Tata"); +} + + +TEST(ParsedDicomFile, JsonEncoding) +{ + ParsedDicomFile f(true); + + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + if (testEncodings[i] != Encoding_Windows1251) + { + //std::cout << EnumerationToString(testEncodings[i]) << std::endl; + f.SetEncoding(testEncodings[i]); + + if (testEncodings[i] != Encoding_Ascii) + { + bool hasCodeExtensions; + ASSERT_EQ(testEncodings[i], f.DetectEncoding(hasCodeExtensions)); + ASSERT_FALSE(hasCodeExtensions); + } + + Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); + f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, ""); + + Json::Value v; + f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0); + ASSERT_EQ(v["PatientName"].asString(), std::string(testEncodingsExpected[i])); + } + } +} + + +TEST(ParsedDicomFile, ToJsonFlags1) +{ + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag", 1, 1, "OrthancCreator"); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, ""); + + ParsedDicomFile f(true); + f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false, ""); // Even group => public tag + f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false, ""); // Even group => public, unknown tag + f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false, "OrthancCreator"); // Odd group => private tag + + Json::Value v; + f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6u, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_EQ(Json::stringValue, v["7050,1000"].type()); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7u, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6u, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7u, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + std::string mime, content; + ASSERT_EQ(Json::stringValue, v["7053,1000"].type()); + ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7053,1000"].asString())); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ("Some private tag", content); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7u, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7052,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7u, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7052,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::stringValue, v["7052,1000"].type()); + ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7052,1000"].asString())); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ("Some unknown tag", content); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(8u, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); + ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); +} + + +TEST(ParsedDicomFile, ToJsonFlags2) +{ + ParsedDicomFile f(true); + + { + // "ParsedDicomFile" uses Little Endian => 'B' (least significant + // byte) will be stored first in the memory buffer and in the + // file, then 'A'. Hence the expected "BA" value below. + Uint16 v[] = { 'A' * 256 + 'B', 0 }; + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint16Array(DCM_PixelData, v, 2).good()); + } + + Json::Value v; + f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(5u, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7fe0,0010")); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6u, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7fe0,0010")); + ASSERT_EQ(Json::nullValue, v["7fe0,0010"].type()); + + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6u, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7fe0,0010")); + ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type()); + ASSERT_EQ("BA", v["7fe0,0010"].asString().substr(0, 2)); + + f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(6u, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7fe0,0010")); + ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type()); + std::string mime, content; + ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString())); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ("BA", content.substr(0, 2)); +} + + +TEST(ParsedDicomFile, ToJsonFlags3) +{ + ParsedDicomFile f(false); + + { + Uint8 v[2] = { 0, 0 }; + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertString(DCM_PatientName, "HELLO^").good()); + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint32(DcmTag(0x4000, 0x0000), 42).good()); + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint8Array(DCM_PixelData, v, 2).good()); + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x07fe1, 0x0010), "WORLD^").good()); + } + + std::string s; + Toolbox::EncodeDataUriScheme(s, "application/octet-stream", std::string(2, '\0')); + + { + Json::Value v; + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_StopAfterPixelData), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(3u, v.size()); + ASSERT_EQ("HELLO^", v["0010,0010"].asString()); + ASSERT_EQ("42", v["4000,0000"].asString()); + ASSERT_EQ(s, v["7fe0,0010"].asString()); + } + + { + Json::Value v; + f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_SkipGroupLengths), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(2u, v.size()); + ASSERT_EQ("HELLO^", v["0010,0010"].asString()); + ASSERT_EQ("WORLD^", v["7fe1,0010"].asString()); + } +} + + +TEST(DicomFindAnswers, Basic) +{ + DicomFindAnswers a(false); + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_ID, "hello", false); + a.Add(m); + } + + { + ParsedDicomFile d(true); + d.ReplacePlainString(DICOM_TAG_PATIENT_ID, "my"); + a.Add(d); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_ID, "world", false); + a.Add(m); + } + + Json::Value j; + a.ToJson(j, DicomToJsonFormat_Human); + ASSERT_EQ(3u, j.size()); + + //std::cout << j; +} + + +TEST(ParsedDicomFile, FromJson) +{ + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7057, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag2", 1, 1, "ORTHANC"); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7059, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag3", 1, 1, ""); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1002), ValueRepresentation_PersonName, "Declared public tag2", 1, 1, ""); + + Json::Value v; + const std::string sopClassUid = "1.2.840.10008.5.1.4.1.1.1"; // CR Image Storage: + + // Test the private creator + ASSERT_EQ(DcmTag_ERROR_TagName, FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "NOPE")); + ASSERT_EQ("MyPrivateTag2", FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "ORTHANC")); + + { + v["SOPClassUID"] = sopClassUid; + v["SpecificCharacterSet"] = "ISO_IR 148"; // This is latin-5 + v["PatientName"] = "Sébastien"; + v["7050-1002"] = "Some public tag"; // Even group => public tag + v["7052-1000"] = "Some unknown tag"; // Even group => public, unknown tag + v["7057-1000"] = "Some private tag"; // Odd group => private tag + v["7059-1000"] = "Some private tag2"; // Odd group => private tag, with an odd length to test padding + + std::string s; + Toolbox::EncodeDataUriScheme(s, "application/octet-stream", "Sebastien"); + v["StudyDescription"] = s; + + v["PixelData"] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // A red dot of 5x5 pixels + v["0040,0100"] = Json::arrayValue; // ScheduledProcedureStepSequence + + Json::Value vv; + vv["Modality"] = "MR"; + v["0040,0100"].append(vv); + + vv["Modality"] = "CT"; + v["0040,0100"].append(vv); + } + + const DicomToJsonFlags toJsonFlags = static_cast(DicomToJsonFlags_IncludeBinary | + DicomToJsonFlags_IncludePixelData | + DicomToJsonFlags_IncludePrivateTags | + DicomToJsonFlags_IncludeUnknownTags | + DicomToJsonFlags_ConvertBinaryToAscii); + + + { + std::unique_ptr dicom + (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_GenerateIdentifiers), "")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Human, toJsonFlags, 0); + + ASSERT_EQ(vv["SOPClassUID"].asString(), sopClassUid); + ASSERT_EQ(vv["MediaStorageSOPClassUID"].asString(), sopClassUid); + ASSERT_TRUE(vv.isMember("SOPInstanceUID")); + ASSERT_TRUE(vv.isMember("SeriesInstanceUID")); + ASSERT_TRUE(vv.isMember("StudyInstanceUID")); + ASSERT_TRUE(vv.isMember("PatientID")); + } + + + { + std::unique_ptr dicom + (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_GenerateIdentifiers), "")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Human, static_cast(DicomToJsonFlags_IncludePixelData), 0); + + std::string mime, content; + ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, vv["PixelData"].asString())); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ(5u * 5u * 3u /* the red dot is 5x5 pixels in RGB24 */ + 1 /* for padding */, content.size()); + } + + + { + std::unique_ptr dicom + (ParsedDicomFile::CreateFromJson(v, static_cast(DicomFromJsonFlags_DecodeDataUriScheme), "")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, toJsonFlags, 0); + + ASSERT_FALSE(vv.isMember("SOPInstanceUID")); + ASSERT_FALSE(vv.isMember("SeriesInstanceUID")); + ASSERT_FALSE(vv.isMember("StudyInstanceUID")); + ASSERT_FALSE(vv.isMember("PatientID")); + ASSERT_EQ(2u, vv["0040,0100"].size()); + ASSERT_EQ("MR", vv["0040,0100"][0]["0008,0060"].asString()); + ASSERT_EQ("CT", vv["0040,0100"][1]["0008,0060"].asString()); + ASSERT_EQ("Some public tag", vv["7050,1002"].asString()); + ASSERT_EQ("Some unknown tag", vv["7052,1000"].asString()); + ASSERT_EQ("Some private tag", vv["7057,1000"].asString()); + ASSERT_EQ("Some private tag2", vv["7059,1000"].asString()); + ASSERT_EQ("Sébastien", vv["0010,0010"].asString()); + ASSERT_EQ("Sebastien", vv["0008,1030"].asString()); + ASSERT_EQ("ISO_IR 148", vv["0008,0005"].asString()); + ASSERT_EQ("5", vv[DICOM_TAG_ROWS.Format()].asString()); + ASSERT_EQ("5", vv[DICOM_TAG_COLUMNS.Format()].asString()); + ASSERT_TRUE(vv[DICOM_TAG_PIXEL_DATA.Format()].asString().empty()); + } +} + + + +TEST(TestImages, PatternGrayscale8) +{ + Orthanc::Image image(Orthanc::PixelFormat_Grayscale8, 256, 256, false); + + for (int y = 0; y < 256; y++) + { + uint8_t *p = reinterpret_cast(image.GetRow(y)); + for (int x = 0; x < 256; x++, p++) + { + *p = y; + } + } + + Orthanc::ImageAccessor r; + + image.GetRegion(r, 32, 32, 64, 192); + Orthanc::ImageProcessing::Set(r, 0); + + image.GetRegion(r, 160, 32, 64, 192); + Orthanc::ImageProcessing::Set(r, 255); + + std::string saved; + + { + ParsedDicomFile f(true); + f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7"); + f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998"); + f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC"); + f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc"); + f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns"); + f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale8"); + f.EmbedImage(image); + + f.SaveToMemoryBuffer(saved); + } + + { + Orthanc::ParsedDicomFile f(saved); + + std::unique_ptr decoded(f.DecodeFrame(0)); + ASSERT_EQ(256u, decoded->GetWidth()); + ASSERT_EQ(256u, decoded->GetHeight()); + ASSERT_EQ(Orthanc::PixelFormat_Grayscale8, decoded->GetFormat()); + + for (int y = 0; y < 256; y++) + { + const void* a = image.GetConstRow(y); + const void* b = decoded->GetConstRow(y); + ASSERT_EQ(0, memcmp(a, b, 256)); + } + } +} + + +TEST(TestImages, PatternRGB) +{ + Orthanc::Image image(Orthanc::PixelFormat_RGB24, 384, 256, false); + + for (int y = 0; y < 256; y++) + { + uint8_t *p = reinterpret_cast(image.GetRow(y)); + for (int x = 0; x < 128; x++, p += 3) + { + p[0] = y; + p[1] = 0; + p[2] = 0; + } + for (int x = 128; x < 128 * 2; x++, p += 3) + { + p[0] = 0; + p[1] = 255 - y; + p[2] = 0; + } + for (int x = 128 * 2; x < 128 * 3; x++, p += 3) + { + p[0] = 0; + p[1] = 0; + p[2] = y; + } + } + + std::string saved; + + { + ParsedDicomFile f(true); + f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7"); + f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998"); + f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC"); + f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc"); + f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns"); + f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "RGB24"); + f.EmbedImage(image); + + f.SaveToMemoryBuffer(saved); + } + + { + Orthanc::ParsedDicomFile f(saved); + + std::unique_ptr decoded(f.DecodeFrame(0)); + ASSERT_EQ(384u, decoded->GetWidth()); + ASSERT_EQ(256u, decoded->GetHeight()); + ASSERT_EQ(Orthanc::PixelFormat_RGB24, decoded->GetFormat()); + + for (int y = 0; y < 256; y++) + { + const void* a = image.GetConstRow(y); + const void* b = decoded->GetConstRow(y); + ASSERT_EQ(0, memcmp(a, b, 3 * 384)); + } + } +} + + +TEST(TestImages, PatternUint16) +{ + Orthanc::Image image(Orthanc::PixelFormat_Grayscale16, 256, 256, false); + + uint16_t v = 0; + for (int y = 0; y < 256; y++) + { + uint16_t *p = reinterpret_cast(image.GetRow(y)); + for (int x = 0; x < 256; x++, v++, p++) + { + *p = v; + } + } + + Orthanc::ImageAccessor r; + + image.GetRegion(r, 32, 32, 64, 192); + Orthanc::ImageProcessing::Set(r, 0); + + image.GetRegion(r, 160, 32, 64, 192); + Orthanc::ImageProcessing::Set(r, 65535); + + std::string saved; + + { + ParsedDicomFile f(true); + f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7"); + f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998"); + f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC"); + f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc"); + f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns"); + f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale16"); + f.EmbedImage(image); + + f.SaveToMemoryBuffer(saved); + } + + { + Orthanc::ParsedDicomFile f(saved); + + std::unique_ptr decoded(f.DecodeFrame(0)); + ASSERT_EQ(256u, decoded->GetWidth()); + ASSERT_EQ(256u, decoded->GetHeight()); + ASSERT_EQ(Orthanc::PixelFormat_Grayscale16, decoded->GetFormat()); + + for (int y = 0; y < 256; y++) + { + const void* a = image.GetConstRow(y); + const void* b = decoded->GetConstRow(y); + ASSERT_EQ(0, memcmp(a, b, 512)); + } + } +} + + +TEST(TestImages, PatternInt16) +{ + Orthanc::Image image(Orthanc::PixelFormat_SignedGrayscale16, 256, 256, false); + + int16_t v = -32768; + for (int y = 0; y < 256; y++) + { + int16_t *p = reinterpret_cast(image.GetRow(y)); + for (int x = 0; x < 256; x++, v++, p++) + { + *p = v; + } + } + + Orthanc::ImageAccessor r; + image.GetRegion(r, 32, 32, 64, 192); + Orthanc::ImageProcessing::Set(r, -32768); + + image.GetRegion(r, 160, 32, 64, 192); + Orthanc::ImageProcessing::Set(r, 32767); + + std::string saved; + + { + ParsedDicomFile f(true); + f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7"); + f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998"); + f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC"); + f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc"); + f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns"); + f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "SignedGrayscale16"); + f.EmbedImage(image); + + f.SaveToMemoryBuffer(saved); + } + + { + Orthanc::ParsedDicomFile f(saved); + + std::unique_ptr decoded(f.DecodeFrame(0)); + ASSERT_EQ(256u, decoded->GetWidth()); + ASSERT_EQ(256u, decoded->GetHeight()); + ASSERT_EQ(Orthanc::PixelFormat_SignedGrayscale16, decoded->GetFormat()); + + for (int y = 0; y < 256; y++) + { + const void* a = image.GetConstRow(y); + const void* b = decoded->GetConstRow(y); + ASSERT_EQ(0, memcmp(a, b, 512)); + } + } +} + + + +static void CheckEncoding(ParsedDicomFile& dicom, + Encoding expected) +{ + const char* value = NULL; + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_SpecificCharacterSet, value).good()); + + Encoding encoding; + ASSERT_TRUE(GetDicomEncoding(encoding, value)); + ASSERT_EQ(expected, encoding); +} + + +TEST(ParsedDicomFile, DicomMapEncodings1) +{ + SetDefaultDicomEncoding(Encoding_Ascii); + ASSERT_EQ(Encoding_Ascii, GetDefaultDicomEncoding()); + + { + DicomMap m; + ParsedDicomFile dicom(m, GetDefaultDicomEncoding(), false); + ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card()); + CheckEncoding(dicom, Encoding_Ascii); + } + + { + DicomMap m; + ParsedDicomFile dicom(m, Encoding_Latin4, false); + ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card()); + CheckEncoding(dicom, Encoding_Latin4); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false); + ParsedDicomFile dicom(m, GetDefaultDicomEncoding(), false); + ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card()); + CheckEncoding(dicom, Encoding_Latin5); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false); + ParsedDicomFile dicom(m, Encoding_Latin1, false); + ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card()); + CheckEncoding(dicom, Encoding_Latin5); + } +} + + +TEST(ParsedDicomFile, DicomMapEncodings2) +{ + const char* utf8 = NULL; + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + if (testEncodings[i] == Encoding_Utf8) + { + utf8 = testEncodingsEncoded[i]; + break; + } + } + + ASSERT_TRUE(utf8 != NULL); + + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + // 1251 codepage is not supported by the core DICOM standard, ignore it + if (testEncodings[i] != Encoding_Windows1251) + { + { + // Sanity check to test the proper behavior of "EncodingTests.py" + std::string encoded = Toolbox::ConvertFromUtf8(testEncodingsExpected[i], testEncodings[i]); + ASSERT_STREQ(testEncodingsEncoded[i], encoded.c_str()); + std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i], false); + ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str()); + + if (testEncodings[i] != Encoding_Chinese) + { + // A specific source string is used in "EncodingTests.py" to + // test against Chinese, it is normal that it does not correspond to UTF8 + + const std::string tmp = Toolbox::ConvertToUtf8( + Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i], false); + ASSERT_STREQ(testEncodingsExpected[i], tmp.c_str()); + } + } + + + Json::Value v; + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false); + + ParsedDicomFile dicom(m, testEncodings[i], false); + + const char* encoded = NULL; + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, encoded).good()); + ASSERT_STREQ(testEncodingsEncoded[i], encoded); + + dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0); + + Encoding encoding; + ASSERT_TRUE(GetDicomEncoding(encoding, v["SpecificCharacterSet"].asCString())); + ASSERT_EQ(encoding, testEncodings[i]); + ASSERT_STREQ(testEncodingsExpected[i], v["PatientName"].asCString()); + } + + + { + DicomMap m; + m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(testEncodings[i]), false); + m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false); + + ParsedDicomFile dicom(m, testEncodings[i], false); + + Json::Value v2; + dicom.DatasetToJson(v2, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0); + + ASSERT_EQ(v2["PatientName"].asString(), v["PatientName"].asString()); + ASSERT_EQ(v2["SpecificCharacterSet"].asString(), v["SpecificCharacterSet"].asString()); + } + } + } +} + + +TEST(ParsedDicomFile, ChangeEncoding) +{ + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + // 1251 codepage is not supported by the core DICOM standard, ignore it + if (testEncodings[i] != Encoding_Windows1251) + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false); + + std::string tag; + + ParsedDicomFile dicom(m, Encoding_Utf8, false); + bool hasCodeExtensions; + ASSERT_EQ(Encoding_Utf8, dicom.DetectEncoding(hasCodeExtensions)); + ASSERT_FALSE(hasCodeExtensions); + ASSERT_TRUE(dicom.GetTagValue(tag, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(tag, testEncodingsExpected[i]); + + { + Json::Value v; + dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0); + ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), "ISO_IR 192"); + ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]); + } + + dicom.ChangeEncoding(testEncodings[i]); + + ASSERT_EQ(testEncodings[i], dicom.DetectEncoding(hasCodeExtensions)); + ASSERT_FALSE(hasCodeExtensions); + + const char* c = NULL; + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, c).good()); + EXPECT_STREQ(c, testEncodingsEncoded[i]); + + ASSERT_TRUE(dicom.GetTagValue(tag, DICOM_TAG_PATIENT_NAME)); // Decodes to UTF-8 + EXPECT_EQ(tag, testEncodingsExpected[i]); + + { + Json::Value v; + dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0); + ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), GetDicomSpecificCharacterSet(testEncodings[i])); + ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]); + } + } + } +} + + +TEST(Toolbox, CaseWithAccents) +{ + ASSERT_EQ(toUpperResult, Toolbox::ToUpperCaseWithAccents(toUpperSource)); +} + + + +TEST(ParsedDicomFile, InvalidCharacterSets) +{ + { + // No encoding provided, fallback to default encoding + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false); + + ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false); + + bool hasCodeExtensions; + ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions)); + ASSERT_FALSE(hasCodeExtensions); + } + + { + // Valid encoding, "ISO_IR 13" is Japanese + DicomMap m; + m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", false); + m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false); + + ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false); + + bool hasCodeExtensions; + ASSERT_EQ(Encoding_Japanese, d.DetectEncoding(hasCodeExtensions)); + ASSERT_FALSE(hasCodeExtensions); + } + + { + // Invalid value for an encoding ("nope" is not in the DICOM standard) + DicomMap m; + m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "nope", false); + m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false); + + ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3, false), + OrthancException); + } + + { + // Invalid encoding, as provided as a binary string + DicomMap m; + m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", true); + m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false); + + ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3, false), + OrthancException); + } + + { + // Encoding provided as an empty string, fallback to default encoding + // In Orthanc <= 1.3.1, this test was throwing an exception + DicomMap m; + m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "", false); + m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false); + + ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false); + + bool hasCodeExtensions; + ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions)); + ASSERT_FALSE(hasCodeExtensions); + } +} + + + +TEST(ParsedDicomFile, FloatPrecision) +{ + Float32 v; + + switch (Toolbox::DetectEndianness()) + { + case Endianness_Little: + reinterpret_cast(&v)[3] = 0x4E; + reinterpret_cast(&v)[2] = 0x9C; + reinterpret_cast(&v)[1] = 0xAD; + reinterpret_cast(&v)[0] = 0x8F; + break; + + case Endianness_Big: + reinterpret_cast(&v)[0] = 0x4E; + reinterpret_cast(&v)[1] = 0x9C; + reinterpret_cast(&v)[2] = 0xAD; + reinterpret_cast(&v)[3] = 0x8F; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + ParsedDicomFile f(false); + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertFloat32(DCM_ExaminedBodyThickness /* VR: FL */, v).good()); + + { + Float32 u; + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->findAndGetFloat32(DCM_ExaminedBodyThickness, u).good()); + ASSERT_FLOAT_EQ(u, v); + ASSERT_TRUE(memcmp(&u, &v, 4) == 0); + } + + { + Json::Value json; + f.DatasetToJson(json, DicomToJsonFormat_Short, DicomToJsonFlags_None, 256); + ASSERT_EQ("1314310016", json["0010,9431"].asString()); + } + + { + DicomMap summary; + f.ExtractDicomSummary(summary, 256); + ASSERT_EQ("1314310016", summary.GetStringValue(DicomTag(0x0010, 0x9431), "nope", false)); + } + + { + // This flavor uses "Json::Value" serialization + DicomWebJsonVisitor visitor; + f.Apply(visitor); + Float32 u = visitor.GetResult() ["00109431"]["Value"][0].asFloat(); + ASSERT_FLOAT_EQ(u, v); + ASSERT_TRUE(memcmp(&u, &v, 4) == 0); + } +} + + + +TEST(Toolbox, RemoveIso2022EscapeSequences) +{ + // +----------------------------------+ + // | one-byte control messages | + // +----------------------------------+ + + static const uint8_t iso2022_cstr_oneByteControl[] = { + 0x0f, 0x41, + 0x0e, 0x42, + 0x8e, 0x1b, 0x4e, 0x43, + 0x8f, 0x1b, 0x4f, 0x44, + 0x8e, 0x1b, 0x4a, 0x45, + 0x8f, 0x1b, 0x4a, 0x46, + 0x50, 0x51, 0x52, 0x00 + }; + + static const uint8_t iso2022_cstr_oneByteControl_ref[] = { + 0x41, + 0x42, + 0x43, + 0x44, + 0x8e, 0x1b, 0x4a, 0x45, + 0x8f, 0x1b, 0x4a, 0x46, + 0x50, 0x51, 0x52, 0x00 + }; + + // +----------------------------------+ + // | two-byte control messages | + // +----------------------------------+ + + static const uint8_t iso2022_cstr_twoByteControl[] = { + 0x1b, 0x6e, 0x41, + 0x1b, 0x6f, 0x42, + 0x1b, 0x4e, 0x43, + 0x1b, 0x4f, 0x44, + 0x1b, 0x7e, 0x45, + 0x1b, 0x7d, 0x46, + 0x1b, 0x7c, 0x47, 0x00 + }; + + static const uint8_t iso2022_cstr_twoByteControl_ref[] = { + 0x41, + 0x42, + 0x43, + 0x44, + 0x45, + 0x46, + 0x47, 0x00 + }; + + // +----------------------------------+ + // | various-length escape sequences | + // +----------------------------------+ + + static const uint8_t iso2022_cstr_escapeSequence[] = { + 0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq) + 0x1b, 0x50, 0x42, // ditto + 0x1b, 0x7f, 0x43, // ditto + 0x1b, 0x21, 0x4a, 0x44, // this will match + 0x1b, 0x20, 0x21, 0x2f, 0x40, 0x45, // this will match + 0x1b, 0x20, 0x21, 0x2f, 0x2f, 0x40, 0x46, // this will match too + 0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match! + }; + + static const uint8_t iso2022_cstr_escapeSequence_ref[] = { + 0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq) + 0x1b, 0x50, 0x42, // ditto + 0x1b, 0x7f, 0x43, // ditto + 0x44, // this will match + 0x45, // this will match + 0x46, // this will match too + 0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match! + }; + + + // +----------------------------------+ + // | a real-world japanese sample | + // +----------------------------------+ + + static const uint8_t iso2022_cstr_real_ir13[] = { + 0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3, + 0x3d, 0x1b, 0x24, 0x42, 0x3b, 0x33, 0x45, 0x44, + 0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x42, + 0x40, 0x4f, 0x3a, 0x1b, 0x28, 0x4a, 0x3d, 0x1b, + 0x24, 0x42, 0x24, 0x64, 0x24, 0x5e, 0x24, 0x40, + 0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x24, + 0x3f, 0x24, 0x6d, 0x24, 0x26, 0x1b, 0x28, 0x4a, 0x00 + }; + + static const uint8_t iso2022_cstr_real_ir13_ref[] = { + 0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3, + 0x3d, + 0x3b, 0x33, 0x45, 0x44, + 0x5e, + 0x42, + 0x40, 0x4f, 0x3a, + 0x3d, + 0x24, 0x64, 0x24, 0x5e, 0x24, 0x40, + 0x5e, + 0x24, + 0x3f, 0x24, 0x6d, 0x24, 0x26, 0x00 + }; + + + + // +----------------------------------+ + // | the actual test | + // +----------------------------------+ + + std::string iso2022_str_oneByteControl( + reinterpret_cast(iso2022_cstr_oneByteControl)); + std::string iso2022_str_oneByteControl_ref( + reinterpret_cast(iso2022_cstr_oneByteControl_ref)); + std::string iso2022_str_twoByteControl( + reinterpret_cast(iso2022_cstr_twoByteControl)); + std::string iso2022_str_twoByteControl_ref( + reinterpret_cast(iso2022_cstr_twoByteControl_ref)); + std::string iso2022_str_escapeSequence( + reinterpret_cast(iso2022_cstr_escapeSequence)); + std::string iso2022_str_escapeSequence_ref( + reinterpret_cast(iso2022_cstr_escapeSequence_ref)); + std::string iso2022_str_real_ir13( + reinterpret_cast(iso2022_cstr_real_ir13)); + std::string iso2022_str_real_ir13_ref( + reinterpret_cast(iso2022_cstr_real_ir13_ref)); + + std::string dest; + + Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_oneByteControl); + ASSERT_EQ(dest, iso2022_str_oneByteControl_ref); + + Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_twoByteControl); + ASSERT_EQ(dest, iso2022_str_twoByteControl_ref); + + Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_escapeSequence); + ASSERT_EQ(dest, iso2022_str_escapeSequence_ref); + + Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_real_ir13); + ASSERT_EQ(dest, iso2022_str_real_ir13_ref); +} + + + +static std::string DecodeFromSpecification(const std::string& s) +{ + std::vector tokens; + Toolbox::TokenizeString(tokens, s, ' '); + + std::string result; + result.resize(tokens.size()); + + for (size_t i = 0; i < tokens.size(); i++) + { + std::vector components; + Toolbox::TokenizeString(components, tokens[i], '/'); + + if (components.size() != 2) + { + throw; + } + + int a = boost::lexical_cast(components[0]); + int b = boost::lexical_cast(components[1]); + if (a < 0 || a > 15 || + b < 0 || b > 15 || + (a == 0 && b == 0)) + { + throw; + } + + result[i] = static_cast(a * 16 + b); + } + + return result; +} + + + +// Compatibility wrapper +static pugi::xpath_node SelectNode(const pugi::xml_document& doc, + const char* xpath) +{ +#if PUGIXML_VERSION <= 140 + return doc.select_single_node(xpath); // Deprecated in pugixml 1.5 +#else + return doc.select_node(xpath); +#endif +} + + +TEST(Toolbox, EncodingsKorean) +{ + // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_I.2.html + + std::string korean = DecodeFromSpecification( + "04/08 06/15 06/14 06/07 05/14 04/07 06/09 06/12 06/04 06/15 06/14 06/07 03/13 " + "01/11 02/04 02/09 04/03 15/11 15/03 05/14 01/11 02/04 02/09 04/03 13/01 12/14 " + "13/04 13/07 03/13 01/11 02/04 02/09 04/03 12/08 10/11 05/14 01/11 02/04 02/09 " + "04/03 11/01 14/06 11/05 11/15"); + + // This array can be re-generated using command-line: + // echo -n "Hong^Gildong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"' + static const uint8_t utf8raw[] = { + 0x48, 0x6f, 0x6e, 0x67, 0x5e, 0x47, 0x69, 0x6c, 0x64, 0x6f, 0x6e, 0x67, 0x3d, 0xe6, + 0xb4, 0xaa, 0x5e, 0xe5, 0x90, 0x89, 0xe6, 0xb4, 0x9e, 0x3d, 0xed, 0x99, 0x8d, 0x5e, + 0xea, 0xb8, 0xb8, 0xeb, 0x8f, 0x99 + }; + + std::string utf8(reinterpret_cast(utf8raw), sizeof(utf8raw)); + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 149"); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString + (DCM_PatientName, korean.c_str(), OFBool(true)).good()); + + bool hasCodeExtensions; + Encoding encoding = dicom.DetectEncoding(hasCodeExtensions); + ASSERT_EQ(Encoding_Korean, encoding); + ASSERT_TRUE(hasCodeExtensions); + + std::string value; + ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(utf8, value); + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString()); + ASSERT_EQ(utf8.substr(13, 10), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString()); + ASSERT_EQ(utf8.substr(24), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString()); + +#if ORTHANC_ENABLE_PUGIXML == 1 + // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1 + std::string xml; + visitor.FormatXml(xml); + + pugi::xml_document doc; + doc.load_buffer(xml.c_str(), xml.size()); + + pugi::xpath_node node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value"); + ASSERT_STREQ("ISO 2022 IR 149", node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]"); + ASSERT_STREQ("CS", node.node().attribute("vr").value()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]"); + ASSERT_STREQ("PN", node.node().attribute("vr").value()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName"); + ASSERT_STREQ("Hong", node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName"); + ASSERT_STREQ("Gildong", node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName"); + ASSERT_EQ(utf8.substr(13, 3), node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName"); + ASSERT_EQ(utf8.substr(17, 6), node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName"); + ASSERT_EQ(utf8.substr(24, 3), node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName"); + ASSERT_EQ(utf8.substr(28), node.node().text().as_string()); +#endif + + { + DicomMap m; + m.FromDicomWeb(visitor.GetResult()); + ASSERT_EQ(2u, m.GetSize()); + + std::string s; + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false)); + ASSERT_EQ("ISO 2022 IR 149", s); + + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); + std::vector v; + Toolbox::TokenizeString(v, s, '='); + ASSERT_EQ(3u, v.size()); + ASSERT_EQ("Hong^Gildong", v[0]); + ASSERT_EQ(utf8, s); + } +} + + +TEST(Toolbox, EncodingsJapaneseKanji) +{ + // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_H.3.html + + std::string japanese = DecodeFromSpecification( + "05/09 06/01 06/13 06/01 06/04 06/01 05/14 05/04 06/01 07/02 06/15 07/05 03/13 " + "01/11 02/04 04/02 03/11 03/03 04/05 04/04 01/11 02/08 04/02 05/14 01/11 02/04 " + "04/02 04/02 04/00 04/15 03/10 01/11 02/08 04/02 03/13 01/11 02/04 04/02 02/04 " + "06/04 02/04 05/14 02/04 04/00 01/11 02/08 04/02 05/14 01/11 02/04 04/02 02/04 " + "03/15 02/04 06/13 02/04 02/06 01/11 02/08 04/02"); + + // This array can be re-generated using command-line: + // echo -n "Yamada^Tarou=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"' + static const uint8_t utf8raw[] = { + 0x59, 0x61, 0x6d, 0x61, 0x64, 0x61, 0x5e, 0x54, 0x61, 0x72, 0x6f, 0x75, 0x3d, 0xe5, + 0xb1, 0xb1, 0xe7, 0x94, 0xb0, 0x5e, 0xe5, 0xa4, 0xaa, 0xe9, 0x83, 0x8e, 0x3d, 0xe3, + 0x82, 0x84, 0xe3, 0x81, 0xbe, 0xe3, 0x81, 0xa0, 0x5e, 0xe3, 0x81, 0x9f, 0xe3, 0x82, + 0x8d, 0xe3, 0x81, 0x86 + }; + + std::string utf8(reinterpret_cast(utf8raw), sizeof(utf8raw)); + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 87"); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString + (DCM_PatientName, japanese.c_str(), OFBool(true)).good()); + + bool hasCodeExtensions; + Encoding encoding = dicom.DetectEncoding(hasCodeExtensions); + ASSERT_EQ(Encoding_JapaneseKanji, encoding); + ASSERT_TRUE(hasCodeExtensions); + + std::string value; + ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(utf8, value); + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString()); + ASSERT_EQ(utf8.substr(13, 13), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString()); + ASSERT_EQ(utf8.substr(27), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString()); + +#if ORTHANC_ENABLE_PUGIXML == 1 + // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1 + std::string xml; + visitor.FormatXml(xml); + + pugi::xml_document doc; + doc.load_buffer(xml.c_str(), xml.size()); + + pugi::xpath_node node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value"); + ASSERT_STREQ("ISO 2022 IR 87", node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]"); + ASSERT_STREQ("CS", node.node().attribute("vr").value()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]"); + ASSERT_STREQ("PN", node.node().attribute("vr").value()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName"); + ASSERT_STREQ("Yamada", node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName"); + ASSERT_STREQ("Tarou", node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName"); + ASSERT_EQ(utf8.substr(13, 6), node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName"); + ASSERT_EQ(utf8.substr(20, 6), node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName"); + ASSERT_EQ(utf8.substr(27, 9), node.node().text().as_string()); + + node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName"); + ASSERT_EQ(utf8.substr(37), node.node().text().as_string()); +#endif + + { + DicomMap m; + m.FromDicomWeb(visitor.GetResult()); + ASSERT_EQ(2u, m.GetSize()); + + std::string s; + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false)); + ASSERT_EQ("ISO 2022 IR 87", s); + + ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); + std::vector v; + Toolbox::TokenizeString(v, s, '='); + ASSERT_EQ(3u, v.size()); + ASSERT_EQ("Yamada^Tarou", v[0]); + ASSERT_EQ(utf8, s); + } +} + + + +TEST(Toolbox, EncodingsChinese3) +{ + // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.3.html + + static const uint8_t chinese[] = { + 0x57, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, + 0x6e, 0x67, 0x3d, 0xcd, 0xf5, 0x5e, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x00 + }; + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030"); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString + (DCM_PatientName, reinterpret_cast(chinese), OFBool(true)).good()); + + bool hasCodeExtensions; + Encoding encoding = dicom.DetectEncoding(hasCodeExtensions); + ASSERT_EQ(Encoding_Chinese, encoding); + ASSERT_FALSE(hasCodeExtensions); + + std::string value; + ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME)); + + std::vector tokens; + Orthanc::Toolbox::TokenizeString(tokens, value, '='); + ASSERT_EQ(3u, tokens.size()); + ASSERT_EQ("Wang^XiaoDong", tokens[0]); + ASSERT_TRUE(tokens[2].empty()); + + std::vector middle; + Orthanc::Toolbox::TokenizeString(middle, tokens[1], '^'); + ASSERT_EQ(2u, middle.size()); + ASSERT_EQ(3u, middle[0].size()); + ASSERT_EQ(6u, middle[1].size()); + + // CDF5 in GB18030 + ASSERT_EQ(static_cast(0xe7), middle[0][0]); + ASSERT_EQ(static_cast(0x8e), middle[0][1]); + ASSERT_EQ(static_cast(0x8b), middle[0][2]); + + // D0A1 in GB18030 + ASSERT_EQ(static_cast(0xe5), middle[1][0]); + ASSERT_EQ(static_cast(0xb0), middle[1][1]); + ASSERT_EQ(static_cast(0x8f), middle[1][2]); + + // B6AB in GB18030 + ASSERT_EQ(static_cast(0xe4), middle[1][3]); + ASSERT_EQ(static_cast(0xb8), middle[1][4]); + ASSERT_EQ(static_cast(0x9c), middle[1][5]); +} + + +TEST(Toolbox, EncodingsChinese4) +{ + // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.4.html + + static const uint8_t chinese[] = { + 0x54, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6c, 0x69, 0x6e, + 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0xd6, 0xd0, 0xce, + 0xc4, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, + 0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, + 0x65, 0x73, 0xd6, 0xd0, 0xce, 0xc4, 0x2c, 0x20, 0x74, 0x6f, 0x6f, 0x2e, 0x0d, + 0x0a, 0x54, 0x68, 0x65, 0x20, 0x74, 0x68, 0x69, 0x72, 0x64, 0x20, 0x6c, 0x69, + 0x6e, 0x65, 0x2e, 0x0d, 0x0a, 0x00 + }; + + static const uint8_t patternRaw[] = { + 0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87 + }; + + const std::string pattern(reinterpret_cast(patternRaw), sizeof(patternRaw)); + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030"); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString + (DCM_PatientComments, reinterpret_cast(chinese), OFBool(true)).good()); + + bool hasCodeExtensions; + Encoding encoding = dicom.DetectEncoding(hasCodeExtensions); + ASSERT_EQ(Encoding_Chinese, encoding); + ASSERT_FALSE(hasCodeExtensions); + + std::string value; + ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_COMMENTS)); + + std::vector lines; + Orthanc::Toolbox::TokenizeString(lines, value, '\n'); + ASSERT_EQ(4u, lines.size()); + ASSERT_TRUE(boost::starts_with(lines[0], "The first line includes")); + ASSERT_TRUE(boost::ends_with(lines[0], ".\r")); + ASSERT_TRUE(lines[0].find(pattern) != std::string::npos); + ASSERT_TRUE(boost::starts_with(lines[1], "The second line includes")); + ASSERT_TRUE(boost::ends_with(lines[1], ", too.\r")); + ASSERT_TRUE(lines[1].find(pattern) != std::string::npos); + ASSERT_EQ("The third line.\r", lines[2]); + ASSERT_FALSE(lines[1].find(pattern) == std::string::npos); + ASSERT_TRUE(lines[3].empty()); +} + + +TEST(Toolbox, EncodingsSimplifiedChinese2) +{ + // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html + + static const uint8_t chinese[] = { + 0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, + 0x6e, 0x67, 0x3d, 0x1b, 0x24, 0x29, 0x41, 0xd5, 0xc5, 0x5e, 0x1b, 0x24, + 0x29, 0x41, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x20, 0x00 + }; + + // echo -n "Zhang^XiaoDong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"' + static const uint8_t utf8[] = { + 0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, 0x6e, 0x67, + 0x3d, 0xe5, 0xbc, 0xa0, 0x5e, 0xe5, 0xb0, 0x8f, 0xe4, 0xb8, 0x9c, 0x3d + }; + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58"); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString + (DCM_PatientName, reinterpret_cast(chinese), OFBool(true)).good()); + + bool hasCodeExtensions; + Encoding encoding = dicom.DetectEncoding(hasCodeExtensions); + ASSERT_EQ(Encoding_SimplifiedChinese, encoding); + ASSERT_TRUE(hasCodeExtensions); + + std::string value; + ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(value, std::string(reinterpret_cast(utf8), sizeof(utf8))); +} + + +TEST(Toolbox, EncodingsSimplifiedChinese3) +{ + // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html + + static const uint8_t chinese[] = { + 0x31, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xd2, 0xbb, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a, + 0x32, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xb6, 0xfe, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a, + 0x33, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xc8, 0xfd, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a, 0x00 + }; + + static const uint8_t line1[] = { + 0x31, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x80, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87, + 0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r' + }; + + static const uint8_t line2[] = { + 0x32, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xba, 0x8c, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87, + 0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r' + }; + + static const uint8_t line3[] = { + 0x33, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x89, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87, + 0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r' + }; + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58"); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString + (DCM_PatientName, reinterpret_cast(chinese), OFBool(true)).good()); + + bool hasCodeExtensions; + Encoding encoding = dicom.DetectEncoding(hasCodeExtensions); + ASSERT_EQ(Encoding_SimplifiedChinese, encoding); + ASSERT_TRUE(hasCodeExtensions); + + std::string value; + ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME)); + + std::vector lines; + Toolbox::TokenizeString(lines, value, '\n'); + ASSERT_EQ(4u, lines.size()); + ASSERT_EQ(std::string(reinterpret_cast(line1), sizeof(line1)), lines[0]); + ASSERT_EQ(std::string(reinterpret_cast(line2), sizeof(line2)), lines[1]); + ASSERT_EQ(std::string(reinterpret_cast(line3), sizeof(line3)), lines[2]); + ASSERT_TRUE(lines[3].empty()); +} + + +static void SetTagKey(ParsedDicomFile& dicom, + const DicomTag& tag, + const DicomTag& value) +{ + // This function emulates a call to function + // "dicom.GetDcmtkObject().getDataset()->putAndInsertTagKey(tag, + // value)" that was not available in DCMTK 3.6.0 + + std::unique_ptr element(new DcmAttributeTag(ToDcmtkBridge::Convert(tag))); + + DcmTagKey v = ToDcmtkBridge::Convert(value); + if (!element->putTagVal(v).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + dicom.GetDcmtkObject().getDataset()->insert(element.release()); +} + + +TEST(DicomWebJson, ValueRepresentation) +{ + // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html + + ParsedDicomFile dicom(false); + dicom.ReplacePlainString(DicomTag(0x0040, 0x0241), "AE"); + dicom.ReplacePlainString(DicomTag(0x0010, 0x1010), "AS"); + SetTagKey(dicom, DicomTag(0x0020, 0x9165), DicomTag(0x0010, 0x0020)); + dicom.ReplacePlainString(DicomTag(0x0008, 0x0052), "CS"); + dicom.ReplacePlainString(DicomTag(0x0008, 0x0012), "DA"); + dicom.ReplacePlainString(DicomTag(0x0010, 0x1020), "42"); // DS + dicom.ReplacePlainString(DicomTag(0x0008, 0x002a), "DT"); + dicom.ReplacePlainString(DicomTag(0x0010, 0x9431), "43"); // FL + dicom.ReplacePlainString(DicomTag(0x0008, 0x1163), "44"); // FD + dicom.ReplacePlainString(DicomTag(0x0008, 0x1160), "45"); // IS + dicom.ReplacePlainString(DicomTag(0x0008, 0x0070), "LO"); + dicom.ReplacePlainString(DicomTag(0x0010, 0x4000), "LT"); + dicom.ReplacePlainString(DicomTag(0x0028, 0x2000), "OB"); + dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "3.14159"); // OD (other double) + dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "2.71828"); // OF (other float) + dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46"); // OL (other long) + ASSERT_THROW(dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "O"), OrthancException); + dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "OWOW"); + dicom.ReplacePlainString(DicomTag(0x0010, 0x0010), "PN"); + dicom.ReplacePlainString(DicomTag(0x0008, 0x0050), "SH"); + dicom.ReplacePlainString(DicomTag(0x0018, 0x6020), "-15"); // SL + dicom.ReplacePlainString(DicomTag(0x0018, 0x9219), "-16"); // SS + dicom.ReplacePlainString(DicomTag(0x0008, 0x0081), "ST"); + dicom.ReplacePlainString(DicomTag(0x0008, 0x0013), "TM"); + dicom.ReplacePlainString(DicomTag(0x0008, 0x0119), "UC"); + dicom.ReplacePlainString(DicomTag(0x0008, 0x0016), "UI"); + dicom.ReplacePlainString(DicomTag(0x0008, 0x1161), "128"); // UL + dicom.ReplacePlainString(DicomTag(0x4342, 0x1234), "UN"); // Inexistent tag + dicom.ReplacePlainString(DicomTag(0x0008, 0x0120), "UR"); + dicom.ReplacePlainString(DicomTag(0x0008, 0x0301), "17"); // US + dicom.ReplacePlainString(DicomTag(0x0040, 0x0031), "UT"); + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + std::string s; + + // The tag (0002,0002) is "Media Storage SOP Class UID" and is + // automatically copied by DCMTK from tag (0008,0016) + ASSERT_EQ("UI", visitor.GetResult() ["00020002"]["vr"].asString()); + ASSERT_EQ("UI", visitor.GetResult() ["00020002"]["Value"][0].asString()); + ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["vr"].asString()); + ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["Value"][0].asString()); + ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["vr"].asString()); + ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["Value"][0].asString()); + ASSERT_EQ("AT", visitor.GetResult() ["00209165"]["vr"].asString()); + ASSERT_EQ("00100020", visitor.GetResult() ["00209165"]["Value"][0].asString()); + ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["vr"].asString()); + ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["Value"][0].asString()); + ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["vr"].asString()); + ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["Value"][0].asString()); + ASSERT_EQ("DS", visitor.GetResult() ["00101020"]["vr"].asString()); + ASSERT_EQ("42", visitor.GetResult() ["00101020"]["Value"][0].asString()); + ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["vr"].asString()); + ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["Value"][0].asString()); + ASSERT_EQ("FL", visitor.GetResult() ["00109431"]["vr"].asString()); + ASSERT_FLOAT_EQ(43.0f, visitor.GetResult() ["00109431"]["Value"][0].asFloat()); + ASSERT_EQ("FD", visitor.GetResult() ["00081163"]["vr"].asString()); + ASSERT_FLOAT_EQ(44.0f, visitor.GetResult() ["00081163"]["Value"][0].asFloat()); + ASSERT_EQ("IS", visitor.GetResult() ["00081160"]["vr"].asString()); + ASSERT_FLOAT_EQ(45.0f, visitor.GetResult() ["00081160"]["Value"][0].asFloat()); + ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["vr"].asString()); + ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["Value"][0].asString()); + ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["vr"].asString()); + ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["Value"][0].asString()); + + ASSERT_EQ("OB", visitor.GetResult() ["00282000"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["00282000"]["InlineBinary"].asString()); + ASSERT_EQ("OB", s); + +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_EQ("OD", visitor.GetResult() ["7FE00009"]["vr"].asString()); + ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast(visitor.GetResult() ["7FE00009"]["Value"][0].asString())); +#else + ASSERT_EQ("UN", visitor.GetResult() ["7FE00009"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString()); + ASSERT_EQ(8u, s.size()); // Because of padding + ASSERT_EQ(0, s[7]); + ASSERT_EQ("3.14159", s.substr(0, 7)); +#endif + + ASSERT_EQ("OF", visitor.GetResult() ["00640009"]["vr"].asString()); + ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast(visitor.GetResult() ["00640009"]["Value"][0].asString())); + +#if DCMTK_VERSION_NUMBER < 361 + ASSERT_EQ("UN", visitor.GetResult() ["00660040"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["00660040"]["InlineBinary"].asString()); + ASSERT_EQ("46", s); +#elif DCMTK_VERSION_NUMBER == 361 + ASSERT_EQ("UL", visitor.GetResult() ["00660040"]["vr"].asString()); + ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt()); +#else + ASSERT_EQ("OL", visitor.GetResult() ["00660040"]["vr"].asString()); + ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt()); +#endif + + ASSERT_EQ("OW", visitor.GetResult() ["00281201"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["00281201"]["InlineBinary"].asString()); + ASSERT_EQ("OWOW", s); + + ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["vr"].asString()); + ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["Value"][0]["Alphabetic"].asString()); + + ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["vr"].asString()); + ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["Value"][0].asString()); + + ASSERT_EQ("SL", visitor.GetResult() ["00186020"]["vr"].asString()); + ASSERT_EQ(-15, visitor.GetResult() ["00186020"]["Value"][0].asInt()); + + ASSERT_EQ("SS", visitor.GetResult() ["00189219"]["vr"].asString()); + ASSERT_EQ(-16, visitor.GetResult() ["00189219"]["Value"][0].asInt()); + + ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["vr"].asString()); + ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["Value"][0].asString()); + + ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["vr"].asString()); + ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["Value"][0].asString()); + +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["vr"].asString()); + ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["Value"][0].asString()); +#else + ASSERT_EQ("UN", visitor.GetResult() ["00080119"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["00080119"]["InlineBinary"].asString()); + ASSERT_EQ("UC", s); +#endif + + ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["vr"].asString()); + ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["Value"][0].asString()); + + ASSERT_EQ("UL", visitor.GetResult() ["00081161"]["vr"].asString()); + ASSERT_EQ(128u, visitor.GetResult() ["00081161"]["Value"][0].asUInt()); + + ASSERT_EQ("UN", visitor.GetResult() ["43421234"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["43421234"]["InlineBinary"].asString()); + ASSERT_EQ("UN", s); + +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["vr"].asString()); + ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["Value"][0].asString()); +#else + ASSERT_EQ("UN", visitor.GetResult() ["00080120"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["00080120"]["InlineBinary"].asString()); + ASSERT_EQ("UR", s); +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_EQ("US", visitor.GetResult() ["00080301"]["vr"].asString()); + ASSERT_EQ(17u, visitor.GetResult() ["00080301"]["Value"][0].asUInt()); +#else + ASSERT_EQ("UN", visitor.GetResult() ["00080301"]["vr"].asString()); + Toolbox::DecodeBase64(s, visitor.GetResult() ["00080301"]["InlineBinary"].asString()); + ASSERT_EQ("17", s); +#endif + + ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["vr"].asString()); + ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["Value"][0].asString()); + + std::string xml; + visitor.FormatXml(xml); + + { + DicomMap m; + m.FromDicomWeb(visitor.GetResult()); + ASSERT_EQ(31u, m.GetSize()); + + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0002, 0x0002), false)); ASSERT_EQ("UI", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0241), false)); ASSERT_EQ("AE", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x1010), false)); ASSERT_EQ("AS", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0020, 0x9165), false)); ASSERT_EQ("00100020", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0052), false)); ASSERT_EQ("CS", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0012), false)); ASSERT_EQ("DA", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x1020), false)); ASSERT_EQ("42", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x002a), false)); ASSERT_EQ("DT", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x9431), false)); ASSERT_EQ("43", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1163), false)); ASSERT_EQ("44", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1160), false)); ASSERT_EQ("45", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0070), false)); ASSERT_EQ("LO", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x4000), false)); ASSERT_EQ("LT", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x2000), true)); ASSERT_EQ("OB", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7fe0, 0x0009), true)); + +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast(s)); +#else + ASSERT_EQ(8u, s.size()); // Because of padding + ASSERT_EQ(0, s[7]); + ASSERT_EQ("3.14159", s.substr(0, 7)); +#endif + + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0064, 0x0009), true)); + ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast(s)); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x1201), true)); ASSERT_EQ("OWOW", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x0010), false)); ASSERT_EQ("PN", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0050), false)); ASSERT_EQ("SH", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x6020), false)); ASSERT_EQ("-15", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x9219), false)); ASSERT_EQ("-16", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0081), false)); ASSERT_EQ("ST", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0013), false)); ASSERT_EQ("TM", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0016), false)); ASSERT_EQ("UI", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1161), false)); ASSERT_EQ("128", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x4342, 0x1234), true)); ASSERT_EQ("UN", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0031), false)); ASSERT_EQ("UT", s); + +#if DCMTK_VERSION_NUMBER >= 361 + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), false)); ASSERT_EQ("46", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), false)); ASSERT_EQ("UC", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), false)); ASSERT_EQ("UR", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), false)); ASSERT_EQ("17", s); +#else + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), true)); ASSERT_EQ("46", s); // OL + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), true)); ASSERT_EQ("UC", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), true)); ASSERT_EQ("UR", s); + ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), true)); ASSERT_EQ("17", s); // US (but tag unknown to DCMTK 3.6.0) +#endif + } +} + + +TEST(DicomWebJson, Sequence) +{ + ParsedDicomFile dicom(false); + + { + std::unique_ptr sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence)); + + for (unsigned int i = 0; i < 3; i++) + { + std::unique_ptr item(new DcmItem); + std::string s = "item" + boost::lexical_cast(i); + item->putAndInsertString(DCM_ReferencedSOPInstanceUID, s.c_str(), OFFalse); + ASSERT_TRUE(sequence->insert(item.release(), false, false).good()); + } + + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->insert(sequence.release(), false, false).good()); + } + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + ASSERT_EQ("SQ", visitor.GetResult() ["00081115"]["vr"].asString()); + ASSERT_EQ(3u, visitor.GetResult() ["00081115"]["Value"].size()); + + std::set items; + + for (Json::Value::ArrayIndex i = 0; i < 3; i++) + { + ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i].size()); + ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"].size()); + ASSERT_EQ("UI", visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["vr"].asString()); + items.insert(visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"][0].asString()); + } + + ASSERT_EQ(3u, items.size()); + ASSERT_TRUE(items.find("item0") != items.end()); + ASSERT_TRUE(items.find("item1") != items.end()); + ASSERT_TRUE(items.find("item2") != items.end()); + + std::string xml; + visitor.FormatXml(xml); + + { + DicomMap m; + m.FromDicomWeb(visitor.GetResult()); + ASSERT_EQ(0u, m.GetSize()); // Sequences are not handled by DicomMap + } +} + + +TEST(ParsedDicomCache, Basic) +{ + ParsedDicomCache cache(10); + ASSERT_EQ(0u, cache.GetCurrentSize()); + ASSERT_EQ(0u, cache.GetNumberOfItems()); + + DicomMap tags; + tags.SetValue(DICOM_TAG_PATIENT_ID, "patient1", false); + cache.Acquire("a", new ParsedDicomFile(tags, Encoding_Latin1, true), 20); + ASSERT_EQ(20u, cache.GetCurrentSize()); + ASSERT_EQ(1u, cache.GetNumberOfItems()); + + { + ParsedDicomCache::Accessor accessor(cache, "b"); + ASSERT_FALSE(accessor.IsValid()); + ASSERT_THROW(accessor.GetDicom(), OrthancException); + ASSERT_THROW(accessor.GetFileSize(), OrthancException); + } + + { + ParsedDicomCache::Accessor accessor(cache, "a"); + ASSERT_TRUE(accessor.IsValid()); + std::string s; + ASSERT_TRUE(accessor.GetDicom().GetTagValue(s, DICOM_TAG_PATIENT_ID)); + ASSERT_EQ("patient1", s); + ASSERT_EQ(20u, accessor.GetFileSize()); + } + + tags.SetValue(DICOM_TAG_PATIENT_ID, "patient2", false); + cache.Acquire("b", new ParsedDicomFile(tags, Encoding_Latin1, true), 5); + ASSERT_EQ(5u, cache.GetCurrentSize()); + ASSERT_EQ(1u, cache.GetNumberOfItems()); + + cache.Acquire("c", new ParsedDicomFile(true), 5); + ASSERT_EQ(10u, cache.GetCurrentSize()); + ASSERT_EQ(2u, cache.GetNumberOfItems()); + + { + ParsedDicomCache::Accessor accessor(cache, "b"); + ASSERT_TRUE(accessor.IsValid()); + std::string s; + ASSERT_TRUE(accessor.GetDicom().GetTagValue(s, DICOM_TAG_PATIENT_ID)); + ASSERT_EQ("patient2", s); + ASSERT_EQ(5u, accessor.GetFileSize()); + } + + cache.Acquire("d", new ParsedDicomFile(true), 5); + ASSERT_EQ(10u, cache.GetCurrentSize()); + ASSERT_EQ(2u, cache.GetNumberOfItems()); + + ASSERT_TRUE(ParsedDicomCache::Accessor(cache, "b").IsValid()); + ASSERT_FALSE(ParsedDicomCache::Accessor(cache, "c").IsValid()); // recycled by LRU + ASSERT_TRUE(ParsedDicomCache::Accessor(cache, "d").IsValid()); + + cache.Invalidate("d"); + ASSERT_EQ(5u, cache.GetCurrentSize()); + ASSERT_EQ(1u, cache.GetNumberOfItems()); + ASSERT_TRUE(ParsedDicomCache::Accessor(cache, "b").IsValid()); + ASSERT_FALSE(ParsedDicomCache::Accessor(cache, "d").IsValid()); + + cache.Acquire("e", new ParsedDicomFile(true), 15); + ASSERT_EQ(15u, cache.GetCurrentSize()); + ASSERT_EQ(1u, cache.GetNumberOfItems()); + + ASSERT_FALSE(ParsedDicomCache::Accessor(cache, "c").IsValid()); + ASSERT_FALSE(ParsedDicomCache::Accessor(cache, "d").IsValid()); + ASSERT_TRUE(ParsedDicomCache::Accessor(cache, "e").IsValid()); + + cache.Invalidate("e"); + ASSERT_EQ(0u, cache.GetCurrentSize()); + ASSERT_EQ(0u, cache.GetNumberOfItems()); + ASSERT_FALSE(ParsedDicomCache::Accessor(cache, "e").IsValid()); +} + + +static bool MyIsMatch(const DicomPath& a, + const DicomPath& b) +{ + bool expected = DicomPath::IsMatch(a, b); + + std::vector prefixTags; + std::vector prefixIndexes; + + for (size_t i = 0; i < b.GetPrefixLength(); i++) + { + prefixTags.push_back(b.GetPrefixTag(i)); + prefixIndexes.push_back(b.GetPrefixIndex(i)); + } + + if (expected == DicomPath::IsMatch(a, prefixTags, prefixIndexes, b.GetFinalTag())) + { + return expected; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } +} + + +TEST(DicomModification, DicomPath) +{ + // Those are samples inspired by those from "man dcmodify" + + static const DicomTag DICOM_TAG_ACQUISITION_MATRIX(0x0018, 0x1310); + static const DicomTag DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE(0x0008, 0x1111); + + DicomPath path = DicomPath::Parse("(0010,0010)"); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + ASSERT_THROW(path.GetPrefixTag(0), OrthancException); + + path = DicomPath::Parse("0018,1310"); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_ACQUISITION_MATRIX, path.GetFinalTag()); + ASSERT_EQ("(0018,1310)", path.Format()); + + // The following sample won't work without DCMTK + path = DicomPath::Parse("PatientID"); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_ID, path.GetFinalTag()); + ASSERT_EQ("(0010,0020)", path.Format()); + + path = DicomPath::Parse("(0018,1310)"); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(0u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_ACQUISITION_MATRIX, path.GetFinalTag()); + ASSERT_EQ("(0018,1310)", path.Format()); + + path = DicomPath::Parse("(0008,1111)[0].PatientName"); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(0u, path.GetPrefixIndex(0)); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse("(0008,1111)[1].(0008,1111)[2].(0010,0010)"); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(2u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(1u, path.GetPrefixIndex(0)); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(1)); + ASSERT_FALSE(path.IsPrefixUniversal(1)); + ASSERT_EQ(2u, path.GetPrefixIndex(1)); + ASSERT_THROW(path.GetPrefixTag(2), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse("(0008,1111)[*].PatientName"); + ASSERT_TRUE(path.HasUniversal()); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_TRUE(path.IsPrefixUniversal(0)); + ASSERT_THROW(path.GetPrefixIndex(0), OrthancException); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + ASSERT_EQ("(0008,1111)[*].(0010,0010)", path.Format()); + + path = DicomPath::Parse("(0008,1111)[1].(0008,1111)[*].(0010,0010)"); + ASSERT_TRUE(path.HasUniversal()); + ASSERT_EQ(2u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(1u, path.GetPrefixIndex(0)); + ASSERT_EQ(DICOM_TAG_REFERENCED_PERFORMED_PROCEDURE_STEP_SEQUENCE, path.GetPrefixTag(0)); + ASSERT_TRUE(path.IsPrefixUniversal(1)); + ASSERT_THROW(path.GetPrefixIndex(1), OrthancException); + ASSERT_THROW(path.GetPrefixTag(2), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse("PatientID[1].PatientName"); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_ID, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(1u, path.GetPrefixIndex(0)); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + + path = DicomPath::Parse(" PatientID [ 42 ] . PatientName "); + ASSERT_FALSE(path.HasUniversal()); + ASSERT_EQ(1u, path.GetPrefixLength()); + ASSERT_EQ(DICOM_TAG_PATIENT_ID, path.GetPrefixTag(0)); + ASSERT_FALSE(path.IsPrefixUniversal(0)); + ASSERT_EQ(42u, path.GetPrefixIndex(0)); + ASSERT_THROW(path.GetPrefixTag(1), OrthancException); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, path.GetFinalTag()); + ASSERT_EQ("(0010,0020)[42].(0010,0010)", path.Format()); + + ASSERT_THROW(path.SetPrefixIndex(1, 44), OrthancException); + path.SetPrefixIndex(0, 44); + ASSERT_EQ("(0010,0020)[44].(0010,0010)", path.Format()); + + ASSERT_THROW(DicomPath::Parse("nope"), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)[.PatientID"), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)[].PatientID"), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010[].PatientID"), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)0].PatientID"), OrthancException); + ASSERT_THROW(DicomPath::Parse("(0010,0010)[-1].PatientID"), OrthancException); + + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)"), + DicomPath::Parse("(0010,0010)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)"), + DicomPath::Parse("(0010,0020)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), + DicomPath::Parse("(0010,0010)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)"))); + ASSERT_THROW(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)"), + DicomPath::Parse("(0010,0010)[*].(0010,0020)")), OrthancException); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[*].(0010,0020)[*].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); + ASSERT_TRUE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[1].(0010,0020)[3].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); + ASSERT_FALSE(MyIsMatch(DicomPath::Parse("(0010,0010)[2].(0010,0020)[2].(0010,0030)"), + DicomPath::Parse("(0010,0010)[1].(0010,0020)[2].(0010,0030)[3].(0010,0040)"))); +} + + + +TEST(ParsedDicomFile, DicomPath) +{ + Json::Value v = Json::objectValue; + v["PatientName"] = "Hello"; + v["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + + { + Json::Value a = Json::arrayValue; + + { + Json::Value item = Json::objectValue; + item["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + item["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.719"; + a.append(item); + } + + { + Json::Value item = Json::objectValue; + item["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; // ReferencedSOPClassUID + item["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.726"; + a.append(item); + } + + v["ReferencedImageSequence"] = a; + } + + { + Json::Value a = Json::arrayValue; + + { + Json::Value item = Json::objectValue; + item["StudyInstanceUID"] = "1.2.840.113704.1.111.7016.1342451220.40"; + + { + Json::Value b = Json::arrayValue; + + { + Json::Value c = Json::objectValue; + c["CodeValue"] = "122403"; + c["0008,103e"] = "WORLD"; // Series description + b.append(c); + } + + item["PurposeOfReferenceCodeSequence"] = b; + } + + a.append(item); + } + + v["RelatedSeriesSequence"] = a; + } + + static const char* CODE_VALUE = "0008,0100"; + static const char* PATIENT_ID = "0010,0020"; + static const char* PATIENT_NAME = "0010,0010"; + static const char* PURPOSE_CODE_SEQ = "0040,a170"; + static const char* REF_IM_SEQ = "0008,1140"; + static const char* REF_SOP_CLASS = "0008,1150"; + static const char* REF_SOP_INSTANCE = "0008,1155"; + static const char* REL_SERIES_SEQ = "0008,1250"; + static const char* STUDY_INSTANCE_UID = "0020,000d"; + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ(5u, vv.size()); + ASSERT_TRUE(vv.isMember(PATIENT_NAME)); + ASSERT_EQ(2u, vv[REF_IM_SEQ].size()); + ASSERT_EQ(1u, vv[REL_SERIES_SEQ].size()); + ASSERT_EQ(2u, vv[REF_IM_SEQ][0].size()); + ASSERT_EQ(2u, vv[REL_SERIES_SEQ][0].size()); + ASSERT_EQ(1u, vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ].size()); + + ASSERT_TRUE(vv[REF_IM_SEQ][0].isMember(REF_SOP_CLASS)); + ASSERT_TRUE(vv[REF_IM_SEQ][1].isMember(REF_SOP_CLASS)); + ASSERT_TRUE(vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0].isMember(CODE_VALUE)); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->RemovePath(DicomPath::Parse("ReferencedImageSequence[*].ReferencedSOPClassUID")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ(2u, vv[REF_IM_SEQ].size()); + ASSERT_EQ(1u, vv[REF_IM_SEQ][0].size()); + ASSERT_EQ(1u, vv[REF_IM_SEQ][1].size()); + ASSERT_FALSE(vv[REF_IM_SEQ][0].isMember(REF_SOP_CLASS)); + ASSERT_FALSE(vv[REF_IM_SEQ][1].isMember(REF_SOP_CLASS)); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->RemovePath(DicomPath::Parse("ReferencedImageSequence[0].ReferencedSOPClassUID")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ(2u, vv[REF_IM_SEQ].size()); + ASSERT_EQ(1u, vv[REF_IM_SEQ][0].size()); + ASSERT_EQ(2u, vv[REF_IM_SEQ][1].size()); + ASSERT_FALSE(vv[REF_IM_SEQ][0].isMember(REF_SOP_CLASS)); + ASSERT_TRUE(vv[REF_IM_SEQ][1].isMember(REF_SOP_CLASS)); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->RemovePath(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ(2u, vv[REF_IM_SEQ].size()); + ASSERT_EQ(2u, vv[REF_IM_SEQ][0].size()); + ASSERT_EQ(1u, vv[REF_IM_SEQ][1].size()); + ASSERT_TRUE(vv[REF_IM_SEQ][0].isMember(REF_SOP_CLASS)); + ASSERT_FALSE(vv[REF_IM_SEQ][1].isMember(REF_SOP_CLASS)); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->RemovePath(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence[0].CodeValue")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + static const char* SERIES_DESCRIPTION = "0008,103e"; + + ASSERT_EQ("WORLD", vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][SERIES_DESCRIPTION].asString()); + ASSERT_FALSE(vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0].isMember(CODE_VALUE)); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->RemovePath(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ(1u, vv[REL_SERIES_SEQ][0].size()); + ASSERT_FALSE(vv[REL_SERIES_SEQ][0].isMember(PURPOSE_CODE_SEQ)); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->RemovePath(DicomPath::Parse("RelatedSeriesSequence")); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_FALSE(vv.isMember(REL_SERIES_SEQ)); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->RemovePath(DicomPath(DICOM_TAG_PATIENT_NAME)); + dicom->ReplacePath(DicomPath::Parse("ReferencedImageSequence[*].ReferencedSOPClassUID"), + "Hello1", false, DicomReplaceMode_ThrowIfAbsent, ""); + ASSERT_THROW(dicom->ReplacePath(DicomPath::Parse("ReferencedImageSequence[*].PatientID"), + "Hello2", false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException); + dicom->ReplacePath(DicomPath::Parse("ReferencedImageSequence[*].PatientID"), + "Hello3", false, DicomReplaceMode_InsertIfAbsent, ""); + dicom->ReplacePath(DicomPath::Parse("ReferencedImageSequence[*].PatientName"), + "Hello4", false, DicomReplaceMode_IgnoreIfAbsent, ""); + dicom->ReplacePath(DicomPath::Parse("RelatedSeriesSequence[*].PurposeOfReferenceCodeSequence[*].CodeValue"), + "Hello5", false, DicomReplaceMode_ThrowIfAbsent, ""); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ(4u, vv.size()); + ASSERT_FALSE(vv.isMember(PATIENT_NAME)); + ASSERT_EQ("Hello1", vv[REF_IM_SEQ][0][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello3", vv[REF_IM_SEQ][0][PATIENT_ID].asString()); + ASSERT_EQ("Hello1", vv[REF_IM_SEQ][1][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello3", vv[REF_IM_SEQ][1][PATIENT_ID].asString()); + ASSERT_EQ("Hello5", vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][CODE_VALUE].asString()); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->ReplacePath(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID"), + "Hello1", false, DicomReplaceMode_ThrowIfAbsent, ""); + dicom->ReplacePath(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence[0].CodeValue"), + "Hello2", false, DicomReplaceMode_ThrowIfAbsent, ""); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", vv[REF_IM_SEQ][0][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello1", vv[REF_IM_SEQ][1][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello2", vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][CODE_VALUE].asString()); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->ClearPath(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID"), true); + dicom->ClearPath(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence[0].CodeValue"), true); + dicom->ClearPath(DicomPath::Parse("ReferencedImageSequence[0].PatientID"), false); + dicom->ClearPath(DicomPath::Parse("ReferencedImageSequence[0].PatientName"), true); + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_EQ(3u, vv[REF_IM_SEQ][0].size()); + ASSERT_EQ(2u, vv[REF_IM_SEQ][1].size()); + + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", vv[REF_IM_SEQ][0][REF_SOP_CLASS].asString()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", vv[REF_IM_SEQ][0][REF_SOP_INSTANCE].asString()); + ASSERT_EQ("", vv[REF_IM_SEQ][0][PATIENT_ID].asString()); + + ASSERT_EQ("", vv[REF_IM_SEQ][1][REF_SOP_CLASS].asString()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); + + ASSERT_EQ("", vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][CODE_VALUE].asString()); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + { + DicomModification modif; + modif.Replace(DicomPath(DICOM_TAG_PATIENT_NAME), "Hello1", false); + modif.Replace(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID"), "Hello2", false); + modif.Replace(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence[0].CodeValue"), "Hello3", false); + modif.Apply(*dicom); + } + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_TRUE(vv.isMember(PATIENT_NAME)); + ASSERT_EQ("Hello1", vv[PATIENT_NAME].asString()); + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", vv[REF_IM_SEQ][0][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello2", vv[REF_IM_SEQ][1][REF_SOP_CLASS].asString()); + ASSERT_EQ("Hello3", vv[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0][CODE_VALUE].asString()); + ASSERT_EQ(2u, vv[REL_SERIES_SEQ][0].size()); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + { + DicomModification modif; + modif.Remove(DicomPath(DICOM_TAG_PATIENT_NAME)); + modif.Remove(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID")); + modif.Remove(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence")); + modif.Apply(*dicom); + } + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_FALSE(vv.isMember(PATIENT_NAME)); + ASSERT_EQ(2u, vv[REF_IM_SEQ][0].size()); + ASSERT_TRUE(vv[REF_IM_SEQ][0].isMember(REF_SOP_CLASS)); + ASSERT_EQ(1u, vv[REF_IM_SEQ][1].size()); + ASSERT_FALSE(vv[REF_IM_SEQ][1].isMember(REF_SOP_CLASS)); + ASSERT_EQ(1u, vv[REL_SERIES_SEQ][0].size()); + } + + { + std::unique_ptr dicom1(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + std::unique_ptr dicom2(dicom1->Clone(true)); + + { + DicomModification modif; + modif.SetupAnonymization(DicomVersion_2023b); + modif.Apply(*dicom1); + modif.Apply(*dicom2); + } + + // Same anonymization context and same input DICOM => hence, same output DICOM + Json::Value vv1, vv2; + dicom1->DatasetToJson(vv1, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + dicom2->DatasetToJson(vv2, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + ASSERT_EQ(vv1.toStyledString(), vv2.toStyledString()); + + ASSERT_TRUE(Toolbox::IsUuid(vv1[PATIENT_NAME].asString())); + ASSERT_EQ("1.2.840.10008.5.1.4.1.1.4", vv1[REF_IM_SEQ][0][REF_SOP_CLASS].asString()); + ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", vv1[REF_IM_SEQ][0][REF_SOP_INSTANCE].asString()); + ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv1[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); + ASSERT_NE("1.2.840.113704.1.111.7016.1342451220.40", vv1[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString()); + + // Contrarily to Orthanc 1.9.4, the "SERIES_DESCRIPTION" is also removed from nested sequences + ASSERT_EQ(1u, vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0].size()); + ASSERT_EQ("122403", vv1[REL_SERIES_SEQ][0][PURPOSE_CODE_SEQ][0]["0008,0100"].asString()); + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + { + DicomModification modif; + modif.SetupAnonymization(DicomVersion_2023b); + modif.Keep(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPInstanceUID")); + modif.Keep(DicomPath::Parse("RelatedSeriesSequence")); + modif.Apply(*dicom); + } + + Json::Value vv; + dicom->DatasetToJson(vv, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); + + ASSERT_NE("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", vv[REF_IM_SEQ][0][REF_SOP_INSTANCE].asString()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", vv[REF_IM_SEQ][1][REF_SOP_INSTANCE].asString()); // kept + ASSERT_EQ("1.2.840.113704.1.111.7016.1342451220.40", vv[REL_SERIES_SEQ][0][STUDY_INSTANCE_UID].asString()); // kept + } + + { + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + DicomMap m; + ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 0)); + ASSERT_EQ(2u, m.GetSize()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.719", + m.GetStringValue(DICOM_TAG_REFERENCED_SOP_INSTANCE_UID, "", false)); + + ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 1)); + ASSERT_EQ(2u, m.GetSize()); + ASSERT_EQ("1.2.840.113619.2.176.2025.1499492.7040.1171286241.726", + m.GetStringValue(DICOM_TAG_REFERENCED_SOP_INSTANCE_UID, "", false)); + + ASSERT_FALSE(dicom->LookupSequenceItem(m, DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE), 2)); + + ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x0008, 0x1250), 0, DicomTag(0x0040, 0xa170)), 0)); + ASSERT_EQ(2u, m.GetSize()); + ASSERT_EQ("122403", m.GetStringValue(DicomTag(0x0008, 0x0100), "", false)); + ASSERT_EQ("WORLD", m.GetStringValue(DICOM_TAG_SERIES_DESCRIPTION, "", false)); + + ASSERT_FALSE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x0008, 0x1250), 0, DicomTag(0x0040, 0xa170)), 1)); + } +} + + +TEST(FromDcmtkBridge, VisitorRemoveTag) +{ + class V : public ITagVisitor + { + private: + uint32_t seen_; + + public: + V() : seen_(0) + { + } + + unsigned int GetSeen() const + { + return seen_; + } + + virtual Action VisitNotSupported(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr) ORTHANC_OVERRIDE + { + seen_ |= (1 << 0); + + if (parentTags.size() == 0u && + parentIndexes.size() == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_PixelData) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitSequence(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + size_t countItems) ORTHANC_OVERRIDE + { + seen_ |= (1 << 1); + + if (parentTags.size() == 0u && + parentIndexes.size() == 0u && + tag == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + countItems == 1) + { + return Action_None; + } + else if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + countItems == 0 && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ReferencedPatientSequence) + { + return Action_Remove; + } + else if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + countItems == 1 && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ReferencedStudySequence) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitIntegers(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) ORTHANC_OVERRIDE + { + seen_ |= (1 << 2); + + if (parentTags.size() == 0u && + parentIndexes.size() == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_TagAngleSecondAxis && + values.size() == 2 && + values[0] == 12 && + values[1] == 13) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitDoubles(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::vector& values) ORTHANC_OVERRIDE + { + seen_ |= (1 << 3); + + if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_ExaminedBodyThickness && + values.size() == 3 && + std::abs(values[0] - 42.0f) <= 0.001f && + std::abs(values[1] - 43.0f) <= 0.001f && + std::abs(values[2] - 47.0f) <= 0.001f) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitAttributes(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + const std::vector& values) ORTHANC_OVERRIDE + { + seen_ |= (1 << 4); + + if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + DcmTagKey(tag.GetGroup(), tag.GetElement()) == DCM_DimensionIndexPointer && + values.size() == 2 && + values[0] == DICOM_TAG_STUDY_DATE && + values[1] == DICOM_TAG_STUDY_TIME) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitBinary(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const void* data, + size_t size) ORTHANC_OVERRIDE + { + seen_ |= (1 << 5); + + if (parentTags.size() == 1u && + parentIndexes.size() == 1u && + parentTags[0] == DICOM_TAG_REFERENCED_IMAGE_SEQUENCE && + parentIndexes[0] == 0u && + tag.GetGroup() == 0x0011 && + tag.GetElement() == 0x1311 && + size == 4u && + memcmp(data, "abcd", 4) == 0) + { + return Action_Remove; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual Action VisitString(std::string& newValue, + const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::string& value) ORTHANC_OVERRIDE + { + seen_ |= (1 << 6); + return Action_Remove; + } + }; + + + std::unique_ptr dicom; + + { + Json::Value v = Json::objectValue; + v["PatientName"] = "Hello"; + v["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + v["ReferencedImageSequence"][0]["ReferencedSOPClassUID"] = "1.2.840.10008.5.1.4.1.1.4"; + v["ReferencedImageSequence"][0]["ReferencedSOPInstanceUID"] = "1.2.840.113619.2.176.2025.1499492.7040.1171286241.719"; + v["ReferencedImageSequence"][0]["ReferencedPatientSequence"] = Json::arrayValue; // Empty nested sequence + v["ReferencedImageSequence"][0]["ReferencedStudySequence"][0]["PatientID"] = "Hello"; // Non-empty nested sequence + v["ReferencedImageSequence"][0]["0011,1311"] = "abcd"; // Binary + + dicom.reset(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "PrivateCreator")); + + { + // Test value multiplicity (cannot be done using "ParsedDicomFile::CreateFromJson()") + const int16_t a[] = { 12, 13 }; + std::unique_ptr s(new DcmSignedShort(DCM_TagAngleSecondAxis)); // VisitIntegers() + ASSERT_TRUE(s->putSint16Array(a, 2).good()); + dicom->GetDcmtkObject().getDataset()->insert(s.release()); + } + + DcmItem *parent = NULL; + ASSERT_TRUE(dicom->GetDcmtkObject().getDataset()->findAndGetSequenceItem(DCM_ReferencedImageSequence, parent, 0).good()); + + { + const float a[] = { 42, 43, 47 }; + std::unique_ptr s(new DcmFloatingPointSingle(DCM_ExaminedBodyThickness)); // VisitDoubles() + ASSERT_TRUE(s->putFloat32Array(a, 3).good()); + parent->insert(s.release()); + } + + { + const uint16_t a[] = { 0x0008, 0x0020, 0x0008, 0x0030 }; + std::unique_ptr s(new DcmAttributeTag(DCM_DimensionIndexPointer)); // VisitAttributes() + ASSERT_TRUE(s->putUint16Array(a, 2).good()); + parent->insert(s.release()); + } + + ASSERT_TRUE(dicom->GetDcmtkObject().getDataset()->insert(new DcmPixelItem(DCM_PixelData)).good()); // VisitNotSupported() + } + + { + V visitor; + dicom->Apply(visitor); + ASSERT_EQ(127u, visitor.GetSeen()); // Make sure all the methods have been applied + } + + { + Json::Value b; + dicom->DatasetToJson(b, DicomToJsonFormat_Short, DicomToJsonFlags_Default, 0); + ASSERT_EQ(Json::objectValue, b.type()); + + Json::Value::Members members = b.getMemberNames(); + ASSERT_EQ(1u, members.size()); + ASSERT_EQ("0008,1140", members[0]); + + // Check that "b["0008,1140"]" is a sequence with one single empty object + ASSERT_EQ(Json::arrayValue, b["0008,1140"].type()); + ASSERT_EQ(1u, b["0008,1140"].size()); + ASSERT_EQ(Json::objectValue, b["0008,1140"][0].type()); + ASSERT_EQ(0u, b["0008,1140"][0].size()); + } +} + + +TEST(ParsedDicomFile, MultipleFloatValue) +{ + // from https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6 + Json::Value v = Json::objectValue; + v["4010,1001"][0]["4010,1004"] = "30\\20\\10"; + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + ASSERT_TRUE(dicom->HasTag(Orthanc::DicomTag(0x4010, 0x1001))); + + DicomMap m; + ASSERT_TRUE(dicom->LookupSequenceItem(m, DicomPath(DicomTag(0x4010, 0x1001)), 0)); + ASSERT_EQ(1u, m.GetSize()); + std::string value = m.GetStringValue(DicomTag(0x4010, 0x1004), "", false); + ASSERT_EQ("30\\20\\10", value); +} + + +TEST(ParsedDicomFile, ImageInformation) +{ + // If modifying this test, make sure to reflect the modification in + // "TEST(DicomImageInformation, FromDcmtkTests)" in file "ImageProcessingTests.cpp" + + double wc, ww; + double ri, rs; + PhotometricInterpretation p; + + { + ParsedDicomFile dicom(false); + dicom.GetDefaultWindowing(wc, ww, 5); + dicom.GetRescale(ri, rs, 5); + ASSERT_DOUBLE_EQ(128.0, wc); + ASSERT_DOUBLE_EQ(256.0, ww); + ASSERT_FALSE(dicom.LookupPhotometricInterpretation(p)); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } + + { + ParsedDicomFile dicom(false); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_BitsStored, "4").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_PhotometricInterpretation, "RGB").good()); + dicom.GetDefaultWindowing(wc, ww, 5); + ASSERT_DOUBLE_EQ(8.0, wc); + ASSERT_DOUBLE_EQ(16.0, ww); + ASSERT_TRUE(dicom.LookupPhotometricInterpretation(p)); + ASSERT_EQ(PhotometricInterpretation_RGB, p); + } + + { + ParsedDicomFile dicom(false); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowCenter, "12").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowWidth, "-22").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleIntercept, "-22").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleSlope, "-23").good()); + dicom.GetDefaultWindowing(wc, ww, 5); + dicom.GetRescale(ri, rs, 5); + ASSERT_DOUBLE_EQ(12.0, wc); + ASSERT_DOUBLE_EQ(-22.0, ww); + ASSERT_DOUBLE_EQ(-22.0, ri); + ASSERT_DOUBLE_EQ(-23.0, rs); + } + + { + ParsedDicomFile dicom(false); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowCenter, "12\\13\\14").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_WindowWidth, "-22\\-23\\-24").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleIntercept, "32\\33\\34").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DCM_RescaleSlope, "-42\\-43\\-44").good()); + dicom.GetDefaultWindowing(wc, ww, 5); + dicom.GetRescale(ri, rs, 5); + ASSERT_DOUBLE_EQ(12.0, wc); + ASSERT_DOUBLE_EQ(-22.0, ww); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } + + { + // Philips multiframe + Json::Value v = Json::objectValue; + v["PerFrameFunctionalGroupsSequence"][0]["FrameVOILUTSequence"][0]["WindowCenter"] = "614"; + v["PerFrameFunctionalGroupsSequence"][0]["FrameVOILUTSequence"][0]["WindowWidth"] = "1067"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "12"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "2.551648"; + v["PerFrameFunctionalGroupsSequence"][1]["FrameVOILUTSequence"][0]["WindowCenter"] = "-61"; + v["PerFrameFunctionalGroupsSequence"][1]["FrameVOILUTSequence"][0]["WindowWidth"] = "-63"; + v["PerFrameFunctionalGroupsSequence"][1]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "13"; + v["PerFrameFunctionalGroupsSequence"][1]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "-14"; + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->GetDefaultWindowing(wc, ww, 0); + dicom->GetRescale(ri, rs, 0); + ASSERT_DOUBLE_EQ(614.0, wc); + ASSERT_DOUBLE_EQ(1067.0, ww); + ASSERT_DOUBLE_EQ(12.0, ri); + ASSERT_DOUBLE_EQ(2.551648, rs); + + dicom->GetDefaultWindowing(wc, ww, 1); + dicom->GetRescale(ri, rs, 1); + ASSERT_DOUBLE_EQ(-61.0, wc); + ASSERT_DOUBLE_EQ(-63.0, ww); + ASSERT_DOUBLE_EQ(13.0, ri); + ASSERT_DOUBLE_EQ(-14.0, rs); + + dicom->GetDefaultWindowing(wc, ww, 2); + dicom->GetRescale(ri, rs, 2); + ASSERT_DOUBLE_EQ(128.0, wc); + ASSERT_DOUBLE_EQ(256.0, ww); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } + + { + // RT-DOSE + Json::Value v = Json::objectValue; + v["RescaleIntercept"] = "10"; + v["RescaleSlope"] = "20"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleIntercept"] = "30"; + v["PerFrameFunctionalGroupsSequence"][0]["PixelValueTransformationSequence"][0]["RescaleSlope"] = "40"; + std::unique_ptr dicom(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + + dicom->GetRescale(ri, rs, 0); + ASSERT_DOUBLE_EQ(10.0, ri); + ASSERT_DOUBLE_EQ(20.0, rs); + + v["SOPClassUID"] = "1.2.840.10008.5.1.4.1.1.481.2"; + dicom.reset(ParsedDicomFile::CreateFromJson(v, DicomFromJsonFlags_None, "")); + dicom->GetRescale(ri, rs, 0); + ASSERT_DOUBLE_EQ(0.0, ri); + ASSERT_DOUBLE_EQ(1.0, rs); + } +} + + +TEST(DicomMap, DicomWebWithInteger64) +{ + /** + * This failed in Orthanc <= 1.9.7 with + * "http://localhost:8042/dicom-web/studies/1.3.6.1.4.1.14519.5.2.1.314316487728501506587013300243937537423/series/1.3.6.1.4.1.1459.5.2.1.62266640231940987006694557463549207147/instances/1.3.6.1.4.1.14519.5.2.1.147718809116229175846174241356499989705/metadata" + * of patient "GLIOMA01-i_03A6" from collection "ICDC-Glioma" of + * TCIA. + **/ + Json::Value v = Json::objectValue; + v["00191297"]["Value"][0] = 29362240; + v["00191297"]["Value"][1] = Json::Int64(4294948074l); + v["00191297"]["vr"] = "UL"; + DicomMap m; + m.FromDicomWeb(v); +} + + +TEST(ParsedDicomFile, InjectEmptyPixelData) +{ + static const char* PIXEL_DATA = "7FE00010"; + + { + ParsedDicomFile dicom(true); + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + ASSERT_FALSE(visitor.GetResult().isMember(PIXEL_DATA)); + } + + { + ParsedDicomFile dicom(true); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord); // Must be ignored + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ(2u, visitor.GetResult() [PIXEL_DATA].size()); + ASSERT_EQ("", visitor.GetResult() [PIXEL_DATA]["InlineBinary"].asString()); + ASSERT_EQ("OB", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } + + { + ParsedDicomFile dicom(true); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte); // Must be ignored + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ(2u, visitor.GetResult() [PIXEL_DATA].size()); + ASSERT_EQ("", visitor.GetResult() [PIXEL_DATA]["InlineBinary"].asString()); + ASSERT_EQ("OW", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } +} + + +TEST(ParsedDicomFile, RemoveFromPixelData) +{ + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0000), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0009), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint8Array(DcmTag(0x7fe0, 0x0010), NULL, 0).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0011), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe1, 0x0000), "").good()); + + { + DicomMap m; + dicom.ExtractDicomSummary(m, 0); + + ASSERT_EQ(10u, m.GetSize()); + ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PIXEL_DATA)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0011)); + ASSERT_TRUE(m.HasTag(0x7fe1, 0x0000)); + } + + dicom.RemoveFromPixelData(); + + { + DicomMap m; + dicom.ExtractDicomSummary(m, 0); + + ASSERT_EQ(7u, m.GetSize()); + ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009)); + ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA)); + ASSERT_FALSE(m.HasTag(0x7fe0, 0x0011)); + ASSERT_FALSE(m.HasTag(0x7fe1, 0x0000)); + } +} + + +TEST(ParsedDicomFile, GuessPixelDataValueRepresentation) +{ + typedef std::list< std::pair > Syntaxes; + + // Create a list of the main non-retired transfer syntaxes, from: + // https://www.dicomlibrary.com/dicom/transfer-syntax/ + Syntaxes compressedSyntaxes; + compressedSyntaxes.push_back(std::make_pair(EXS_DeflatedLittleEndianExplicit, DicomTransferSyntax_DeflatedLittleEndianExplicit)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess1, DicomTransferSyntax_JPEGProcess1)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess2_4, DicomTransferSyntax_JPEGProcess2_4)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess14, DicomTransferSyntax_JPEGProcess14)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess14SV1, DicomTransferSyntax_JPEGProcess14SV1)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGLSLossless, DicomTransferSyntax_JPEGLSLossless)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGLSLossy, DicomTransferSyntax_JPEGLSLossy)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000LosslessOnly, DicomTransferSyntax_JPEG2000LosslessOnly)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000, DicomTransferSyntax_JPEG2000)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000MulticomponentLosslessOnly, DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000Multicomponent, DicomTransferSyntax_JPEG2000Multicomponent)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPIPReferenced, DicomTransferSyntax_JPIPReferenced)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPIPReferencedDeflate, DicomTransferSyntax_JPIPReferencedDeflate)); + compressedSyntaxes.push_back(std::make_pair(EXS_RLELossless, DicomTransferSyntax_RLELossless)); + compressedSyntaxes.push_back(std::make_pair(EXS_MPEG2MainProfileAtMainLevel, DicomTransferSyntax_MPEG2MainProfileAtMainLevel)); + compressedSyntaxes.push_back(std::make_pair(EXS_MPEG4HighProfileLevel4_1, DicomTransferSyntax_MPEG4HighProfileLevel4_1)); + compressedSyntaxes.push_back(std::make_pair(EXS_MPEG4BDcompatibleHighProfileLevel4_1, DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1)); + + for (unsigned int i = 0; i < 3; i++) + { + unsigned int bitsAllocated; + switch (i) + { + case 0: bitsAllocated = 1; break; + case 1: bitsAllocated = 8; break; + case 2: bitsAllocated = 16; break; + default: + throw OrthancException(ErrorCode_InternalError); + } + + for (Syntaxes::const_iterator it = compressedSyntaxes.begin(); it != compressedSyntaxes.end(); ++it) + { + // All the compressed transfer syntaxes must have "OB" pixel data + ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(it->second, bitsAllocated)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast(bitsAllocated), false); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(it->second)); + } + + { + DicomMap dicom; + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(it->second)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(it->first, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + DicomTransferSyntax ts; + ASSERT_TRUE(dicom.LookupTransferSyntax(ts)); + ASSERT_EQ(ts, it->second); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation()); + } + } + + { + // Little endian implicit is always OW + ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit, bitsAllocated)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast(bitsAllocated), false); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit)); + } + + { + DicomMap dicom; + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianImplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation()); + } + } + + } + + // Explicit little and big endian with <= 8 bpp is OB + + for (unsigned int i = 0; i < 2; i++) + { + unsigned int bitsAllocated; + switch (i) + { + case 0: bitsAllocated = 1; break; + case 1: bitsAllocated = 8; break; + default: + throw OrthancException(ErrorCode_InternalError); + } + + ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit, bitsAllocated)); + ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit, bitsAllocated)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast(bitsAllocated), false); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit)); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit)); + } + + { + DicomMap dicom; + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit)); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation()); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_BigEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation()); + } + } + + // Explicit little and big endian with > 8 bpp is OW + + ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit, 16)); + ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit, 16)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit)); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, 16).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation()); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, 16).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_BigEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation()); + } +} + + +#if ORTHANC_SANDBOXED != 1 +TEST(ParsedDicomFile, DISABLED_InjectEmptyPixelData2) +{ + static const char* PIXEL_DATA = "7FE00010"; + + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::string path = (std::string(getenv("HOME")) + + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string(GetTransferSyntaxUid(a)) + ".dcm"); + if (Orthanc::SystemToolbox::IsRegularFile(path)) + { + printf("\n======= %s\n", GetTransferSyntaxUid(a)); + + std::string source; + Orthanc::SystemToolbox::ReadFile(source, path); + + ParsedDicomFile dicom(source); + std::unique_ptr removal(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData)); + + { + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + ASSERT_FALSE(visitor.GetResult().isMember(PIXEL_DATA)); + } + + { + DicomWebJsonVisitor visitor; + dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte); + dicom.Apply(visitor); + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ("OB", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } + + removal.reset(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData)); + + { + DicomWebJsonVisitor visitor; + dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord); + dicom.Apply(visitor); + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ("OW", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } + } + } +} +#endif + + + +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + +#include "../Sources/DicomNetworking/DicomStoreUserConnection.h" +#include "../Sources/DicomParsing/DcmtkTranscoder.h" + +TEST(Toto, DISABLED_Transcode3) +{ + DicomAssociationParameters p; + p.SetRemotePort(2000); + + DicomStoreUserConnection scu(p); + scu.SetCommonClassesProposed(false); + scu.SetRetiredBigEndianProposed(true); + + DcmtkTranscoder transcoder(1); + + for (int j = 0; j < 2; j++) + { + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::string path = (std::string(getenv("HOME")) + + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string(GetTransferSyntaxUid(a)) + ".dcm"); + if (Orthanc::SystemToolbox::IsRegularFile(path)) + { + printf("\n======= %s\n", GetTransferSyntaxUid(a)); + + std::string source; + Orthanc::SystemToolbox::ReadFile(source, path); + + std::string c, k; + try + { + scu.Transcode(c, k, transcoder, source.c_str(), source.size(), + DicomTransferSyntax_LittleEndianExplicit, false, "", 0); + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_NotImplemented) + { + LOG(ERROR) << "cannot transcode " << GetTransferSyntaxUid(a); + } + else + { + throw; + } + } + } + } + } +} + + +TEST(Toto, DISABLED_Transcode4) +{ + std::unique_ptr toto; + + { + std::string source; + Orthanc::SystemToolbox::ReadFile(source, std::string(getenv("HOME")) + + "/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm"); + toto.reset(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size())); + } + + const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto); + + DicomTransferSyntax sourceSyntax; + ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto)); + + DcmtkTranscoder transcoder(1); + + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::set s; + s.insert(a); + + std::string t; + + IDicomTranscoder::DicomImage source, target; + source.AcquireParsed(dynamic_cast(toto->clone())); + + if (!transcoder.Transcode(target, source, s, true)) + { + printf("**************** CANNOT: [%s] => [%s]\n", + GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a)); + } + else + { + DicomTransferSyntax targetSyntax; + ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed())); + + ASSERT_EQ(targetSyntax, a); + bool lossy = (a == DicomTransferSyntax_JPEGProcess1 || + a == DicomTransferSyntax_JPEGProcess2_4 || + a == DicomTransferSyntax_JPEGLSLossy); + + printf("SIZE: %d\n", static_cast(t.size())); + if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed())) + { + ASSERT_FALSE(lossy); + } + else + { + ASSERT_TRUE(lossy); + } + } + } +} + +#endif diff --git a/OrthancFramework/UnitTestsSources/GithubCACertificates.h b/OrthancFramework/UnitTestsSources/GithubCACertificates.h new file mode 100644 index 0000000..7afd538 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/GithubCACertificates.h @@ -0,0 +1,37 @@ +#define GITHUB_CERTIFICATES \ +"-----BEGIN CERTIFICATE-----\n" \ +"MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB\n" \ +"iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n" \ +"cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n" \ +"BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx\n" \ +"MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV\n" \ +"BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE\n" \ +"ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g\n" \ +"VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" \ +"AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N\n" \ +"TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj\n" \ +"eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E\n" \ +"oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk\n" \ +"Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY\n" \ +"uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j\n" \ +"BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb\n" \ +"+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G\n" \ +"A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw\n" \ +"CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0\n" \ +"LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr\n" \ +"BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv\n" \ +"bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov\n" \ +"L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H\n" \ +"ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH\n" \ +"7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi\n" \ +"H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx\n" \ +"RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv\n" \ +"xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38\n" \ +"sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL\n" \ +"l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq\n" \ +"6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY\n" \ +"LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5\n" \ +"yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K\n" \ +"00u/I5sUKUErmgQfky3xxzlIPK1aEn8=\n" \ +"-----END CERTIFICATE-----\n" \ +"\n" diff --git a/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp new file mode 100644 index 0000000..6ef237b --- /dev/null +++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp @@ -0,0 +1,1551 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Compatibility.h" +#include "../Sources/DicomFormat/DicomImageInformation.h" +#include "../Sources/Images/Image.h" +#include "../Sources/Images/ImageProcessing.h" +#include "../Sources/Images/ImageTraits.h" +#include "../Sources/OrthancException.h" + +#include + +using namespace Orthanc; + + +TEST(DicomImageInformation, ExtractPixelFormat1) +{ + // Cardiac/MR* + DicomMap m; + m.SetValue(DICOM_TAG_ROWS, "24", false); + m.SetValue(DICOM_TAG_COLUMNS, "16", false); + m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false); + m.SetValue(DICOM_TAG_SAMPLES_PER_PIXEL, "1", false); + m.SetValue(DICOM_TAG_BITS_STORED, "12", false); + m.SetValue(DICOM_TAG_HIGH_BIT, "11", false); + m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "0", false); + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false); + + DicomImageInformation info(m); + PixelFormat format; + ASSERT_TRUE(info.ExtractPixelFormat(format, false)); + ASSERT_EQ(PixelFormat_Grayscale16, format); +} + + +TEST(DicomImageInformation, ExtractPixelFormat2) +{ + // Delphine CT + DicomMap m; + m.SetValue(DICOM_TAG_ROWS, "24", false); + m.SetValue(DICOM_TAG_COLUMNS, "16", false); + m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false); + m.SetValue(DICOM_TAG_SAMPLES_PER_PIXEL, "1", false); + m.SetValue(DICOM_TAG_BITS_STORED, "16", false); + m.SetValue(DICOM_TAG_HIGH_BIT, "15", false); + m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "1", false); + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false); + + DicomImageInformation info(m); + PixelFormat format; + ASSERT_TRUE(info.ExtractPixelFormat(format, false)); + ASSERT_EQ(PixelFormat_SignedGrayscale16, format); +} + + +TEST(DicomImageInformation, Windowing) +{ + DicomMap m; + m.SetValue(DICOM_TAG_ROWS, "24", false); + m.SetValue(DICOM_TAG_COLUMNS, "16", false); + m.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false); + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false); + + { + DicomImageInformation info(m); + ASSERT_DOUBLE_EQ(1.0, info.GetRescaleSlope()); + ASSERT_DOUBLE_EQ(0.0, info.GetRescaleIntercept()); + ASSERT_EQ(PhotometricInterpretation_Monochrome2, info.GetPhotometricInterpretation()); + ASSERT_EQ(0, info.GetWindowsCount()); + ASSERT_DOUBLE_EQ(14.0, info.ApplyRescale(14.0)); + } + + m.SetValue(DICOM_TAG_RESCALE_SLOPE, "10.25", false); + m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-1.75", false); + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME1", false); + + { + DicomImageInformation info(m); + ASSERT_DOUBLE_EQ(10.25, info.GetRescaleSlope()); + ASSERT_DOUBLE_EQ(-1.75, info.GetRescaleIntercept()); + ASSERT_EQ(PhotometricInterpretation_Monochrome1, info.GetPhotometricInterpretation()); + ASSERT_FALSE(info.HasWindows()); + ASSERT_EQ(0, info.GetWindowsCount()); + ASSERT_THROW(info.GetWindow(0), OrthancException); + ASSERT_DOUBLE_EQ(141.75, info.ApplyRescale(14.0)); + } + + m.SetValue(Orthanc::DICOM_TAG_WINDOW_CENTER, "10\\100\\1000", false); + m.SetValue(Orthanc::DICOM_TAG_WINDOW_WIDTH, "50\\60\\70", false); + + { + DicomImageInformation info(m); + ASSERT_TRUE(info.HasWindows()); + ASSERT_EQ(3u, info.GetWindowsCount()); + ASSERT_DOUBLE_EQ(10.0, info.GetWindow(0).GetCenter()); + ASSERT_DOUBLE_EQ(50.0, info.GetWindow(0).GetWidth()); + ASSERT_DOUBLE_EQ(100.0, info.GetWindow(1).GetCenter()); + ASSERT_DOUBLE_EQ(60.0, info.GetWindow(1).GetWidth()); + ASSERT_DOUBLE_EQ(1000.0, info.GetWindow(2).GetCenter()); + ASSERT_DOUBLE_EQ(70.0, info.GetWindow(2).GetWidth()); + ASSERT_THROW(info.GetWindow(3), OrthancException); + } +} + + +TEST(DicomImageInformation, FromDcmtkTests) +{ + // This replicates TEST(ParsedDicomFile, ImageInformation) in + // "FromDcmtkTests.cpp", without the handling of frames and sequences + + DicomMap m; + m.SetValue(DICOM_TAG_ROWS, "24", false); + m.SetValue(DICOM_TAG_COLUMNS, "16", false); + m.SetValue(DICOM_TAG_BITS_ALLOCATED, "8", false); + + { + DicomImageInformation info(m); + Window w = info.GetDefaultWindow(); + ASSERT_DOUBLE_EQ(128.0, w.GetCenter()); + ASSERT_DOUBLE_EQ(256.0, w.GetWidth()); + ASSERT_EQ(PhotometricInterpretation_Unknown, info.GetPhotometricInterpretation()); + ASSERT_DOUBLE_EQ(0.0, info.GetRescaleIntercept()); + ASSERT_DOUBLE_EQ(1.0, info.GetRescaleSlope()); + + double offset, scaling, x; + info.ComputeRenderingTransform(offset, scaling, Window(-100, 200)); + + x = -200; ASSERT_NEAR(0, x * scaling + offset, 0.000001); + x = -100; ASSERT_NEAR(127.5, x * scaling + offset, 0.000001); + x = 0; ASSERT_NEAR(255, x * scaling + offset, 0.000001); + } + + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME1", false); + + { + DicomImageInformation info(m); + ASSERT_EQ(PhotometricInterpretation_Monochrome1, info.GetPhotometricInterpretation()); + + double offset, scaling, x; + info.ComputeRenderingTransform(offset, scaling, Window(-100, 200)); + + x = -200; ASSERT_NEAR(255, x * scaling + offset, 0.000001); + x = -100; ASSERT_NEAR(127.5, x * scaling + offset, 0.000001); + x = 0; ASSERT_NEAR(0, x * scaling + offset, 0.000001); + } + + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2", false); + m.SetValue(DICOM_TAG_RESCALE_SLOPE, "20", false); + m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-100", false); + + { + DicomImageInformation info(m); + ASSERT_EQ(PhotometricInterpretation_Monochrome2, info.GetPhotometricInterpretation()); + + double offset, scaling, x; + info.ComputeRenderingTransform(offset, scaling, Window(-100, 200)); + + x = -5; ASSERT_NEAR(0, x * scaling + offset, 0.000001); + x = 0; ASSERT_NEAR(127.5, x * scaling + offset, 0.000001); + x = 5; ASSERT_NEAR(255, x * scaling + offset, 0.000001); + } + + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME1", false); + + { + DicomImageInformation info(m); + ASSERT_EQ(PhotometricInterpretation_Monochrome1, info.GetPhotometricInterpretation()); + + double offset, scaling, x; + info.ComputeRenderingTransform(offset, scaling, Window(-100, 200)); + + x = -5; ASSERT_NEAR(255, x * scaling + offset, 0.000001); + x = 0; ASSERT_NEAR(127.5, x * scaling + offset, 0.000001); + x = 5; ASSERT_NEAR(0, x * scaling + offset, 0.000001); + } + + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB", false); + m.SetValue(DICOM_TAG_BITS_STORED, "4", false); + + { + DicomImageInformation info(m); + Window w = info.GetDefaultWindow(); + ASSERT_DOUBLE_EQ(8.0, w.GetCenter()); + ASSERT_DOUBLE_EQ(16.0, w.GetWidth()); + ASSERT_EQ(PhotometricInterpretation_RGB, info.GetPhotometricInterpretation()); + } + + m.SetValue(DICOM_TAG_WINDOW_CENTER, "12", false); + m.SetValue(DICOM_TAG_WINDOW_WIDTH, "-22", false); + m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-22", false); + m.SetValue(DICOM_TAG_RESCALE_SLOPE, "-23", false); + + { + DicomImageInformation info(m); + Window w = info.GetDefaultWindow(); + ASSERT_DOUBLE_EQ(12.0, w.GetCenter()); + ASSERT_DOUBLE_EQ(22.0, w.GetWidth()); + ASSERT_DOUBLE_EQ(-22.0, info.GetRescaleIntercept()); + ASSERT_DOUBLE_EQ(-23.0, info.GetRescaleSlope()); + } + + m.Remove(DICOM_TAG_RESCALE_SLOPE); + m.Remove(DICOM_TAG_RESCALE_INTERCEPT); + m.SetValue(DICOM_TAG_WINDOW_CENTER, "12\\13\\14", false); + m.SetValue(DICOM_TAG_WINDOW_WIDTH, "-22\\-23\\-24", false); + m.SetValue(DICOM_TAG_RESCALE_INTERCEPT, "-22\\33\\34", false); + m.SetValue(DICOM_TAG_RESCALE_SLOPE, "-23\\-43\\-44", false); + + { + DicomImageInformation info(m); + Window w = info.GetDefaultWindow(); + ASSERT_DOUBLE_EQ(12.0, w.GetCenter()); + ASSERT_DOUBLE_EQ(22.0, w.GetWidth()); + ASSERT_DOUBLE_EQ(0.0, info.GetRescaleIntercept()); + ASSERT_DOUBLE_EQ(1.0, info.GetRescaleSlope()); + } +} + + +namespace +{ + template + class TestImageTraits : public ::testing::Test + { + private: + std::unique_ptr image_; + + protected: + virtual void SetUp() ORTHANC_OVERRIDE + { + image_.reset(new Image(ImageTraits::PixelTraits::GetPixelFormat(), 7, 9, false)); + } + + virtual void TearDown() ORTHANC_OVERRIDE + { + image_.reset(NULL); + } + + public: + typedef T ImageTraits; + + ImageAccessor& GetImage() + { + return *image_; + } + }; + + template + class TestIntegerImageTraits : public TestImageTraits + { + }; +} + + +typedef ::testing::Types< + ImageTraits, + ImageTraits, + ImageTraits + > IntegerFormats; +TYPED_TEST_CASE(TestIntegerImageTraits, IntegerFormats); + +typedef ::testing::Types< + ImageTraits, + ImageTraits, + ImageTraits, + ImageTraits, + ImageTraits + > AllFormats; +TYPED_TEST_CASE(TestImageTraits, AllFormats); + + +TYPED_TEST(TestImageTraits, SetZero) +{ + ImageAccessor& image = this->GetImage(); + + memset(image.GetBuffer(), 128, image.GetHeight() * image.GetWidth()); + + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + ImageProcessing::Set(image, 0); + break; + + case PixelFormat_RGB24: + case PixelFormat_BGRA32: + ImageProcessing::Set(image, 0, 0, 0, 0); + break; + + default: + ASSERT_TRUE(0); + } + + typename TestFixture::ImageTraits::PixelType zero, value; + TestFixture::ImageTraits::PixelTraits::SetZero(zero); + + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + for (unsigned int x = 0; x < image.GetWidth(); x++) + { + TestFixture::ImageTraits::GetPixel(value, image, x, y); + ASSERT_TRUE(TestFixture::ImageTraits::PixelTraits::IsEqual(zero, value)); + } + } +} + + +TYPED_TEST(TestIntegerImageTraits, SetZeroFloat) +{ + ImageAccessor& image = this->GetImage(); + + memset(image.GetBuffer(), 128, image.GetHeight() * image.GetWidth()); + + float c = 0.0f; + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + for (unsigned int x = 0; x < image.GetWidth(); x++, c++) + { + TestFixture::ImageTraits::SetFloatPixel(image, c, x, y); + } + } + + c = 0.0f; + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + for (unsigned int x = 0; x < image.GetWidth(); x++, c++) + { + ASSERT_FLOAT_EQ(c, TestFixture::ImageTraits::GetFloatPixel(image, x, y)); + } + } +} + + +TYPED_TEST(TestIntegerImageTraits, FillPolygon) +{ + ImageAccessor& image = this->GetImage(); + + ImageProcessing::Set(image, 128); + + // draw a triangle + std::vector points; + points.push_back(ImageProcessing::ImagePoint(1,1)); + points.push_back(ImageProcessing::ImagePoint(1,5)); + points.push_back(ImageProcessing::ImagePoint(5,5)); + + ImageProcessing::FillPolygon(image, points, 255); + + // outside polygon + ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 0, 0)); + ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 0, 6)); + ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 6, 6)); + ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 6, 0)); + + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 1)); + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 2)); + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 5)); + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 2, 4)); + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 5, 5)); +} + +TYPED_TEST(TestIntegerImageTraits, FillPolygonLargerThanImage) +{ + ImageAccessor& image = this->GetImage(); + + ImageProcessing::Set(image, 0); + + std::vector points; + points.push_back(ImageProcessing::ImagePoint(0, 0)); + points.push_back(ImageProcessing::ImagePoint(image.GetWidth(),0)); + points.push_back(ImageProcessing::ImagePoint(image.GetWidth(),image.GetHeight())); + points.push_back(ImageProcessing::ImagePoint(0,image.GetHeight())); + + ImageProcessing::FillPolygon(image, points, 255); + + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + for (unsigned int x = 0; x < image.GetWidth(); x++) + { + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, x, y)); + } + } +} + +TYPED_TEST(TestIntegerImageTraits, FillPolygonFullImage) +{ + ImageAccessor& image = this->GetImage(); + + ImageProcessing::Set(image, 0); + + std::vector points; + points.push_back(ImageProcessing::ImagePoint(0, 0)); + points.push_back(ImageProcessing::ImagePoint(image.GetWidth() - 1,0)); + points.push_back(ImageProcessing::ImagePoint(image.GetWidth() - 1,image.GetHeight() - 1)); + points.push_back(ImageProcessing::ImagePoint(0,image.GetHeight() - 1)); + + ImageProcessing::FillPolygon(image, points, 255); + + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 0, 0)); + ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, image.GetWidth() - 1, image.GetHeight() - 1)); +} + + + + +static void SetGrayscale8Pixel(ImageAccessor& image, + unsigned int x, + unsigned int y, + uint8_t value) +{ + ImageTraits::SetPixel(image, value, x, y); +} + +static bool TestGrayscale8Pixel(const ImageAccessor& image, + unsigned int x, + unsigned int y, + uint8_t value) +{ + PixelTraits::PixelType p; + ImageTraits::GetPixel(p, image, x, y); + if (p != value) printf("%d %d\n", p, value); + return p == value; +} + +static void SetGrayscale16Pixel(ImageAccessor& image, + unsigned int x, + unsigned int y, + uint16_t value) +{ + ImageTraits::SetPixel(image, value, x, y); +} + +static bool TestGrayscale16Pixel(const ImageAccessor& image, + unsigned int x, + unsigned int y, + uint16_t value) +{ + PixelTraits::PixelType p; + ImageTraits::GetPixel(p, image, x, y); + if (p != value) printf("%d %d\n", p, value); + return p == value; +} + +static void SetSignedGrayscale16Pixel(ImageAccessor& image, + unsigned int x, + unsigned int y, + int16_t value) +{ + ImageTraits::SetPixel(image, value, x, y); +} + +static bool TestSignedGrayscale16Pixel(const ImageAccessor& image, + unsigned int x, + unsigned int y, + int16_t value) +{ + PixelTraits::PixelType p; + ImageTraits::GetPixel(p, image, x, y); + if (p != value) printf("%d %d\n", p, value); + return p == value; +} + +static void SetRGB24Pixel(ImageAccessor& image, + unsigned int x, + unsigned int y, + uint8_t red, + uint8_t green, + uint8_t blue) +{ + PixelTraits::PixelType p; + p.red_ = red; + p.green_ = green; + p.blue_ = blue; + ImageTraits::SetPixel(image, p, x, y); +} + +static bool TestRGB24Pixel(const ImageAccessor& image, + unsigned int x, + unsigned int y, + uint8_t red, + uint8_t green, + uint8_t blue) +{ + PixelTraits::PixelType p; + ImageTraits::GetPixel(p, image, x, y); + bool ok = (p.red_ == red && + p.green_ == green && + p.blue_ == blue); + if (!ok) printf("%d,%d,%d %d,%d,%d\n", p.red_, p.green_, p.blue_, red, green, blue); + return ok; +} + + +TEST(ImageProcessing, FlipGrayscale8) +{ + { + Image image(PixelFormat_Grayscale8, 0, 0, false); + ImageProcessing::FlipX(image); + ImageProcessing::FlipY(image); + } + + { + Image image(PixelFormat_Grayscale8, 1, 1, false); + SetGrayscale8Pixel(image, 0, 0, 128); + ImageProcessing::FlipX(image); + ImageProcessing::FlipY(image); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 128)); + } + + { + Image image(PixelFormat_Grayscale8, 3, 2, false); + SetGrayscale8Pixel(image, 0, 0, 10); + SetGrayscale8Pixel(image, 1, 0, 20); + SetGrayscale8Pixel(image, 2, 0, 30); + SetGrayscale8Pixel(image, 0, 1, 40); + SetGrayscale8Pixel(image, 1, 1, 50); + SetGrayscale8Pixel(image, 2, 1, 60); + + ImageProcessing::FlipX(image); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 30)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 10)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 60)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 50)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 40)); + + ImageProcessing::FlipY(image); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 60)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 50)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 40)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 30)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 10)); + } +} + + + +TEST(ImageProcessing, FlipRGB24) +{ + Image image(PixelFormat_RGB24, 2, 2, false); + SetRGB24Pixel(image, 0, 0, 10, 100, 110); + SetRGB24Pixel(image, 1, 0, 20, 100, 110); + SetRGB24Pixel(image, 0, 1, 30, 100, 110); + SetRGB24Pixel(image, 1, 1, 40, 100, 110); + + ImageProcessing::FlipX(image); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 20, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 10, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 40, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 30, 100, 110)); + + ImageProcessing::FlipY(image); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 40, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 30, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 20, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 10, 100, 110)); +} + + +TEST(ImageProcessing, ResizeBasicGrayscale8) +{ + Image source(PixelFormat_Grayscale8, 2, 2, false); + SetGrayscale8Pixel(source, 0, 0, 10); + SetGrayscale8Pixel(source, 1, 0, 20); + SetGrayscale8Pixel(source, 0, 1, 30); + SetGrayscale8Pixel(source, 1, 1, 40); + + { + Image target(PixelFormat_Grayscale8, 2, 4, false); + ImageProcessing::Resize(target, source); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 10)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 10)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 2, 30)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 2, 40)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 3, 30)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 3, 40)); + } + + { + Image target(PixelFormat_Grayscale8, 4, 2, false); + ImageProcessing::Resize(target, source); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 10)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 10)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 30)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 30)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 1, 40)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 1, 40)); + } +} + + +TEST(ImageProcessing, ResizeBasicRGB24) +{ + Image source(PixelFormat_RGB24, 2, 2, false); + SetRGB24Pixel(source, 0, 0, 10, 100, 110); + SetRGB24Pixel(source, 1, 0, 20, 100, 110); + SetRGB24Pixel(source, 0, 1, 30, 100, 110); + SetRGB24Pixel(source, 1, 1, 40, 100, 110); + + { + Image target(PixelFormat_RGB24, 2, 4, false); + ImageProcessing::Resize(target, source); + ASSERT_TRUE(TestRGB24Pixel(target, 0, 0, 10, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 1, 0, 20, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 0, 1, 10, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 1, 1, 20, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 0, 2, 30, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 1, 2, 40, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 0, 3, 30, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 1, 3, 40, 100, 110)); + } + + { + Image target(PixelFormat_RGB24, 4, 2, false); + ImageProcessing::Resize(target, source); + ASSERT_TRUE(TestRGB24Pixel(target, 0, 0, 10, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 1, 0, 10, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 2, 0, 20, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 3, 0, 20, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 0, 1, 30, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 1, 1, 30, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 2, 1, 40, 100, 110)); + ASSERT_TRUE(TestRGB24Pixel(target, 3, 1, 40, 100, 110)); + } +} + + +TEST(ImageProcessing, ResizeEmptyGrayscale8) +{ + { + Image source(PixelFormat_Grayscale8, 0, 0, false); + Image target(PixelFormat_Grayscale8, 2, 2, false); + ImageProcessing::Resize(target, source); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 0)); + } + + { + Image source(PixelFormat_Grayscale8, 2, 2, false); + Image target(PixelFormat_Grayscale8, 0, 0, false); + ImageProcessing::Resize(target, source); + } +} + + +TEST(ImageProcessing, Convolution) +{ + std::vector k1(5, 1); + std::vector k2(1, 1); + + { + Image image(PixelFormat_Grayscale8, 1, 1, false); + SetGrayscale8Pixel(image, 0, 0, 100); + ImageProcessing::SeparableConvolution(image, k1, 2, k2, 0, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100)); + ImageProcessing::SeparableConvolution(image, k1, 2, k1, 2, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100)); + ImageProcessing::SeparableConvolution(image, k2, 0, k1, 2, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100)); + ImageProcessing::SeparableConvolution(image, k2, 0, k2, 0, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100)); + } + + { + Image image(PixelFormat_RGB24, 1, 1, false); + SetRGB24Pixel(image, 0, 0, 10, 20, 30); + ImageProcessing::SeparableConvolution(image, k1, 2, k2, 0, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30)); + ImageProcessing::SeparableConvolution(image, k1, 2, k1, 2, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30)); + ImageProcessing::SeparableConvolution(image, k2, 0, k1, 2, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30)); + ImageProcessing::SeparableConvolution(image, k2, 0, k2, 0, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30)); + } + + { + Image dirac(PixelFormat_Grayscale8, 9, 1, false); + ImageProcessing::Set(dirac, 0); + SetGrayscale8Pixel(dirac, 4, 0, 100); + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 100)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 100)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0)); + } + } + + { + Image dirac(PixelFormat_Grayscale8, 1, 9, false); + ImageProcessing::Set(dirac, 0); + SetGrayscale8Pixel(dirac, 0, 4, 100); + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 20)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 100)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0, true /* round */); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 100)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0)); + } + } + + { + Image dirac(PixelFormat_RGB24, 9, 1, false); + ImageProcessing::Set(dirac, 0); + SetRGB24Pixel(dirac, 4, 0, 100, 120, 140); + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 100, 120, 140)); + ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 100, 120, 140)); + ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0)); + } + } + + { + Image dirac(PixelFormat_RGB24, 1, 9, false); + ImageProcessing::Set(dirac, 0); + SetRGB24Pixel(dirac, 0, 4, 100, 120, 140); + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 20, 24, 28)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 100, 120, 140)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0)); + } + + { + std::unique_ptr image(Image::Clone(dirac)); + ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0, true /* round */); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 100, 120, 140)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0)); + ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0)); + } + } +} + + +TEST(ImageProcessing, SmoothGaussian5x5) +{ + /** + Test the point spread function, as can be seen in Octave: + g1 = [ 1 4 6 4 1 ]; + g1 /= sum(g1); + g2 = conv2(g1, g1'); + floor(conv2(diag([ 0 0 100 0 0 ]), g2, 'same')) % red/green channels + floor(conv2(diag([ 0 0 200 0 0 ]), g2, 'same')) % blue channel + **/ + + { + Image image(PixelFormat_Grayscale8, 5, 5, false); + ImageProcessing::Set(image, 0); + SetGrayscale8Pixel(image, 2, 2, 100); + ImageProcessing::SmoothGaussian5x5(image, true /* round */); + + // In Octave: round(conv2([1 4 6 4 1],[1 4 6 4 1]')/256*100) + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 6)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 9)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 1, 6)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 1, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 2, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 2, 9)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 2, 14)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 2, 9)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 2, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 3, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 3, 6)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 3, 9)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 3, 6)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 3, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 4, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 4, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 4, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 4, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 4, 0)); + } + + { + Image image(PixelFormat_RGB24, 5, 5, false); + ImageProcessing::Set(image, 0); + SetRGB24Pixel(image, 2, 2, 100, 100, 200); + ImageProcessing::SmoothGaussian5x5(image, true /* round */); + + // In Octave: + // R,G = round(conv2([1 4 6 4 1],[1 4 6 4 1]')/256*100) + // B = round(conv2([1 4 6 4 1],[1 4 6 4 1]')/256*200) + ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 0, 0, 1)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 2, 0, 2, 2, 5)); + ASSERT_TRUE(TestRGB24Pixel(image, 3, 0, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 4, 0, 0, 0, 1)); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 6, 6, 13)); + ASSERT_TRUE(TestRGB24Pixel(image, 2, 1, 9, 9, 19)); + ASSERT_TRUE(TestRGB24Pixel(image, 3, 1, 6, 6, 13)); + ASSERT_TRUE(TestRGB24Pixel(image, 4, 1, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 2, 2, 2, 5)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 2, 9, 9, 19)); + ASSERT_TRUE(TestRGB24Pixel(image, 2, 2, 14, 14, 28)); + ASSERT_TRUE(TestRGB24Pixel(image, 3, 2, 9, 9, 19)); + ASSERT_TRUE(TestRGB24Pixel(image, 4, 2, 2, 2, 5)); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 3, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 3, 6, 6, 13)); + ASSERT_TRUE(TestRGB24Pixel(image, 2, 3, 9, 9, 19)); + ASSERT_TRUE(TestRGB24Pixel(image, 3, 3, 6, 6, 13)); + ASSERT_TRUE(TestRGB24Pixel(image, 4, 3, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 0, 4, 0, 0, 1)); + ASSERT_TRUE(TestRGB24Pixel(image, 1, 4, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 2, 4, 2, 2, 5)); + ASSERT_TRUE(TestRGB24Pixel(image, 3, 4, 2, 2, 3)); + ASSERT_TRUE(TestRGB24Pixel(image, 4, 4, 0, 0, 1)); + } +} + +TEST(ImageProcessing, ApplyWindowingFloatToGrayScale8) +{ + { + Image image(PixelFormat_Float32, 6, 1, false); + ImageTraits::SetFloatPixel(image, -5.0f, 0, 0); + ImageTraits::SetFloatPixel(image, 0.0f, 1, 0); + ImageTraits::SetFloatPixel(image, 5.0f, 2, 0); + ImageTraits::SetFloatPixel(image, 10.0f, 3, 0); + ImageTraits::SetFloatPixel(image, 1000.0f, 4, 0); + ImageTraits::SetFloatPixel(image, 2.0f, 5, 0); + + { + Image target(PixelFormat_Grayscale8, 6, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 128)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255*2/10)); + } + + { + Image target(PixelFormat_Grayscale8, 6, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true); + + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 127)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255 - 255*2/10)); + } + + { + Image target(PixelFormat_Grayscale8, 6, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, false); + + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 128)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255*2/10)); + } + + { + Image target(PixelFormat_Grayscale8, 6, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, true); + + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 127)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255 - 256*2/10)); + } + + { + Image target(PixelFormat_Grayscale8, 6, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 50.0f, 100.1f, 10.0f, 30.0f, false); + + ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); // (-5 * 10) + 30 => pixel value = -20 => 0 + ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 256*30/100)); // ((0 * 10) + 30 => pixel value = 30 => 30% + ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 256*80/100)); // ((5 * 10) + 30 => pixel value = 80 => 80% + ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255)); // ((10 * 10) + 30 => pixel value = 130 => 100% + ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255)); // ((1000 * 10) + 30 => pixel value = 10030 => 100% + ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 128)); // ((2 * 10) + 30 => pixel value = 50 => 50% + } + + } +} + +TEST(ImageProcessing, ApplyWindowingFloatToGrayScale16) +{ + { + Image image(PixelFormat_Float32, 6, 1, false); + ImageTraits::SetFloatPixel(image, -5.0f, 0, 0); + ImageTraits::SetFloatPixel(image, 0.0f, 1, 0); + ImageTraits::SetFloatPixel(image, 5.0f, 2, 0); + ImageTraits::SetFloatPixel(image, 10.0f, 3, 0); + ImageTraits::SetFloatPixel(image, 1000.0f, 4, 0); + ImageTraits::SetFloatPixel(image, 2.0f, 5, 0); + + { + Image target(PixelFormat_Grayscale16, 6, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + + ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 0)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 32768)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 5, 0, 65536*2/10)); + } + } +} + +TEST(ImageProcessing, ApplyWindowingGrayScale8ToGrayScale16) +{ + { + Image image(PixelFormat_Grayscale8, 5, 1, false); + SetGrayscale8Pixel(image, 0, 0, 0); + SetGrayscale8Pixel(image, 1, 0, 2); + SetGrayscale8Pixel(image, 2, 0, 5); + SetGrayscale8Pixel(image, 3, 0, 10); + SetGrayscale8Pixel(image, 4, 0, 255); + + { + Image target(PixelFormat_Grayscale16, 5, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + + ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 65536*5/10)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535)); + } + } +} + +TEST(ImageProcessing, ApplyWindowingGrayScale16ToGrayScale16) +{ + { + Image image(PixelFormat_Grayscale16, 5, 1, false); + SetGrayscale16Pixel(image, 0, 0, 0); + SetGrayscale16Pixel(image, 1, 0, 2); + SetGrayscale16Pixel(image, 2, 0, 5); + SetGrayscale16Pixel(image, 3, 0, 10); + SetGrayscale16Pixel(image, 4, 0, 255); + + { + Image target(PixelFormat_Grayscale16, 5, 1, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + + ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 65536*5/10)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535)); + ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535)); + } + } +} + + +TEST(ImageProcessing, ShiftScaleGrayscale8) +{ + Image image(PixelFormat_Grayscale8, 5, 1, false); + SetGrayscale8Pixel(image, 0, 0, 0); + SetGrayscale8Pixel(image, 1, 0, 2); + SetGrayscale8Pixel(image, 2, 0, 5); + SetGrayscale8Pixel(image, 3, 0, 10); + SetGrayscale8Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -1.1f, 1.5f, true); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0)); // (0 - 1.1) * 1.5 = -1.65 ==> 0 + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1)); // (2 - 1.1) * 1.5 = 1.35 => 1 + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 6)); // (5 - 1.1) * 1.5 = 5.85 => 6 + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 13)); // (10 - 1.1) * 1.5 = 13.35 => 13 + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 255)); +} + + +TEST(ImageProcessing, Grayscale8_Identity) +{ + Image image(PixelFormat_Float32, 5, 1, false); + ImageTraits::SetPixel(image, 0, 0, 0); + ImageTraits::SetPixel(image, 2.5, 1, 0); + ImageTraits::SetPixel(image, 5.5, 2, 0); + ImageTraits::SetPixel(image, 10.5, 3, 0); + ImageTraits::SetPixel(image, 255.5, 4, 0); + + Image image2(PixelFormat_Grayscale8, 5, 1, false); + ImageProcessing::ShiftScale(image2, image, 0, 1, false); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 1, 0, 2)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 2, 0, 5)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 3, 0, 10)); + ASSERT_TRUE(TestGrayscale8Pixel(image2, 4, 0, 255)); +} + + +TEST(ImageProcessing, ShiftScaleGrayscale16) +{ + Image image(PixelFormat_Grayscale16, 5, 1, false); + SetGrayscale16Pixel(image, 0, 0, 0); + SetGrayscale16Pixel(image, 1, 0, 2); + SetGrayscale16Pixel(image, 2, 0, 5); + SetGrayscale16Pixel(image, 3, 0, 10); + SetGrayscale16Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -1.1f, 1.5f, true); + ASSERT_TRUE(TestGrayscale16Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 1, 0, 1)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 2, 0, 6)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 3, 0, 13)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 4, 0, 381)); +} + + +TEST(ImageProcessing, ShiftScaleSignedGrayscale16) +{ + Image image(PixelFormat_SignedGrayscale16, 5, 1, false); + SetSignedGrayscale16Pixel(image, 0, 0, 0); + SetSignedGrayscale16Pixel(image, 1, 0, 2); + SetSignedGrayscale16Pixel(image, 2, 0, 5); + SetSignedGrayscale16Pixel(image, 3, 0, 10); + SetSignedGrayscale16Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -17.1f, 11.5f, true); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, -197)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, -174)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, -139)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, -82)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 2736)); +} + + +TEST(ImageProcessing, ShiftScaleSignedGrayscale16_Identity) +{ + Image image(PixelFormat_SignedGrayscale16, 5, 1, false); + SetSignedGrayscale16Pixel(image, 0, 0, 0); + SetSignedGrayscale16Pixel(image, 1, 0, 2); + SetSignedGrayscale16Pixel(image, 2, 0, 5); + SetSignedGrayscale16Pixel(image, 3, 0, 10); + SetSignedGrayscale16Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, 0, 1, true); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, 2)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, 5)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, 10)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 255)); +} + + +TEST(ImageProcessing, ShiftFloatBuggy) +{ + // This test failed in Orthanc 1.10.1 + + Image image(PixelFormat_Float32, 3, 1, false); + ImageTraits::SetFloatPixel(image, -1.0f, 0, 0); + ImageTraits::SetFloatPixel(image, 0.0f, 1, 0); + ImageTraits::SetFloatPixel(image, 1.0f, 2, 0); + + std::unique_ptr cloned(Image::Clone(image)); + + ImageProcessing::ShiftScale2(image, 0, 0.000539, true); + ASSERT_FLOAT_EQ(-0.000539f, ImageTraits::GetFloatPixel(image, 0, 0)); + ASSERT_FLOAT_EQ(0.0f, ImageTraits::GetFloatPixel(image, 1, 0)); + ASSERT_FLOAT_EQ(0.000539f, ImageTraits::GetFloatPixel(image, 2, 0)); + + ImageProcessing::ShiftScale2(*cloned, 0, 0.000539, false); + ASSERT_FLOAT_EQ(-0.000539f, ImageTraits::GetFloatPixel(*cloned, 0, 0)); + ASSERT_FLOAT_EQ(0.0f, ImageTraits::GetFloatPixel(*cloned, 1, 0)); + ASSERT_FLOAT_EQ(0.000539f, ImageTraits::GetFloatPixel(*cloned, 2, 0)); +} + + +TEST(ImageProcessing, ShiftScale2) +{ + std::vector va; + va.push_back(0); + va.push_back(-10); + va.push_back(5); + + std::vector vb; + vb.push_back(0); + vb.push_back(-42); + vb.push_back(42); + + Image source(PixelFormat_Float32, 1, 1, false); + ImageTraits::SetFloatPixel(source, 10, 0, 0); + + for (std::vector::const_iterator a = va.begin(); a != va.end(); ++a) + { + for (std::vector::const_iterator b = vb.begin(); b != vb.end(); ++b) + { + Image target(PixelFormat_Float32, 1, 1, false); + + ImageProcessing::Copy(target, source); + ImageProcessing::ShiftScale2(target, *b, *a, false); + ASSERT_FLOAT_EQ((*a) * 10.0f + (*b), + ImageTraits::GetFloatPixel(target, 0, 0)); + + ImageProcessing::Copy(target, source); + ImageProcessing::ShiftScale(target, *b, *a, false); + ASSERT_FLOAT_EQ((*a) * (10.0f + (*b)), + ImageTraits::GetFloatPixel(target, 0, 0)); + } + } +} + + +namespace +{ + class PolygonSegments : public ImageProcessing::IPolygonFiller + { + private: + std::vector y_, x1_, x2_; + + public: + virtual void Fill(int y, + int x1, + int x2) ORTHANC_OVERRIDE + { + assert(x1 <= x2); + y_.push_back(y); + x1_.push_back(x1); + x2_.push_back(x2); + } + + size_t GetSize() const + { + return y_.size(); + } + + int GetY(size_t i) const + { + return y_[i]; + } + + int GetX1(size_t i) const + { + return x1_[i]; + } + + int GetX2(size_t i) const + { + return x2_[i]; + } + }; +} + + +static bool LookupSegment(unsigned int& x1, + unsigned int& x2, + const Orthanc::ImageAccessor& image, + unsigned int y) +{ + const uint8_t* p = reinterpret_cast(image.GetConstRow(y)); + + bool allZeros = true; + for (unsigned int i = 0; i < image.GetWidth(); i++) + { + if (p[i] == 255) + { + allZeros = false; + break; + } + else if (p[i] > 0) + { + return false; // error + } + } + + if (allZeros) + { + return false; + } + + x1 = 0; + while (p[x1] == 0) + { + x1++; + } + + x2 = image.GetWidth() - 1; + while (p[x2] == 0) + { + x2--; + } + + for (unsigned int i = x1; i <= x2; i++) + { + if (p[i] != 255) + { + return false; + } + } + + return true; +} + + +TEST(ImageProcessing, FillPolygon) +{ + { + // Empty + std::vector polygon; + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(0u, segments.GetSize()); + } + + { + // One point + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(288, 208)); + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(0u, segments.GetSize()); + } + + { + // One horizontal segment + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(10, 100)); + polygon.push_back(ImageProcessing::ImagePoint(50, 100)); + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(1u, segments.GetSize()); + ASSERT_EQ(100, segments.GetY(0)); + ASSERT_EQ(10, segments.GetX1(0)); + ASSERT_EQ(50, segments.GetX2(0)); + } + + { + // Set of horizontal segments + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(10, 100)); + polygon.push_back(ImageProcessing::ImagePoint(20, 100)); + polygon.push_back(ImageProcessing::ImagePoint(30, 100)); + polygon.push_back(ImageProcessing::ImagePoint(50, 100)); + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(1u, segments.GetSize()); + ASSERT_EQ(100, segments.GetY(0)); + ASSERT_EQ(10, segments.GetX1(0)); + ASSERT_EQ(50, segments.GetX2(0)); + } + + { + // Set of vertical segments + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(10, 100)); + polygon.push_back(ImageProcessing::ImagePoint(10, 102)); + polygon.push_back(ImageProcessing::ImagePoint(10, 105)); + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(6u, segments.GetSize()); + for (size_t i = 0; i < segments.GetSize(); i++) + { + ASSERT_EQ(100 + static_cast(i), segments.GetY(i)); + ASSERT_EQ(10, segments.GetX1(i)); + ASSERT_EQ(10, segments.GetX2(i)); + } + } + + { + // One diagonal segment + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(10, 100)); + polygon.push_back(ImageProcessing::ImagePoint(11, 101)); + polygon.push_back(ImageProcessing::ImagePoint(13, 103)); + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(4u, segments.GetSize()); + ASSERT_EQ(100, segments.GetY(0)); + ASSERT_EQ(10, segments.GetX1(0)); + ASSERT_EQ(10, segments.GetX2(0)); + ASSERT_EQ(101, segments.GetY(1)); + ASSERT_EQ(11, segments.GetX1(1)); + ASSERT_EQ(11, segments.GetX2(1)); + ASSERT_EQ(102, segments.GetY(2)); + ASSERT_EQ(12, segments.GetX1(2)); + ASSERT_EQ(12, segments.GetX2(2)); + ASSERT_EQ(103, segments.GetY(3)); + ASSERT_EQ(13, segments.GetX1(3)); + ASSERT_EQ(13, segments.GetX2(3)); + } + + { + // "M" shape + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(5, 5)); + polygon.push_back(ImageProcessing::ImagePoint(7, 7)); + polygon.push_back(ImageProcessing::ImagePoint(9, 5)); + polygon.push_back(ImageProcessing::ImagePoint(9, 8)); + polygon.push_back(ImageProcessing::ImagePoint(5, 8)); + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(6u, segments.GetSize()); + ASSERT_EQ(5, segments.GetY(0)); ASSERT_EQ(5, segments.GetX1(0)); ASSERT_EQ(5, segments.GetX2(0)); + ASSERT_EQ(5, segments.GetY(1)); ASSERT_EQ(9, segments.GetX1(1)); ASSERT_EQ(9, segments.GetX2(1)); + ASSERT_EQ(6, segments.GetY(2)); ASSERT_EQ(5, segments.GetX1(2)); ASSERT_EQ(6, segments.GetX2(2)); + ASSERT_EQ(6, segments.GetY(3)); ASSERT_EQ(8, segments.GetX1(3)); ASSERT_EQ(9, segments.GetX2(3)); + ASSERT_EQ(7, segments.GetY(4)); ASSERT_EQ(5, segments.GetX1(4)); ASSERT_EQ(9, segments.GetX2(4)); + ASSERT_EQ(8, segments.GetY(5)); ASSERT_EQ(5, segments.GetX1(5)); ASSERT_EQ(9, segments.GetX2(5)); + } + + { + // Rectangle + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(10, 50)); + polygon.push_back(ImageProcessing::ImagePoint(200, 50)); + polygon.push_back(ImageProcessing::ImagePoint(200, 100)); + polygon.push_back(ImageProcessing::ImagePoint(10, 100)); + + PolygonSegments segments; + ImageProcessing::FillPolygon(segments, polygon); + ASSERT_EQ(51u, segments.GetSize()); + + for (size_t i = 0; i < segments.GetSize(); i++) + { + ASSERT_EQ(50 + static_cast(i), segments.GetY(i)); + ASSERT_EQ(10, segments.GetX1(i)); + ASSERT_EQ(200, segments.GetX2(i)); + } + } + + { + // Shape that goes outside of the image on the 4 borders + std::vector polygon; + polygon.push_back(ImageProcessing::ImagePoint(5, -5)); + polygon.push_back(ImageProcessing::ImagePoint(40, 15)); + polygon.push_back(ImageProcessing::ImagePoint(20, 32)); + polygon.push_back(ImageProcessing::ImagePoint(-5, 27)); + + Image image(PixelFormat_Grayscale8, 30, 30, false); + ImageProcessing::Set(image, 0); + ImageProcessing::FillPolygon(image, polygon, 255); + + unsigned int x1, x2; + ASSERT_TRUE(LookupSegment(x1, x2, image, 0)); ASSERT_EQ(3u, x1); ASSERT_EQ(14u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 1)); ASSERT_EQ(3u, x1); ASSERT_EQ(16u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 2)); ASSERT_EQ(2u, x1); ASSERT_EQ(18u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 3)); ASSERT_EQ(2u, x1); ASSERT_EQ(19u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 4)); ASSERT_EQ(2u, x1); ASSERT_EQ(21u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 5)); ASSERT_EQ(1u, x1); ASSERT_EQ(23u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 6)); ASSERT_EQ(1u, x1); ASSERT_EQ(25u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 7)); ASSERT_EQ(1u, x1); ASSERT_EQ(26u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 8)); ASSERT_EQ(0u, x1); ASSERT_EQ(28u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 9)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 10)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 11)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 12)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 13)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 14)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 15)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 16)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 17)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 18)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 19)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 20)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 21)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 22)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 23)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 24)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 25)); ASSERT_EQ(0u, x1); ASSERT_EQ(29u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 26)); ASSERT_EQ(0u, x1); ASSERT_EQ(28u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 27)); ASSERT_EQ(0u, x1); ASSERT_EQ(26u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 28)); ASSERT_EQ(0u, x1); ASSERT_EQ(25u, x2); + ASSERT_TRUE(LookupSegment(x1, x2, image, 29)); ASSERT_EQ(5u, x1); ASSERT_EQ(24u, x2); + } +} diff --git a/OrthancFramework/UnitTestsSources/ImageTests.cpp b/OrthancFramework/UnitTestsSources/ImageTests.cpp new file mode 100644 index 0000000..f794a2f --- /dev/null +++ b/OrthancFramework/UnitTestsSources/ImageTests.cpp @@ -0,0 +1,625 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Images/Font.h" +#include "../Sources/Images/Image.h" +#include "../Sources/Images/ImageProcessing.h" +#include "../Sources/Images/JpegReader.h" +#include "../Sources/Images/JpegWriter.h" +#include "../Sources/Images/PamReader.h" +#include "../Sources/Images/PamWriter.h" +#include "../Sources/Images/PngReader.h" +#include "../Sources/Images/PngWriter.h" +#include "../Sources/OrthancException.h" +#include "../Sources/Toolbox.h" + +#if ORTHANC_SANDBOXED != 1 +# include "../Sources/SystemToolbox.h" +# include "../Sources/TemporaryFile.h" +#endif + +#include + + +TEST(PngWriter, ColorPattern) +{ + Orthanc::PngWriter w; + unsigned int width = 17; + unsigned int height = 61; + unsigned int pitch = width * 3; + + std::vector image(height * pitch); + for (unsigned int y = 0; y < height; y++) + { + uint8_t *p = &image[0] + y * pitch; + for (unsigned int x = 0; x < width; x++, p += 3) + { + p[0] = (y % 3 == 0) ? 255 : 0; + p[1] = (y % 3 == 1) ? 255 : 0; + p[2] = (y % 3 == 2) ? 255 : 0; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_RGB24, width, height, pitch, &image[0]); + + std::string f; + +#if ORTHANC_SANDBOXED == 1 + Orthanc::IImageWriter::WriteToMemory(w, f, accessor); +#else + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/ColorPattern.png", accessor); + Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png"); +#endif + + std::string md5; + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5); +} + +TEST(PngWriter, Color16Pattern) +{ + Orthanc::PngWriter w; + unsigned int width = 17; + unsigned int height = 61; + unsigned int pitch = width * 8; + + std::vector image(height * pitch); + for (unsigned int y = 0; y < height; y++) + { + uint8_t *p = &image[0] + y * pitch; + for (unsigned int x = 0; x < width; x++, p += 8) + { + switch (Orthanc::Toolbox::DetectEndianness()) + { + case Orthanc::Endianness_Little: + p[0] = (y % 8 == 0) ? 255 : 0; + p[1] = (y % 8 == 1) ? 255 : 0; + p[2] = (y % 8 == 2) ? 255 : 0; + p[3] = (y % 8 == 3) ? 255 : 0; + p[4] = (y % 8 == 4) ? 255 : 0; + p[5] = (y % 8 == 5) ? 255 : 0; + p[6] = (y % 8 == 6) ? 255 : 0; + p[7] = (y % 8 == 7) ? 255 : 0; + break; + + case Orthanc::Endianness_Big: + p[0] = (y % 8 == 1) ? 255 : 0; + p[1] = (y % 8 == 0) ? 255 : 0; + p[2] = (y % 8 == 3) ? 255 : 0; + p[3] = (y % 8 == 2) ? 255 : 0; + p[4] = (y % 8 == 5) ? 255 : 0; + p[5] = (y % 8 == 4) ? 255 : 0; + p[6] = (y % 8 == 7) ? 255 : 0; + p[7] = (y % 8 == 6) ? 255 : 0; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_RGBA64, width, height, pitch, &image[0]); + + std::string f; + +#if ORTHANC_SANDBOXED == 1 + Orthanc::IImageWriter::WriteToMemory(w, f, accessor); +#else + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Color16Pattern.png", accessor); + Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Color16Pattern.png"); +#endif + + std::string md5; + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("1cca552b6bd152b6fdab35c4a9f02c2a", md5); +} + + +TEST(PngWriter, Gray8Pattern) +{ + Orthanc::PngWriter w; + int width = 17; + int height = 256; + int pitch = width; + + std::vector image(height * pitch); + for (int y = 0; y < height; y++) + { + uint8_t *p = &image[0] + y * pitch; + for (int x = 0; x < width; x++, p++) + { + *p = y; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale8, width, height, pitch, &image[0]); + + std::string f; + +#if ORTHANC_SANDBOXED == 1 + Orthanc::IImageWriter::WriteToMemory(w, f, accessor); +#else + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray8Pattern.png", accessor); + Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png"); +#endif + + std::string md5; + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5); +} + +TEST(PngWriter, Gray16Pattern) +{ + Orthanc::PngWriter w; + int width = 256; + int height = 256; + int pitch = width * 2 + 16; + + std::vector image(height * pitch); + + int v = 0; + for (int y = 0; y < height; y++) + { + uint16_t *p = reinterpret_cast(&image[0] + y * pitch); + for (int x = 0; x < width; x++, p++, v++) + { + *p = v; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]); + + std::string f; + +#if ORTHANC_SANDBOXED == 1 + Orthanc::IImageWriter::WriteToMemory(w, f, accessor); +#else + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray16Pattern.png", accessor); + Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png"); +#endif + + std::string md5; + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5); +} + +TEST(PngWriter, EndToEnd) +{ + Orthanc::PngWriter w; + unsigned int width = 256; + unsigned int height = 256; + unsigned int pitch = width * 2 + 16; + + std::vector image(height * pitch); + + int v = 0; + for (unsigned int y = 0; y < height; y++) + { + uint16_t *p = reinterpret_cast(&image[0] + y * pitch); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + *p = v; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]); + + std::string s; + Orthanc::IImageWriter::WriteToMemory(w, s, accessor); + + { + Orthanc::PngReader r; + r.ReadFromMemory(s); + + ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r.GetWidth(), width); + ASSERT_EQ(r.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t *p = reinterpret_cast((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch()); + ASSERT_EQ(p, r.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(*p, v); + } + } + } + +#if ORTHANC_SANDBOXED != 1 + { + Orthanc::TemporaryFile tmp; + tmp.Write(s); + + Orthanc::PngReader r2; + r2.ReadFromFile(tmp.GetPath()); + + ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r2.GetWidth(), width); + ASSERT_EQ(r2.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t *p = reinterpret_cast((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch()); + ASSERT_EQ(p, r2.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(*p, v); + } + } + } +#endif +} + + + + +TEST(JpegWriter, Basic) +{ + std::string s; + + { + Orthanc::Image img(Orthanc::PixelFormat_Grayscale8, 16, 16, false); + for (unsigned int y = 0, value = 0; y < img.GetHeight(); y++) + { + uint8_t* p = reinterpret_cast(img.GetRow(y)); + for (unsigned int x = 0; x < img.GetWidth(); x++, p++) + { + *p = value++; + } + } + + Orthanc::JpegWriter w; + Orthanc::IImageWriter::WriteToMemory(w, s, img); + +#if ORTHANC_SANDBOXED != 1 + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/hello.jpg", img); + Orthanc::SystemToolbox::WriteFile(s, "UnitTestsResults/hello2.jpg"); + + std::string t; + Orthanc::SystemToolbox::ReadFile(t, "UnitTestsResults/hello.jpg"); + ASSERT_EQ(s.size(), t.size()); + ASSERT_EQ(0, memcmp(s.c_str(), t.c_str(), s.size())); +#endif + } + + { + Orthanc::JpegReader r1; + r1.ReadFromMemory(s); + ASSERT_EQ(16u, r1.GetWidth()); + ASSERT_EQ(16u, r1.GetHeight()); + +#if ORTHANC_SANDBOXED != 1 + Orthanc::JpegReader r2; + r2.ReadFromFile("UnitTestsResults/hello.jpg"); + ASSERT_EQ(16u, r2.GetWidth()); + ASSERT_EQ(16u, r2.GetHeight()); +#endif + + unsigned int value = 0; + for (unsigned int y = 0; y < r1.GetHeight(); y++) + { + const uint8_t* p1 = reinterpret_cast(r1.GetConstRow(y)); +#if ORTHANC_SANDBOXED != 1 + const uint8_t* p2 = reinterpret_cast(r2.GetConstRow(y)); +#endif + for (unsigned int x = 0; x < r1.GetWidth(); x++, value++) + { + ASSERT_TRUE(*p1 == value || + *p1 == value - 1 || + *p1 == value + 1); // Be tolerant to differences of +-1 + +#if ORTHANC_SANDBOXED != 1 + ASSERT_EQ(*p1, *p2); + p2++; +#endif + + p1++; + } + } + } +} + + +TEST(PamWriter, ColorPattern) +{ + Orthanc::PamWriter w; + unsigned int width = 17; + unsigned int height = 61; + unsigned int pitch = width * 3; + + std::vector image(height * pitch); + for (unsigned int y = 0; y < height; y++) + { + uint8_t *p = &image[0] + y * pitch; + for (unsigned int x = 0; x < width; x++, p += 3) + { + p[0] = (y % 3 == 0) ? 255 : 0; + p[1] = (y % 3 == 1) ? 255 : 0; + p[2] = (y % 3 == 2) ? 255 : 0; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_RGB24, width, height, pitch, &image[0]); + + std::string f; + +#if ORTHANC_SANDBOXED == 1 + Orthanc::IImageWriter::WriteToMemory(w, f, accessor); +#else + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/ColorPattern.pam", accessor); + Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.pam"); +#endif + + std::string md5; + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("81a3441754e88969ebbe53e69891e841", md5); +} + +TEST(PamWriter, Gray8Pattern) +{ + Orthanc::PamWriter w; + int width = 17; + int height = 256; + int pitch = width; + + std::vector image(height * pitch); + for (int y = 0; y < height; y++) + { + uint8_t *p = &image[0] + y * pitch; + for (int x = 0; x < width; x++, p++) + { + *p = y; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale8, width, height, pitch, &image[0]); + + std::string f; + +#if ORTHANC_SANDBOXED == 1 + Orthanc::IImageWriter::WriteToMemory(w, f, accessor); +#else + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray8Pattern.pam", accessor); + Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.pam"); +#endif + + std::string md5; + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("7873c408d26a9d11dd1c1de5e69cc0a3", md5); +} + +TEST(PamWriter, Gray16Pattern) +{ + Orthanc::PamWriter w; + int width = 256; + int height = 256; + int pitch = width * 2 + 16; + + std::vector image(height * pitch); + + int v = 0; + for (int y = 0; y < height; y++) + { + uint16_t *p = reinterpret_cast(&image[0] + y * pitch); + for (int x = 0; x < width; x++, p++, v++) + { + *p = v; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]); + + std::string f; + +#if ORTHANC_SANDBOXED == 1 + Orthanc::IImageWriter::WriteToMemory(w, f, accessor); +#else + Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray16Pattern.pam", accessor); + Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.pam"); +#endif + + std::string md5; + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("b268772bf28f3b2b8520ff21c5e3dcb6", md5); +} + +TEST(PamWriter, EndToEnd) +{ + Orthanc::PamWriter w; + unsigned int width = 256; + unsigned int height = 256; + unsigned int pitch = width * 2 + 16; + + std::vector image(height * pitch); + + int v = 0; + for (unsigned int y = 0; y < height; y++) + { + uint16_t *p = reinterpret_cast(&image[0] + y * pitch); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + *p = v; + } + } + + Orthanc::ImageAccessor accessor; + accessor.AssignReadOnly(Orthanc::PixelFormat_Grayscale16, width, height, pitch, &image[0]); + + std::string s; + Orthanc::IImageWriter::WriteToMemory(w, s, accessor); + + { + Orthanc::PamReader r(true); + r.ReadFromMemory(s); + + ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r.GetWidth(), width); + ASSERT_EQ(r.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t *p = reinterpret_cast + ((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch()); + ASSERT_EQ(p, r.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(v, *p); + } + } + } + + { + // true means "enforce alignment by using a temporary buffer" + Orthanc::PamReader r(true); + r.ReadFromMemory(s); + + ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r.GetWidth(), width); + ASSERT_EQ(r.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t* p = reinterpret_cast + ((const uint8_t*)r.GetConstBuffer() + y * r.GetPitch()); + ASSERT_EQ(p, r.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(v, *p); + } + } + } + +#if ORTHANC_SANDBOXED != 1 + { + Orthanc::TemporaryFile tmp; + tmp.Write(s); + + Orthanc::PamReader r2(true); + r2.ReadFromFile(tmp.GetPath()); + + ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r2.GetWidth(), width); + ASSERT_EQ(r2.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t *p = reinterpret_cast + ((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch()); + ASSERT_EQ(p, r2.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(*p, v); + } + } + } +#endif + +#if ORTHANC_SANDBOXED != 1 + { + Orthanc::TemporaryFile tmp; + tmp.Write(s); + + // true means "enforce alignment by using a temporary buffer" + Orthanc::PamReader r2(true); + r2.ReadFromFile(tmp.GetPath()); + + ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r2.GetWidth(), width); + ASSERT_EQ(r2.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t* p = reinterpret_cast + ((const uint8_t*)r2.GetConstBuffer() + y * r2.GetPitch()); + ASSERT_EQ(p, r2.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(*p, v); + } + } + } +#endif +} + + +TEST(PngWriter, Gray16Then8) +{ + Orthanc::Image image16(Orthanc::PixelFormat_Grayscale16, 32, 32, false); + Orthanc::Image image8(Orthanc::PixelFormat_Grayscale8, 32, 32, false); + + memset(image16.GetBuffer(), 0, image16.GetHeight() * image16.GetPitch()); + memset(image8.GetBuffer(), 0, image8.GetHeight() * image8.GetPitch()); + + { + Orthanc::PamWriter w; + std::string s; + Orthanc::IImageWriter::WriteToMemory(w, s, image16); + Orthanc::IImageWriter::WriteToMemory(w, s, image8); // No problem here + } + + { + Orthanc::PamWriter w; + std::string s; + Orthanc::IImageWriter::WriteToMemory(w, s, image8); + Orthanc::IImageWriter::WriteToMemory(w, s, image16); // No problem here + } + + { + Orthanc::PngWriter w; + std::string s; + Orthanc::IImageWriter::WriteToMemory(w, s, image8); + Orthanc::IImageWriter::WriteToMemory(w, s, image16); // No problem here + } + + { + // The following call leads to "Invalid read of size 1" in Orthanc <= 1.9.2 + Orthanc::PngWriter w; + std::string s; + Orthanc::IImageWriter::WriteToMemory(w, s, image16); + Orthanc::IImageWriter::WriteToMemory(w, s, image8); // Problem here + } +} diff --git a/OrthancFramework/UnitTestsSources/JobsTests.cpp b/OrthancFramework/UnitTestsSources/JobsTests.cpp new file mode 100644 index 0000000..eb330d7 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp @@ -0,0 +1,1671 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../../OrthancFramework/Sources/Compatibility.h" +#include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h" +#include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h" +#include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h" +#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" +#include "../../OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h" +#include "../../OrthancFramework/Sources/JobsEngine/JobsEngine.h" +#include "../../OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h" +#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" +#include "../../OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h" +#include "../../OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h" +#include "../../OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h" +#include "../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" +#include "../../OrthancFramework/Sources/Logging.h" +#include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h" +#include "../../OrthancFramework/Sources/OrthancException.h" +#include "../../OrthancFramework/Sources/SerializationToolbox.h" + + +using namespace Orthanc; + +namespace +{ + class DummyJob : public IJob + { + private: + bool fails_; + unsigned int count_; + unsigned int steps_; + + public: + DummyJob() : + fails_(false), + count_(0), + steps_(4) + { + } + + explicit DummyJob(bool fails) : + fails_(fails), + count_(0), + steps_(4) + { + } + + virtual void Start() ORTHANC_OVERRIDE + { + } + + virtual void Reset() ORTHANC_OVERRIDE + { + } + + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE + { + if (fails_) + { + return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL); + } + else if (count_ == steps_ - 1) + { + return JobStepResult::Success(); + } + else + { + count_++; + return JobStepResult::Continue(); + } + } + + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE + { + } + + virtual float GetProgress() const ORTHANC_OVERRIDE + { + return static_cast(count_) / static_cast(steps_ - 1); + } + + virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE + { + type = "DummyJob"; + } + + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE + { + value = Json::objectValue; + value["Type"] = "DummyJob"; + return true; + } + + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE + { + value["hello"] = "world"; + } + + virtual bool GetOutput(std::string& output, + MimeType& mime, + std::string& filename, + const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + }; + + + class DummyInstancesJob : public SetOfInstancesJob + { + private: + bool trailingStepDone_; + + protected: + virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE + { + return (instance != "nope"); + } + + virtual bool HandleTrailingStep() ORTHANC_OVERRIDE + { + if (HasTrailingStep()) + { + if (trailingStepDone_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + trailingStepDone_ = true; + return true; + } + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + public: + DummyInstancesJob() : + trailingStepDone_(false) + { + } + + explicit DummyInstancesJob(const Json::Value& value) : + SetOfInstancesJob(value) + { + if (HasTrailingStep()) + { + trailingStepDone_ = (GetPosition() == GetCommandsCount()); + } + else + { + trailingStepDone_ = false; + } + } + + bool IsTrailingStepDone() const + { + return trailingStepDone_; + } + + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE + { + } + + virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE + { + s = "DummyInstancesJob"; + } + }; + + + class DummyUnserializer : public GenericJobUnserializer + { + public: + virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE + { + if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob") + { + return new DummyInstancesJob(value); + } + else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob") + { + return new DummyJob; + } + else + { + return GenericJobUnserializer::UnserializeJob(value); + } + } + }; + + + class DynamicInteger : public IDynamicObject + { + private: + int value_; + std::set& target_; + + public: + DynamicInteger(int value, std::set& target) : + value_(value), target_(target) + { + } + + int GetValue() const + { + return value_; + } + }; +} + + +TEST(MultiThreading, SharedMessageQueueBasic) +{ + std::set s; + + SharedMessageQueue q; + ASSERT_TRUE(q.WaitEmpty(0)); + q.Enqueue(new DynamicInteger(10, s)); + ASSERT_FALSE(q.WaitEmpty(1)); + q.Enqueue(new DynamicInteger(20, s)); + q.Enqueue(new DynamicInteger(30, s)); + q.Enqueue(new DynamicInteger(40, s)); + + std::unique_ptr i; + i.reset(dynamic_cast(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue()); + i.reset(dynamic_cast(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue()); + i.reset(dynamic_cast(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue()); + ASSERT_FALSE(q.WaitEmpty(1)); + i.reset(dynamic_cast(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue()); + ASSERT_TRUE(q.WaitEmpty(0)); + ASSERT_EQ(NULL, q.Dequeue(1)); +} + + +TEST(MultiThreading, SharedMessageQueueClean) +{ + std::set s; + + try + { + SharedMessageQueue q; + q.Enqueue(new DynamicInteger(10, s)); + q.Enqueue(new DynamicInteger(20, s)); + throw OrthancException(ErrorCode_InternalError); + } + catch (OrthancException&) + { + } +} + + + + +static bool CheckState(JobsRegistry& registry, + const std::string& id, + JobState state) +{ + JobState s; + if (registry.GetState(s, id)) + { + return state == s; + } + else + { + return false; + } +} + + +static bool CheckErrorCode(JobsRegistry& registry, + const std::string& id, + ErrorCode code) +{ + JobInfo s; + if (registry.GetJobInfo(s, id)) + { + return code == s.GetStatus().GetErrorCode(); + } + else + { + return false; + } +} + + +TEST(JobsRegistry, Priority) +{ + JobsRegistry registry(10); + + std::string i1, i2, i3, i4; + registry.Submit(i1, new DummyJob(), 10); + registry.Submit(i2, new DummyJob(), 30); + registry.Submit(i3, new DummyJob(), 20); + registry.Submit(i4, new DummyJob(), 5); + + registry.SetMaxCompletedJobs(2); + + std::set id; + registry.ListJobs(id); + + ASSERT_EQ(4u, id.size()); + ASSERT_TRUE(id.find(i1) != id.end()); + ASSERT_TRUE(id.find(i2) != id.end()); + ASSERT_TRUE(id.find(i3) != id.end()); + ASSERT_TRUE(id.find(i4) != id.end()); + + ASSERT_TRUE(CheckState(registry, i2, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(30, job.GetPriority()); + ASSERT_EQ(i2, job.GetId()); + + ASSERT_TRUE(CheckState(registry, i2, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, i2, JobState_Failure)); + ASSERT_TRUE(CheckState(registry, i3, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(20, job.GetPriority()); + ASSERT_EQ(i3, job.GetId()); + + job.MarkSuccess(); + + ASSERT_TRUE(CheckState(registry, i3, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, i3, JobState_Success)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(10, job.GetPriority()); + ASSERT_EQ(i1, job.GetId()); + } + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(5, job.GetPriority()); + ASSERT_EQ(i4, job.GetId()); + } + + { + JobsRegistry::RunningJob job(registry, 1); + ASSERT_FALSE(job.IsValid()); + } + + JobState s; + ASSERT_TRUE(registry.GetState(s, i1)); + ASSERT_FALSE(registry.GetState(s, i2)); // Removed because oldest + ASSERT_FALSE(registry.GetState(s, i3)); // Removed because second oldest + ASSERT_TRUE(registry.GetState(s, i4)); + + registry.SetMaxCompletedJobs(1); // (*) + ASSERT_FALSE(registry.GetState(s, i1)); // Just discarded by (*) + ASSERT_TRUE(registry.GetState(s, i4)); +} + + +TEST(JobsRegistry, Simultaneous) +{ + JobsRegistry registry(10); + + std::string i1, i2; + registry.Submit(i1, new DummyJob(), 20); + registry.Submit(i2, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, i1, JobState_Pending)); + ASSERT_TRUE(CheckState(registry, i2, JobState_Pending)); + + { + JobsRegistry::RunningJob job1(registry, 0); + JobsRegistry::RunningJob job2(registry, 0); + + ASSERT_TRUE(job1.IsValid()); + ASSERT_TRUE(job2.IsValid()); + + job1.MarkFailure(); + job2.MarkSuccess(); + + ASSERT_TRUE(CheckState(registry, i1, JobState_Running)); + ASSERT_TRUE(CheckState(registry, i2, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, i1, JobState_Failure)); + ASSERT_TRUE(CheckState(registry, i2, JobState_Success)); +} + + +TEST(JobsRegistry, Resubmit) +{ + JobsRegistry registry(10); + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + job.MarkFailure(); + + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(id, job.GetId()); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, Retry) +{ + JobsRegistry registry(10); + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + job.MarkRetry(0); + + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + + registry.ScheduleRetries(); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + job.MarkSuccess(); + + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, PausePending) +{ + JobsRegistry registry(10); + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + registry.Pause(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Pause(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resume(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); +} + + +TEST(JobsRegistry, PauseRunning) +{ + JobsRegistry registry(10); + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + registry.Resubmit(id); + job.MarkPause(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resubmit(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resume(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, PauseRetry) +{ + JobsRegistry registry(10); + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + job.MarkRetry(0); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + + registry.Pause(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + + registry.Resume(id); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); +} + + +TEST(JobsRegistry, Cancel) +{ + JobsRegistry registry(10); + + std::string id; + registry.Submit(id, new DummyJob(), 10); + + ASSERT_FALSE(registry.Cancel("nope")); + + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Resubmit(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + job.MarkSuccess(); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Success)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + registry.Submit(id, new DummyJob(), 10); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(id, job.GetId()); + + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + + job.MarkCanceled(); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Resubmit(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Pause(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Paused)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + ASSERT_TRUE(registry.Resubmit(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Pending)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); + + { + JobsRegistry::RunningJob job(registry, 0); + ASSERT_TRUE(job.IsValid()); + ASSERT_EQ(id, job.GetId()); + + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + ASSERT_TRUE(CheckState(registry, id, JobState_Running)); + + job.MarkRetry(500); + } + + ASSERT_TRUE(CheckState(registry, id, JobState_Retry)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success)); + + ASSERT_TRUE(registry.Cancel(id)); + ASSERT_TRUE(CheckState(registry, id, JobState_Failure)); + ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob)); +} + + + +TEST(JobsEngine, SubmitAndWait) +{ + JobsEngine engine(10); + engine.SetThreadSleep(10); + engine.SetWorkersCount(3); + engine.Start(); + + Json::Value content = Json::nullValue; + engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10); + ASSERT_EQ(Json::objectValue, content.type()); + ASSERT_EQ("world", content["hello"].asString()); + + content = Json::nullValue; + ASSERT_THROW(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10), OrthancException); + ASSERT_EQ(Json::nullValue, content.type()); + + engine.Stop(); +} + + +TEST(JobsEngine, DISABLED_SequenceOfOperationsJob) +{ + JobsEngine engine(10); + engine.SetThreadSleep(10); + engine.SetWorkersCount(3); + engine.Start(); + + std::string id; + SequenceOfOperationsJob* job = NULL; + + { + std::unique_ptr a(new SequenceOfOperationsJob); + job = a.get(); + engine.GetRegistry().Submit(id, a.release(), 0); + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(500)); + + { + SequenceOfOperationsJob::Lock lock(*job); + size_t i = lock.AddOperation(new LogJobOperation); + size_t j = lock.AddOperation(new LogJobOperation); + size_t k = lock.AddOperation(new LogJobOperation); + + StringOperationValue a("Hello"); + StringOperationValue b("World"); + lock.AddInput(i, a); + lock.AddInput(i, b); + + lock.Connect(i, j); + lock.Connect(j, k); + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(2000)); + + engine.Stop(); + +} + + +static bool CheckSameJson(const Json::Value& a, + const Json::Value& b) +{ + std::string s = a.toStyledString(); + std::string t = b.toStyledString(); + + if (s == t) + { + return true; + } + else + { + LOG(ERROR) << "Expected serialization: " << s; + LOG(ERROR) << "Actual serialization: " << t; + return false; + } +} + + +static bool CheckIdempotentSerialization(IJobUnserializer& unserializer, + const IJob& job) +{ + Json::Value a = 42; + + if (!job.Serialize(a)) + { + return false; + } + else + { + std::unique_ptr unserialized(unserializer.UnserializeJob(a)); + + Json::Value b = 43; + if (unserialized->Serialize(b)) + { + return (CheckSameJson(a, b)); + } + else + { + return false; + } + } +} + + +static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer, + const SetOfInstancesJob& job) +{ + Json::Value a = 42; + + if (!job.Serialize(a)) + { + return false; + } + else + { + std::unique_ptr unserialized + (dynamic_cast(unserializer.UnserializeJob(a))); + + Json::Value b = 43; + if (unserialized->Serialize(b)) + { + return (CheckSameJson(a, b) && + job.HasTrailingStep() == unserialized->HasTrailingStep() && + job.GetPosition() == unserialized->GetPosition() && + job.GetInstancesCount() == unserialized->GetInstancesCount() && + job.GetCommandsCount() == unserialized->GetCommandsCount()); + } + else + { + return false; + } + } +} + + +static bool CheckIdempotentSerialization(IJobUnserializer& unserializer, + const IJobOperationValue& value) +{ + Json::Value a = 42; + value.Serialize(a); + + std::unique_ptr unserialized(unserializer.UnserializeValue(a)); + + Json::Value b = 43; + unserialized->Serialize(b); + + return CheckSameJson(a, b); +} + + +TEST(JobsSerialization, BadFileFormat) +{ + GenericJobUnserializer unserializer; + + Json::Value s; + + s = Json::objectValue; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + s = Json::arrayValue; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + s = "hello"; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + s = 42; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); +} + + +TEST(JobsSerialization, JobOperationValues) +{ + Json::Value s; + + { + JobOperationValues values; + values.Append(new NullOperationValue); + values.Append(new StringOperationValue("hello")); + values.Append(new StringOperationValue("world")); + + s = 42; + values.Serialize(s); + } + + { + GenericJobUnserializer unserializer; + std::unique_ptr values(JobOperationValues::Unserialize(unserializer, s)); + ASSERT_EQ(3u, values->GetSize()); + ASSERT_EQ(IJobOperationValue::Type_Null, values->GetValue(0).GetType()); + ASSERT_EQ(IJobOperationValue::Type_String, values->GetValue(1).GetType()); + ASSERT_EQ(IJobOperationValue::Type_String, values->GetValue(2).GetType()); + + ASSERT_EQ("hello", dynamic_cast(values->GetValue(1)).GetContent()); + ASSERT_EQ("world", dynamic_cast(values->GetValue(2)).GetContent()); + } +} + + +TEST(JobsSerialization, GenericValues) +{ + GenericJobUnserializer unserializer; + Json::Value s; + + { + NullOperationValue null; + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, null)); + null.Serialize(s); + } + + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + std::unique_ptr value; + value.reset(unserializer.UnserializeValue(s)); + + ASSERT_EQ(IJobOperationValue::Type_Null, value->GetType()); + + { + StringOperationValue str("Hello"); + + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, str)); + str.Serialize(s); + } + + ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + value.reset(unserializer.UnserializeValue(s)); + + ASSERT_EQ(IJobOperationValue::Type_String, value->GetType()); + ASSERT_EQ("Hello", dynamic_cast(*value).GetContent()); +} + + +TEST(JobsSerialization, GenericJobs) +{ + Json::Value s; + + // This tests SetOfInstancesJob + + { + DummyInstancesJob job; + job.SetDescription("description"); + job.AddInstance("hello"); + job.AddInstance("nope"); + job.AddInstance("world"); + job.SetPermissive(true); + ASSERT_THROW(job.Step("jobId"), OrthancException); // Not started yet + ASSERT_FALSE(job.HasTrailingStep()); + ASSERT_FALSE(job.IsTrailingStepDone()); + job.Start(); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_TRUE(job.Serialize(s)); + } + + { + DummyUnserializer unserializer; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + std::unique_ptr job; + job.reset(unserializer.UnserializeJob(s)); + + const DummyInstancesJob& tmp = dynamic_cast(*job); + ASSERT_FALSE(tmp.IsStarted()); + ASSERT_TRUE(tmp.IsPermissive()); + ASSERT_EQ("description", tmp.GetDescription()); + ASSERT_EQ(3u, tmp.GetInstancesCount()); + ASSERT_EQ(2u, tmp.GetPosition()); + ASSERT_EQ(1u, tmp.GetFailedInstances().size()); + ASSERT_EQ("hello", tmp.GetInstance(0)); + ASSERT_EQ("nope", tmp.GetInstance(1)); + ASSERT_EQ("world", tmp.GetInstance(2)); + ASSERT_TRUE(tmp.IsFailedInstance("nope")); + } + + // SequenceOfOperationsJob + + { + SequenceOfOperationsJob job; + job.SetDescription("hello"); + + { + SequenceOfOperationsJob::Lock lock(job); + size_t a = lock.AddOperation(new LogJobOperation); + size_t b = lock.AddOperation(new LogJobOperation); + lock.Connect(a, b); + + StringOperationValue s1("hello"); + StringOperationValue s2("world"); + lock.AddInput(a, s1); + lock.AddInput(a, s2); + lock.SetTrailingOperationTimeout(300); + } + + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + + { + GenericJobUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job)); + } + + ASSERT_TRUE(job.Serialize(s)); + } + + { + GenericJobUnserializer unserializer; + ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException); + ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException); + + std::unique_ptr job; + job.reset(unserializer.UnserializeJob(s)); + + std::string tmp; + dynamic_cast(*job).GetDescription(tmp); + ASSERT_EQ("hello", tmp); + } +} + + +static bool IsSameTagValue(const ParsedDicomFile& dicom1, + const ParsedDicomFile& dicom2, + DicomTag tag) +{ + std::string a, b; + return (dicom1.GetTagValue(a, tag) && + dicom2.GetTagValue(b, tag) && + (a == b)); +} + + + +TEST(JobsSerialization, DicomModification) +{ + Json::Value s; + + ParsedDicomFile source(true); + source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false, ""); + source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false, ""); + source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false, ""); + + std::unique_ptr modified(source.Clone(true)); + + { + DicomModification modification; + modification.SetLevel(ResourceType_Series); + modification.Clear(DICOM_TAG_STUDY_DESCRIPTION); + modification.Remove(DICOM_TAG_SERIES_DESCRIPTION); + modification.Replace(DICOM_TAG_PATIENT_NAME, "Test 4", true); + + modification.Apply(*modified); + + s = 42; + modification.Serialize(s); + } + + { + DicomModification modification(s); + ASSERT_EQ(ResourceType_Series, modification.GetLevel()); + + std::unique_ptr second(source.Clone(true)); + modification.Apply(*second); + + std::string t; + ASSERT_TRUE(second->GetTagValue(t, DICOM_TAG_STUDY_DESCRIPTION)); + ASSERT_TRUE(t.empty()); + ASSERT_FALSE(second->GetTagValue(t, DICOM_TAG_SERIES_DESCRIPTION)); + ASSERT_TRUE(second->GetTagValue(t, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ("Test 4", t); + + ASSERT_TRUE(IsSameTagValue(source, *modified, DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(IsSameTagValue(source, *second, DICOM_TAG_STUDY_INSTANCE_UID)); + + ASSERT_FALSE(IsSameTagValue(source, *second, DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(IsSameTagValue(*modified, *second, DICOM_TAG_SERIES_INSTANCE_UID)); + } +} + + +TEST(JobsSerialization, DicomModification2) +{ + Json::Value s; + + { + DicomModification modification; + modification.SetupAnonymization(DicomVersion_2017c); + modification.Remove(DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE, 1, DICOM_TAG_SOP_INSTANCE_UID)); + modification.Replace(DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE, 1, DICOM_TAG_SOP_CLASS_UID), "Hello", true); + modification.Keep(DicomPath(DICOM_TAG_REFERENCED_IMAGE_SEQUENCE, 1, DICOM_TAG_PATIENT_NAME)); + + s = 42; + modification.Serialize(s); + } + + { + DicomModification modification(s); + + // Check idempotent serialization + Json::Value ss; + modification.Serialize(ss); + ASSERT_EQ(s.toStyledString(), ss.toStyledString()); + } +} + + +TEST(JobsSerialization, Registry) +{ + Json::Value s; + std::string i1, i2; + + { + JobsRegistry registry(10); + registry.Submit(i1, new DummyJob(), 10); + registry.Submit(i2, new SequenceOfOperationsJob(), 30); + registry.Serialize(s); + } + + { + DummyUnserializer unserializer; + JobsRegistry registry(unserializer, s, 10); + + Json::Value t; + registry.Serialize(t); + ASSERT_TRUE(CheckSameJson(s, t)); + } +} + + +TEST(JobsSerialization, TrailingStep) +{ + { + Json::Value s; + + DummyInstancesJob job; + ASSERT_EQ(0u, job.GetCommandsCount()); + ASSERT_EQ(0u, job.GetInstancesCount()); + + job.Start(); + ASSERT_EQ(0u, job.GetPosition()); + ASSERT_FALSE(job.HasTrailingStep()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); + ASSERT_EQ(1u, job.GetPosition()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_THROW(job.Step("jobId"), OrthancException); + } + + { + Json::Value s; + + DummyInstancesJob job; + job.AddInstance("hello"); + job.AddInstance("world"); + ASSERT_EQ(2u, job.GetCommandsCount()); + ASSERT_EQ(2u, job.GetInstancesCount()); + + job.Start(); + ASSERT_EQ(0u, job.GetPosition()); + ASSERT_FALSE(job.HasTrailingStep()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + ASSERT_EQ(1u, job.GetPosition()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); + ASSERT_EQ(2u, job.GetPosition()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_THROW(job.Step("jobId"), OrthancException); + } + + { + Json::Value s; + + DummyInstancesJob job; + ASSERT_EQ(0u, job.GetInstancesCount()); + ASSERT_EQ(0u, job.GetCommandsCount()); + job.AddTrailingStep(); + ASSERT_EQ(0u, job.GetInstancesCount()); + ASSERT_EQ(1u, job.GetCommandsCount()); + + job.Start(); // This adds the trailing step + ASSERT_EQ(0u, job.GetPosition()); + ASSERT_TRUE(job.HasTrailingStep()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); + ASSERT_EQ(1u, job.GetPosition()); + ASSERT_TRUE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_THROW(job.Step("jobId"), OrthancException); + } + + { + Json::Value s; + + DummyInstancesJob job; + job.AddInstance("hello"); + ASSERT_EQ(1u, job.GetInstancesCount()); + ASSERT_EQ(1u, job.GetCommandsCount()); + job.AddTrailingStep(); + ASSERT_EQ(1u, job.GetInstancesCount()); + ASSERT_EQ(2u, job.GetCommandsCount()); + + job.Start(); + ASSERT_EQ(2u, job.GetCommandsCount()); + ASSERT_EQ(0u, job.GetPosition()); + ASSERT_TRUE(job.HasTrailingStep()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + ASSERT_EQ(1u, job.GetPosition()); + ASSERT_FALSE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); + ASSERT_EQ(2u, job.GetPosition()); + ASSERT_TRUE(job.IsTrailingStepDone()); + + { + DummyUnserializer unserializer; + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + } + + ASSERT_THROW(job.Step("jobId"), OrthancException); + } +} + + +TEST(JobsSerialization, RemoteModalityParameters) +{ + Json::Value s; + + { + RemoteModalityParameters modality; + ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); + modality.Serialize(s, false); + ASSERT_EQ(Json::arrayValue, s.type()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); + ASSERT_FALSE(modality.HasTimeout()); + ASSERT_EQ(0u, modality.GetTimeout()); + } + + { + RemoteModalityParameters modality(s); + ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); + ASSERT_EQ("ORTHANC", modality.GetApplicationEntityTitle()); + ASSERT_EQ("127.0.0.1", modality.GetHost()); + ASSERT_EQ(104u, modality.GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, modality.GetManufacturer()); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_FindWorklist)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); + ASSERT_FALSE(modality.HasLocalAet()); + ASSERT_THROW(modality.GetLocalAet(), OrthancException); + ASSERT_FALSE(modality.HasTimeout()); + ASSERT_EQ(0u, modality.GetTimeout()); + } + + s = Json::nullValue; + + { + RemoteModalityParameters modality; + ASSERT_THROW(modality.SetPortNumber(0), OrthancException); + ASSERT_THROW(modality.SetPortNumber(65535), OrthancException); + modality.SetApplicationEntityTitle("HELLO"); + modality.SetHost("world"); + modality.SetPortNumber(45); + modality.SetManufacturer(ModalityManufacturer_GenericNoWildcardInDates); + ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); + modality.Serialize(s, true); + ASSERT_EQ(Json::objectValue, s.type()); + ASSERT_FALSE(modality.HasLocalAet()); + ASSERT_FALSE(modality.HasTimeout()); + ASSERT_EQ(0u, modality.GetTimeout()); + } + + { + RemoteModalityParameters modality(s); + ASSERT_EQ("HELLO", modality.GetApplicationEntityTitle()); + ASSERT_EQ("world", modality.GetHost()); + ASSERT_EQ(45u, modality.GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_GenericNoWildcardInDates, modality.GetManufacturer()); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_FindWorklist)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); + ASSERT_FALSE(modality.HasLocalAet()); + ASSERT_FALSE(modality.HasTimeout()); + ASSERT_EQ(0u, modality.GetTimeout()); + } + + s["Port"] = "46"; + + { + RemoteModalityParameters modality(s); + ASSERT_EQ(46u, modality.GetPortNumber()); + } + + s["Port"] = -1; ASSERT_THROW(RemoteModalityParameters m(s), OrthancException); + s["Port"] = 65535; ASSERT_THROW(RemoteModalityParameters m(s), OrthancException); + s["Port"] = "nope"; ASSERT_THROW(RemoteModalityParameters m(s), OrthancException); + + std::set operations; + operations.insert(DicomRequestType_Echo); + operations.insert(DicomRequestType_Find); + operations.insert(DicomRequestType_FindWorklist); + operations.insert(DicomRequestType_Get); + operations.insert(DicomRequestType_Move); + operations.insert(DicomRequestType_Store); + operations.insert(DicomRequestType_NAction); + operations.insert(DicomRequestType_NEventReport); + + ASSERT_EQ(8u, operations.size()); + + for (std::set::const_iterator + it = operations.begin(); it != operations.end(); ++it) + { + { + RemoteModalityParameters modality; + modality.SetRequestAllowed(*it, false); + ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); + + modality.Serialize(s, false); + ASSERT_EQ(Json::objectValue, s.type()); + } + + { + RemoteModalityParameters modality(s); + + ASSERT_FALSE(modality.IsRequestAllowed(*it)); + + for (std::set::const_iterator + it2 = operations.begin(); it2 != operations.end(); ++it2) + { + if (*it2 != *it) + { + ASSERT_TRUE(modality.IsRequestAllowed(*it2)); + } + } + } + } + + s = Json::nullValue; + + { + RemoteModalityParameters modality; + modality.SetLocalAet("hello"); + modality.SetTimeout(42); + ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); + modality.Serialize(s, true); + ASSERT_EQ(Json::objectValue, s.type()); + ASSERT_TRUE(modality.HasLocalAet()); + ASSERT_TRUE(modality.HasTimeout()); + ASSERT_EQ(42u, modality.GetTimeout()); + } + + { + RemoteModalityParameters modality(s); + ASSERT_TRUE(modality.HasLocalAet()); + ASSERT_EQ("hello", modality.GetLocalAet()); + ASSERT_TRUE(modality.HasTimeout()); + ASSERT_EQ(42u, modality.GetTimeout()); + } + + { + Json::Value t; + t["AllowStorageCommitment"] = false; + t["AET"] = "AET"; + t["Host"] = "host"; + t["Port"] = "104"; + + RemoteModalityParameters modality(t); + ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); + ASSERT_EQ("AET", modality.GetApplicationEntityTitle()); + ASSERT_EQ("host", modality.GetHost()); + ASSERT_EQ(104u, modality.GetPortNumber()); + ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); + ASSERT_FALSE(modality.HasLocalAet()); + ASSERT_THROW(modality.GetLocalAet(), OrthancException); + ASSERT_FALSE(modality.HasTimeout()); + ASSERT_EQ(0u, modality.GetTimeout()); + } + + { + Json::Value t; + t["AllowNAction"] = false; + t["AllowNEventReport"] = true; + t["AET"] = "AET"; + t["Host"] = "host"; + t["Port"] = "104"; + t["AllowTranscoding"] = false; + t["UseDicomTls"] = true; + t["LocalAet"] = "world"; + t["Timeout"] = 20; + + RemoteModalityParameters modality(t); + ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); + ASSERT_EQ("AET", modality.GetApplicationEntityTitle()); + ASSERT_EQ("host", modality.GetHost()); + ASSERT_EQ(104u, modality.GetPortNumber()); + ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_FALSE(modality.IsTranscodingAllowed()); + ASSERT_TRUE(modality.IsDicomTlsEnabled()); + ASSERT_TRUE(modality.HasLocalAet()); + ASSERT_EQ("world", modality.GetLocalAet()); + ASSERT_TRUE(modality.HasTimeout()); + ASSERT_EQ(20u, modality.GetTimeout()); + } + + { + Json::Value t; + t["AllowNAction"] = true; + t["AllowNEventReport"] = true; + t["AET"] = "AET"; + t["Host"] = "host"; + t["Port"] = "104"; + + RemoteModalityParameters modality(t); + ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); + ASSERT_EQ("AET", modality.GetApplicationEntityTitle()); + ASSERT_EQ("host", modality.GetHost()); + ASSERT_EQ(104u, modality.GetPortNumber()); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); + ASSERT_FALSE(modality.HasLocalAet()); + ASSERT_THROW(modality.GetLocalAet(), OrthancException); + } +} + + + +TEST(JobsSerialization, DicomAssociationParameters) +{ + { + DicomAssociationParameters a; + + Json::Value v = Json::objectValue; + a.SerializeJob(v); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ("ORTHANC", v["LocalAet"].asString()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), v["Timeout"].asUInt()); + ASSERT_TRUE(v.isMember("Remote")); + ASSERT_TRUE(v.isMember("MaximumPduLength")); + + ASSERT_EQ(5u, v.getMemberNames().size()); + + DicomAssociationParameters b; + b.UnserializeJob(v); + ASSERT_EQ("ANY-SCP", b.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("127.0.0.1", b.GetRemoteModality().GetHost()); + ASSERT_EQ(104u, b.GetRemoteModality().GetPortNumber()); + ASSERT_EQ("ORTHANC", b.GetLocalApplicationEntityTitle()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultMaximumPduLength(), b.GetMaximumPduLength()); + ASSERT_FALSE(b.GetRemoteModality().IsDicomTlsEnabled()); + ASSERT_FALSE(b.GetRemoteModality().HasLocalAet()); + ASSERT_THROW(b.GetRemoteModality().GetLocalAet(), OrthancException); + ASSERT_FALSE(b.GetRemoteModality().HasTimeout()); + ASSERT_EQ(0u, b.GetRemoteModality().GetTimeout()); + ASSERT_TRUE(b.IsRemoteCertificateRequired()); + } + + { + RemoteModalityParameters p; + p.SetApplicationEntityTitle("WORLD"); + p.SetPortNumber(4242); + p.SetHost("hello.world.com"); + p.SetDicomTlsEnabled(true); + p.SetTimeout(42); + + DicomAssociationParameters a("HELLO", p); + a.SetOwnCertificatePath("key", "crt"); + a.SetTrustedCertificatesPath("trusted"); + a.SetRemoteCertificateRequired(false); + + ASSERT_THROW(a.SetMaximumPduLength(4095), OrthancException); + ASSERT_THROW(a.SetMaximumPduLength(131073), OrthancException); + a.SetMaximumPduLength(4096); + a.SetMaximumPduLength(131072); + + Json::Value v = Json::objectValue; + a.SerializeJob(v); + + ASSERT_EQ(8u, v.getMemberNames().size()); + + DicomAssociationParameters b = DicomAssociationParameters::UnserializeJob(v); + + ASSERT_EQ("WORLD", b.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("hello.world.com", b.GetRemoteModality().GetHost()); + ASSERT_EQ(4242u, b.GetRemoteModality().GetPortNumber()); + ASSERT_EQ("HELLO", b.GetLocalApplicationEntityTitle()); + ASSERT_TRUE(b.GetRemoteModality().IsDicomTlsEnabled()); + ASSERT_EQ("key", b.GetOwnPrivateKeyPath()); + ASSERT_EQ("crt", b.GetOwnCertificatePath()); + ASSERT_EQ("trusted", b.GetTrustedCertificatesPath()); + ASSERT_EQ(131072u, b.GetMaximumPduLength()); + ASSERT_TRUE(b.GetRemoteModality().HasTimeout()); + ASSERT_EQ(42u, b.GetRemoteModality().GetTimeout()); + ASSERT_FALSE(b.IsRemoteCertificateRequired()); + } +} + + +TEST(SerializationToolbox, Numbers) +{ + { + int32_t i; + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "42")); ASSERT_EQ(42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "-42")); ASSERT_EQ(-42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "-2147483648")); ASSERT_EQ(-2147483648l, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger32(i, "2147483647")); ASSERT_EQ(2147483647l, i); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "-2147483649")); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "2147483648")); + ASSERT_FALSE(SerializationToolbox::ParseInteger32(i, "-2\\-3\\-4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstInteger32(i, "-2\\-3\\-4")); ASSERT_EQ(-2, i); + } + + { + uint32_t i; + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger32(i, "42")); ASSERT_EQ(42u, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "-42")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger32(i, "4294967295")); ASSERT_EQ(4294967295u, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "4294967296")); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger32(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstUnsignedInteger32(i, "2\\3\\4")); ASSERT_EQ(2u, i); + } + + { + int64_t i; + ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "42")); ASSERT_EQ(42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "-42")); ASSERT_EQ(-42, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "-2147483649")); ASSERT_EQ(-2147483649ll, i); + ASSERT_TRUE(SerializationToolbox::ParseInteger64(i, "2147483648")); ASSERT_EQ(2147483648ll, i); + ASSERT_FALSE(SerializationToolbox::ParseInteger64(i, "-2\\-3\\-4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstInteger64(i, "-2\\-3\\-4")); ASSERT_EQ(-2, i); + } + + { + uint64_t i; + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger64(i, "42")); ASSERT_EQ(42u, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "-42")); + ASSERT_TRUE(SerializationToolbox::ParseUnsignedInteger64(i, "4294967296")); ASSERT_EQ(4294967296lu, i); + ASSERT_FALSE(SerializationToolbox::ParseUnsignedInteger64(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstUnsignedInteger64(i, "2\\3\\4")); ASSERT_EQ(2u, i); + } + + { + float i; + ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "42")); ASSERT_FLOAT_EQ(42.0f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-42")); ASSERT_FLOAT_EQ(-42.0f, i); + ASSERT_FALSE(SerializationToolbox::ParseFloat(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstFloat(i, "1.367\\2.367\\3.367")); ASSERT_FLOAT_EQ(1.367f, i); + + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "1.2")); ASSERT_FLOAT_EQ(1.2f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-1.2e+2")); ASSERT_FLOAT_EQ(-120.0f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "-1e-2")); ASSERT_FLOAT_EQ(-0.01f, i); + ASSERT_TRUE(SerializationToolbox::ParseFloat(i, "1.3671875")); ASSERT_FLOAT_EQ(1.3671875f, i); + } + + { + double i; + ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "")); + ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "ee")); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "42")); ASSERT_DOUBLE_EQ(42.0, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-42")); ASSERT_DOUBLE_EQ(-42.0, i); + ASSERT_FALSE(SerializationToolbox::ParseDouble(i, "2\\3\\4")); + ASSERT_TRUE(SerializationToolbox::ParseFirstDouble(i, "1.367\\2.367\\3.367")); ASSERT_DOUBLE_EQ(1.367, i); + + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "1.2")); ASSERT_DOUBLE_EQ(1.2, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-1.2e+2")); ASSERT_DOUBLE_EQ(-120.0, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "-1e-2")); ASSERT_DOUBLE_EQ(-0.01, i); + ASSERT_TRUE(SerializationToolbox::ParseDouble(i, "1.3671875")); ASSERT_DOUBLE_EQ(1.3671875, i); + } +} diff --git a/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp b/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp new file mode 100644 index 0000000..7ecc811 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp @@ -0,0 +1,53 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS) +# error ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS is not defined +#endif + +#include + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + +#include "../Sources/DicomParsing/Internals/DicomImageDecoder.h" +#include "../Sources/DicomParsing/ParsedDicomFile.h" +#include "../Sources/Images/ImageBuffer.h" +#include "../Sources/Images/PngWriter.h" +#include "../Sources/OrthancException.h" + +#include + +using namespace Orthanc; + + + +// TODO Write a test + + +#endif diff --git a/OrthancFramework/UnitTestsSources/LoggingTests.cpp b/OrthancFramework/UnitTestsSources/LoggingTests.cpp new file mode 100644 index 0000000..352ee42 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/LoggingTests.cpp @@ -0,0 +1,409 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Logging.h" +#include "../Sources/OrthancException.h" + +#include +#include + + +static std::stringstream testErrorStream; +void TestError(const char* message) +{ + testErrorStream << message; +} + +static std::stringstream testWarningStream; +void TestWarning(const char* message) +{ + testWarningStream << message; +} + +static std::stringstream testInfoStream; +void TestInfo(const char* message) +{ + testInfoStream << message; +} + +/** + Extracts the log line payload + + "E0423 16:55:43.001194 LoggingTests.cpp:102] Foo bar?\n" + --> + "Foo bar" + + If the log line cannot be matched, the function returns false. +*/ + +#define EOLSTRING "\n" + +static bool GetLogLinePayload(std::string& payload, + const std::string& logLine) +{ + const char* regexStr = "[A-Z][0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6} " + ".{16} [a-zA-Z\\.\\-_]+:[0-9]+\\] (.*)" EOLSTRING "$"; + + boost::regex regexObj(regexStr); + + //std::stringstream regexSStr; + //regexSStr << "E[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6} " + // "[a-zA-Z\\.\\-_]+:[0-9]+\\](.*)\r\n$"; + //std::string regexStr = regexSStr.str(); + boost::regex pattern(regexStr); + boost::cmatch what; + if (regex_match(logLine.c_str(), what, regexObj)) + { + payload = what[1]; + return true; + } + else + { + return false; + } +} + + +namespace +{ + class LoggingMementoScope + { + public: + LoggingMementoScope() + { + } + + ~LoggingMementoScope() + { + Orthanc::Logging::Reset(); + } + }; + + + /** + * std::streambuf subclass used in FunctionCallingStream + **/ + template + class FuncStreamBuf : public std::stringbuf + { + public: + explicit FuncStreamBuf(T func) : func_(func) {} + + virtual int sync() + { + std::string text = this->str(); + const char* buf = text.c_str(); + func_(buf); + this->str(""); + return 0; + } + private: + T func_; + }; +} + + +#if ORTHANC_ENABLE_LOGGING_STDIO == 0 +TEST(FuncStreamBuf, BasicTest) +{ + LoggingMementoScope loggingConfiguration; + + Orthanc::Logging::EnableTraceLevel(true); + + typedef void(*LoggingFunctionFunc)(const char*); + + FuncStreamBuf errorStreamBuf(TestError); + std::ostream errorStream(&errorStreamBuf); + + FuncStreamBuf warningStreamBuf(TestWarning); + std::ostream warningStream(&warningStreamBuf); + + FuncStreamBuf infoStreamBuf(TestInfo); + std::ostream infoStream(&infoStreamBuf); + + Orthanc::Logging::SetErrorWarnInfoLoggingStreams(errorStream, warningStream, infoStream); + + { + const char* text = "E is the set of all sets that do not contain themselves. Does E contain itself?"; + LOG(ERROR) << text; + std::string logLine = testErrorStream.str(); + testErrorStream.str(""); + testErrorStream.clear(); + std::string payload; + bool ok = GetLogLinePayload(payload, logLine); + ASSERT_TRUE(ok); + ASSERT_STREQ(payload.c_str(), text); + } + + // make sure loglines do not accumulate + { + const char* text = "some more nonsensical babblingiciously stupid gibberish"; + LOG(ERROR) << text; + std::string logLine = testErrorStream.str(); + testErrorStream.str(""); + testErrorStream.clear(); + std::string payload; + bool ok = GetLogLinePayload(payload, logLine); + ASSERT_TRUE(ok); + ASSERT_STREQ(payload.c_str(), text); + } + + { + const char* text = "Trougoudou 53535345345353"; + LOG(WARNING) << text; + std::string logLine = testWarningStream.str(); + testWarningStream.str(""); + testWarningStream.clear(); + std::string payload; + bool ok = GetLogLinePayload(payload, logLine); + ASSERT_TRUE(ok); + ASSERT_STREQ(payload.c_str(), text); + } + + { + const char* text = "Prout 111929"; + LOG(INFO) << text; + std::string logLine = testInfoStream.str(); + testInfoStream.str(""); + testInfoStream.clear(); + std::string payload; + bool ok = GetLogLinePayload(payload, logLine); + ASSERT_TRUE(ok); + ASSERT_STREQ(payload.c_str(), text); + } + + Orthanc::Logging::EnableTraceLevel(false); // Back to normal +} +#endif + + + +TEST(Logging, Categories) +{ + using namespace Orthanc::Logging; + + // Unit tests are running in "--verbose" mode (not "--trace") + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_SQLITE)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_SQLITE)); + + // Cannot modify categories for ERROR and WARNING + ASSERT_THROW(SetCategoryEnabled(LogLevel_ERROR, LogCategory_GENERIC, true), + Orthanc::OrthancException); + ASSERT_THROW(SetCategoryEnabled(LogLevel_WARNING, LogCategory_GENERIC, false), + Orthanc::OrthancException); + + + EnableInfoLevel(false); + EnableTraceLevel(false); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_FALSE(IsInfoLevelEnabled()); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_SQLITE)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_SQLITE)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_SQLITE)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_SQLITE)); + + + // Test the "category" setters at INFO level + SetCategoryEnabled(LogLevel_INFO, LogCategory_DICOM, true); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); // At least one category is verbose + + SetCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC, true); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + + SetCategoryEnabled(LogLevel_INFO, LogCategory_DICOM, false); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); // "GENERIC" is still verbose + + SetCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC, false); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_FALSE(IsInfoLevelEnabled()); + + + // Test the "category" setters at TRACE level + SetCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM, true); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_TRUE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + + SetCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC, true); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_TRUE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + + SetCategoryEnabled(LogLevel_INFO, LogCategory_DICOM, false); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_TRUE(IsTraceLevelEnabled()); // "GENERIC" is still at trace level + ASSERT_TRUE(IsInfoLevelEnabled()); + + SetCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC, false); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + + SetCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC, false); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_FALSE(IsInfoLevelEnabled()); + + + + // Test the "macro" setters + EnableInfoLevel(true); + EnableTraceLevel(false); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_SQLITE)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_SQLITE)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_SQLITE)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_SQLITE)); + + EnableInfoLevel(false); + EnableTraceLevel(true); // "--trace" implies "--verbose" + ASSERT_TRUE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_ERROR, LogCategory_SQLITE)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_WARNING, LogCategory_SQLITE)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_SQLITE)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_GENERIC)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_DICOM)); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_SQLITE)); + + + + // Back to normal + EnableInfoLevel(true); + EnableTraceLevel(false); + ASSERT_FALSE(IsTraceLevelEnabled()); + ASSERT_TRUE(IsInfoLevelEnabled()); + ASSERT_TRUE(IsCategoryEnabled(LogLevel_INFO, LogCategory_SQLITE)); + ASSERT_FALSE(IsCategoryEnabled(LogLevel_TRACE, LogCategory_SQLITE)); +} + + +TEST(Logging, Enumerations) +{ + using namespace Orthanc; + + Logging::LogCategory c; + ASSERT_TRUE(Logging::LookupCategory(c, "generic")); ASSERT_EQ(Logging::LogCategory_GENERIC, c); + ASSERT_TRUE(Logging::LookupCategory(c, "plugins")); ASSERT_EQ(Logging::LogCategory_PLUGINS, c); + ASSERT_TRUE(Logging::LookupCategory(c, "http")); ASSERT_EQ(Logging::LogCategory_HTTP, c); + ASSERT_TRUE(Logging::LookupCategory(c, "sqlite")); ASSERT_EQ(Logging::LogCategory_SQLITE, c); + ASSERT_TRUE(Logging::LookupCategory(c, "dicom")); ASSERT_EQ(Logging::LogCategory_DICOM, c); + ASSERT_TRUE(Logging::LookupCategory(c, "jobs")); ASSERT_EQ(Logging::LogCategory_JOBS, c); + ASSERT_TRUE(Logging::LookupCategory(c, "lua")); ASSERT_EQ(Logging::LogCategory_LUA, c); + ASSERT_FALSE(Logging::LookupCategory(c, "nope")); + + ASSERT_EQ(7u, Logging::GetCategoriesCount()); + + std::set s; + for (size_t i = 0; i < Logging::GetCategoriesCount(); i++) + { + ASSERT_TRUE(Logging::LookupCategory(c, Logging::GetCategoryName(i))); + s.insert(Logging::GetCategoryName(i)); + } + + ASSERT_EQ(7u, s.size()); + ASSERT_TRUE(s.find("generic") != s.end()); + ASSERT_TRUE(s.find("plugins") != s.end()); + ASSERT_TRUE(s.find("http") != s.end()); + ASSERT_TRUE(s.find("sqlite") != s.end()); + ASSERT_TRUE(s.find("dicom") != s.end()); + ASSERT_TRUE(s.find("lua") != s.end()); + ASSERT_TRUE(s.find("jobs") != s.end()); + + ASSERT_THROW(Logging::GetCategoryName(Logging::GetCategoriesCount()), OrthancException); + + ASSERT_STREQ("generic", Logging::GetCategoryName(Logging::LogCategory_GENERIC)); + ASSERT_STREQ("plugins", Logging::GetCategoryName(Logging::LogCategory_PLUGINS)); + ASSERT_STREQ("http", Logging::GetCategoryName(Logging::LogCategory_HTTP)); + ASSERT_STREQ("sqlite", Logging::GetCategoryName(Logging::LogCategory_SQLITE)); + ASSERT_STREQ("dicom", Logging::GetCategoryName(Logging::LogCategory_DICOM)); + ASSERT_STREQ("lua", Logging::GetCategoryName(Logging::LogCategory_LUA)); + ASSERT_STREQ("jobs", Logging::GetCategoryName(Logging::LogCategory_JOBS)); +} diff --git a/OrthancFramework/UnitTestsSources/LuaTests.cpp b/OrthancFramework/UnitTestsSources/LuaTests.cpp new file mode 100644 index 0000000..27d8751 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/LuaTests.cpp @@ -0,0 +1,184 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/OrthancException.h" +#include "../Sources/Toolbox.h" +#include "../Sources/Lua/LuaFunctionCall.h" + +#include + + +TEST(Lua, Existing) +{ + Orthanc::LuaContext lua; + lua.Execute("a={}"); + lua.Execute("function f() end"); + + ASSERT_TRUE(lua.IsExistingFunction("f")); + ASSERT_FALSE(lua.IsExistingFunction("a")); + ASSERT_FALSE(lua.IsExistingFunction("Dummy")); +} + + +TEST(Lua, ReturnJson) +{ + Json::Value b = Json::objectValue; + b["a"] = 42; + b["b"] = 44.37; + b["c"] = -43; + + Json::Value c = Json::arrayValue; + c.append("test3"); + c.append("test1"); + c.append("test2"); + + Json::Value a = Json::objectValue; + a["Hello"] = "World"; + a["List"] = Json::arrayValue; + a["List"].append(b); + a["List"].append(c); + + Orthanc::LuaContext lua; + + // This is the identity function (it simply returns its input) + lua.Execute("function identity(a) return a end"); + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson("hello"); + Json::Value v; + f.ExecuteToJson(v, false); + ASSERT_EQ("hello", v.asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(42.25); + Json::Value v; + f.ExecuteToJson(v, false); + ASSERT_FLOAT_EQ(42.25f, v.asFloat()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(-42); + Json::Value v; + f.ExecuteToJson(v, false); + ASSERT_EQ(-42, v.asInt()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + Json::Value vv = Json::arrayValue; + f.PushJson(vv); + Json::Value v; + f.ExecuteToJson(v, false); + ASSERT_EQ(Json::arrayValue, v.type()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + Json::Value vv = Json::objectValue; + f.PushJson(vv); + Json::Value v; + f.ExecuteToJson(v, false); + // Lua does not make the distinction between empty lists and empty objects + ASSERT_EQ(Json::arrayValue, v.type()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(b); + Json::Value v; + f.ExecuteToJson(v, false); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_FLOAT_EQ(42.0f, v["a"].asFloat()); + ASSERT_FLOAT_EQ(44.37f, v["b"].asFloat()); + ASSERT_FLOAT_EQ(-43.0f, v["c"].asFloat()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(c); + Json::Value v; + f.ExecuteToJson(v, false); + ASSERT_EQ(Json::arrayValue, v.type()); + ASSERT_EQ("test3", v[0].asString()); + ASSERT_EQ("test1", v[1].asString()); + ASSERT_EQ("test2", v[2].asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(a); + Json::Value v; + f.ExecuteToJson(v, false); + ASSERT_EQ("World", v["Hello"].asString()); + ASSERT_EQ(Json::intValue, v["List"][0]["a"].type()); + ASSERT_EQ(Json::realValue, v["List"][0]["b"].type()); + ASSERT_EQ(Json::intValue, v["List"][0]["c"].type()); + ASSERT_EQ(42, v["List"][0]["a"].asInt()); + ASSERT_FLOAT_EQ(44.37f, v["List"][0]["b"].asFloat()); + ASSERT_EQ(44, v["List"][0]["b"].asInt()); + ASSERT_EQ(-43, v["List"][0]["c"].asInt()); + ASSERT_EQ("test3", v["List"][1][0].asString()); + ASSERT_EQ("test1", v["List"][1][1].asString()); + ASSERT_EQ("test2", v["List"][1][2].asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(a); + Json::Value v; + f.ExecuteToJson(v, true); + ASSERT_EQ("World", v["Hello"].asString()); + ASSERT_EQ(Json::stringValue, v["List"][0]["a"].type()); + ASSERT_EQ(Json::stringValue, v["List"][0]["b"].type()); + ASSERT_EQ(Json::stringValue, v["List"][0]["c"].type()); + ASSERT_FLOAT_EQ(42.0f, boost::lexical_cast(v["List"][0]["a"].asString())); + ASSERT_FLOAT_EQ(44.37f, boost::lexical_cast(v["List"][0]["b"].asString())); + ASSERT_FLOAT_EQ(-43.0f, boost::lexical_cast(v["List"][0]["c"].asString())); + ASSERT_EQ("test3", v["List"][1][0].asString()); + ASSERT_EQ("test1", v["List"][1][1].asString()); + ASSERT_EQ("test2", v["List"][1][2].asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "DumpJson"); + f.PushJson(a); + std::string s; + f.ExecuteToString(s); + + std::string t; + Orthanc::Toolbox::WriteFastJson(t, a); + ASSERT_EQ(s, t); + } +} diff --git a/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp new file mode 100644 index 0000000..c2f67be --- /dev/null +++ b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp @@ -0,0 +1,580 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Cache/MemoryCache.h" +#include "../Sources/Cache/MemoryStringCache.h" +#include "../Sources/Cache/SharedArchive.h" +#include "../Sources/IDynamicObject.h" +#include "../Sources/Logging.h" +#include "../Sources/SystemToolbox.h" + +#include +#include +#include +#include + + +TEST(LRU, Basic) +{ + Orthanc::LeastRecentlyUsedIndex r; + + r.Add("d"); + r.Add("a"); + r.Add("c"); + r.Add("b"); + + r.MakeMostRecent("a"); + r.MakeMostRecent("d"); + r.MakeMostRecent("b"); + r.MakeMostRecent("c"); + r.MakeMostRecent("d"); + r.MakeMostRecent("c"); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ("a", r.RemoveOldest()); + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ("b", r.RemoveOldest()); + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ("d", r.RemoveOldest()); + ASSERT_EQ("c", r.GetOldest()); + ASSERT_EQ("c", r.RemoveOldest()); + + ASSERT_TRUE(r.IsEmpty()); + + ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException); + ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException); +} + + +TEST(LRU, Payload) +{ + Orthanc::LeastRecentlyUsedIndex r; + + r.Add("a", 420); + r.Add("b", 421); + r.Add("c", 422); + r.Add("d", 423); + + r.MakeMostRecent("a"); + r.MakeMostRecent("d"); + r.MakeMostRecent("b"); + r.MakeMostRecent("c"); + r.MakeMostRecent("d"); + r.MakeMostRecent("c"); + + ASSERT_TRUE(r.Contains("b")); + ASSERT_EQ(421, r.Invalidate("b")); + ASSERT_FALSE(r.Contains("b")); + + int p; + ASSERT_TRUE(r.Contains("a", p)); ASSERT_EQ(420, p); + ASSERT_TRUE(r.Contains("c", p)); ASSERT_EQ(422, p); + ASSERT_TRUE(r.Contains("d", p)); ASSERT_EQ(423, p); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(420, r.GetOldestPayload()); + ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(423, r.GetOldestPayload()); + ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p); + + ASSERT_EQ("c", r.GetOldest()); + ASSERT_EQ(422, r.GetOldestPayload()); + ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p); + + ASSERT_TRUE(r.IsEmpty()); +} + + +TEST(LRU, PayloadUpdate) +{ + Orthanc::LeastRecentlyUsedIndex r; + + r.Add("a", 420); + r.Add("b", 421); + r.Add("d", 423); + + r.MakeMostRecent("a", 424); + r.MakeMostRecent("d", 421); + + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(424, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_TRUE(r.IsEmpty()); +} + + + +TEST(LRU, PayloadUpdateBis) +{ + Orthanc::LeastRecentlyUsedIndex r; + + r.AddOrMakeMostRecent("a", 420); + r.AddOrMakeMostRecent("b", 421); + r.AddOrMakeMostRecent("d", 423); + r.AddOrMakeMostRecent("a", 424); + r.AddOrMakeMostRecent("d", 421); + + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(424, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_TRUE(r.IsEmpty()); +} + +TEST(LRU, GetAllKeys) +{ + Orthanc::LeastRecentlyUsedIndex r; + std::vector keys; + + r.AddOrMakeMostRecent("a", 420); + r.GetAllKeys(keys); + + ASSERT_EQ(1u, keys.size()); + ASSERT_EQ("a", keys[0]); + + r.AddOrMakeMostRecent("b", 421); + r.GetAllKeys(keys); + + ASSERT_EQ(2u, keys.size()); + ASSERT_TRUE(std::find(keys.begin(), keys.end(),"a") != keys.end()); + ASSERT_TRUE(std::find(keys.begin(), keys.end(),"b") != keys.end()); +} + + + +namespace +{ + class Integer : public Orthanc::IDynamicObject + { + private: + std::string& log_; + int value_; + + public: + Integer(std::string& log, int v) : log_(log), value_(v) + { + } + + virtual ~Integer() ORTHANC_OVERRIDE + { + LOG(INFO) << "Removing cache entry for " << value_; + log_ += boost::lexical_cast(value_) + " "; + } + }; + + class IntegerProvider : public Orthanc::Deprecated::ICachePageProvider + { + public: + std::string log_; + + virtual Orthanc::IDynamicObject* Provide(const std::string& s) ORTHANC_OVERRIDE + { + LOG(INFO) << "Providing " << s; + return new Integer(log_, boost::lexical_cast(s)); + } + }; +} + + +TEST(MemoryCache, Basic) +{ + IntegerProvider provider; + + { + Orthanc::Deprecated::MemoryCache cache(provider, 3); + cache.Access("42"); // 42 -> exit + cache.Access("43"); // 43, 42 -> exit + cache.Access("45"); // 45, 43, 42 -> exit + cache.Access("42"); // 42, 45, 43 -> exit + cache.Access("43"); // 43, 42, 45 -> exit + cache.Access("47"); // 45 is removed; 47, 43, 42 -> exit + cache.Access("44"); // 42 is removed; 44, 47, 43 -> exit + cache.Access("42"); // 43 is removed; 42, 44, 47 -> exit + // Closing the cache: 47, 44, 42 are successively removed + } + + ASSERT_EQ("45 42 43 47 44 42 ", provider.log_); +} + + + + + +namespace +{ + class S : public Orthanc::IDynamicObject + { + private: + std::string value_; + + public: + explicit S(const std::string& value) : value_(value) + { + } + + const std::string& GetValue() const + { + return value_; + } + }; +} + + +TEST(LRU, SharedArchive) +{ + std::string first, second; + Orthanc::SharedArchive a(3); + first = a.Add(new S("First item")); + second = a.Add(new S("Second item")); + + for (int i = 1; i < 100; i++) + { + a.Add(new S("Item " + boost::lexical_cast(i))); + + // Continuously protect the two first items + { + Orthanc::SharedArchive::Accessor accessor(a, first); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("First item", dynamic_cast(accessor.GetItem()).GetValue()); + } + + { + Orthanc::SharedArchive::Accessor accessor(a, second); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("Second item", dynamic_cast(accessor.GetItem()).GetValue()); + } + + { + Orthanc::SharedArchive::Accessor accessor(a, "nope"); + ASSERT_FALSE(accessor.IsValid()); + ASSERT_THROW(accessor.GetItem(), Orthanc::OrthancException); + } + } + + std::list i; + a.List(i); + + size_t count = 0; + for (std::list::const_iterator + it = i.begin(); it != i.end(); ++it) + { + if (*it == first || + *it == second) + { + count++; + } + } + + ASSERT_EQ(2u, count); +} + + +TEST(MemoryStringCache, Basic) +{ + Orthanc::MemoryStringCache c; + ASSERT_THROW(c.SetMaximumSize(0), Orthanc::OrthancException); + + c.SetMaximumSize(3); + + std::string v; + { + Orthanc::MemoryStringCache::Accessor a(c); + ASSERT_FALSE(a.Fetch(v, "key1")); + } + + { + Orthanc::MemoryStringCache::Accessor a(c); + ASSERT_FALSE(a.Fetch(v, "key1")); + a.Add("key1", "a"); + ASSERT_TRUE(a.Fetch(v, "key1")); + ASSERT_EQ("a", v); + + ASSERT_FALSE(a.Fetch(v, "key2")); + ASSERT_FALSE(a.Fetch(v, "key3")); + + a.Add("key2", "b"); + ASSERT_TRUE(a.Fetch(v, "key1")); + ASSERT_EQ("a", v); + ASSERT_TRUE(a.Fetch(v, "key2")); + ASSERT_EQ("b", v); + + a.Add("key3", "too-large-value"); + ASSERT_TRUE(a.Fetch(v, "key1")); + ASSERT_EQ("a", v); + ASSERT_TRUE(a.Fetch(v, "key2")); + ASSERT_EQ("b", v); + ASSERT_FALSE(a.Fetch(v, "key3")); + + a.Add("key3", "c"); + ASSERT_TRUE(a.Fetch(v, "key2")); + ASSERT_EQ("b", v); + ASSERT_TRUE(a.Fetch(v, "key1")); + ASSERT_EQ("a", v); + ASSERT_TRUE(a.Fetch(v, "key3")); + ASSERT_EQ("c", v); + + // adding a fourth value should remove the oldest accessed value (key2) + a.Add("key4", "d"); + ASSERT_FALSE(a.Fetch(v, "key2")); + ASSERT_TRUE(a.Fetch(v, "key1")); + ASSERT_EQ("a", v); + ASSERT_TRUE(a.Fetch(v, "key3")); + ASSERT_EQ("c", v); + ASSERT_TRUE(a.Fetch(v, "key4")); + ASSERT_EQ("d", v); + + } +} + +TEST(MemoryStringCache, Invalidate) +{ + Orthanc::MemoryStringCache c; + Orthanc::MemoryStringCache::Accessor a(c); + + a.Add("hello", "a"); + a.Add("hello2", "b"); + + std::string v; + ASSERT_TRUE(a.Fetch(v, "hello")); + ASSERT_EQ("a", v); + ASSERT_TRUE(a.Fetch(v, "hello2")); + ASSERT_EQ("b", v); + + c.Invalidate("hello"); + ASSERT_FALSE(a.Fetch(v, "hello")); + ASSERT_TRUE(a.Fetch(v, "hello2")); + ASSERT_EQ("b", v); +} + + +static int ThreadingScenarioHappyStep = 0; +static Orthanc::MemoryStringCache ThreadingScenarioHappyCache; + +void ThreadingScenarioHappyThread1() +{ + // the first thread to call Fetch (will be in charge of adding) + Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioHappyCache); + std::string v; + + LOG(INFO) << "Thread1 will fetch"; + if (!a.Fetch(v, "key1")) + { + LOG(INFO) << "Thread1 has fetch"; + ThreadingScenarioHappyStep = 1; + + // wait for the other thread to fetch too + while (ThreadingScenarioHappyStep < 2) + { + Orthanc::SystemToolbox::USleep(10000); + } + LOG(INFO) << "Thread1 will add after a short sleep"; + Orthanc::SystemToolbox::USleep(100000); + LOG(INFO) << "Thread1 will add"; + + a.Add("key1", "value1"); + + LOG(INFO) << "Thread1 has added"; + } +} + +void ThreadingScenarioHappyThread2() +{ + Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioHappyCache); + std::string v; + + // nobody has added key2 -> go + if (!a.Fetch(v, "key2")) + { + a.Add("key2", "value2"); + } + + // wait until thread 1 has completed its "Fetch" but not added yet + while (ThreadingScenarioHappyStep < 1) + { + Orthanc::SystemToolbox::USleep(10000); + } + + ThreadingScenarioHappyStep = 2; + LOG(INFO) << "Thread2 will fetch"; + // this should wait until thread 1 has added + if (!a.Fetch(v, "key1")) + { + ASSERT_FALSE(true); // this thread should not add since thread1 should have done it + } + LOG(INFO) << "Thread2 has fetched the value"; + ASSERT_EQ("value1", v); +} + + +TEST(MemoryStringCache, ThreadingScenarioHappy) +{ + boost::thread thread1 = boost::thread(ThreadingScenarioHappyThread1); + boost::thread thread2 = boost::thread(ThreadingScenarioHappyThread2); + + thread1.join(); + thread2.join(); +} + + +static int ThreadingScenarioFailureStep = 0; +static Orthanc::MemoryStringCache ThreadingScenarioFailureCache; + +void ThreadingScenarioFailureThread1() +{ + // the first thread to call Fetch (will be in charge of adding) + Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioFailureCache); + std::string v; + + LOG(INFO) << "Thread1 will fetch"; + if (!a.Fetch(v, "key1")) + { + LOG(INFO) << "Thread1 has fetch"; + ThreadingScenarioFailureStep = 1; + + // wait for the other thread to fetch too + while (ThreadingScenarioFailureStep < 2) + { + Orthanc::SystemToolbox::USleep(10000); + } + LOG(INFO) << "Thread1 will add after a short sleep"; + Orthanc::SystemToolbox::USleep(100000); + LOG(INFO) << "Thread1 fails to add because of an error"; + } +} + +void ThreadingScenarioFailureThread2() +{ + Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioFailureCache); + std::string v; + + // wait until thread 1 has completed its "Fetch" but not added yet + while (ThreadingScenarioFailureStep < 1) + { + Orthanc::SystemToolbox::USleep(10000); + } + + ThreadingScenarioFailureStep = 2; + LOG(INFO) << "Thread2 will fetch and wait for thread1 to add"; + // this should wait until thread 1 has added + if (!a.Fetch(v, "key1")) + { + LOG(INFO) << "Thread2 has been awaken and will add since Thread1 has failed to add"; + a.Add("key1", "value1"); + } + LOG(INFO) << "Thread2 has added the value"; +} + + +TEST(MemoryStringCache, ThreadingScenarioFailure) +{ + boost::thread thread1 = boost::thread(ThreadingScenarioFailureThread1); + boost::thread thread2 = boost::thread(ThreadingScenarioFailureThread2); + + thread1.join(); + thread2.join(); +} + + +static int ThreadingScenarioInvalidateStep = 0; +static Orthanc::MemoryStringCache ThreadingScenarioInvalidateCache; + +void ThreadingScenarioInvalidateThread1() +{ + // the first thread to call Fetch (will be in charge of adding) + Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioInvalidateCache); + std::string v; + + LOG(INFO) << "Thread1 will fetch"; + if (!a.Fetch(v, "key1")) + { + LOG(INFO) << "Thread1 has fetch"; + ThreadingScenarioInvalidateStep = 1; + + // wait for the other thread to fetch too + while (ThreadingScenarioInvalidateStep < 2) + { + Orthanc::SystemToolbox::USleep(10000); + } + LOG(INFO) << "Thread1 will invalidate after a short sleep"; + Orthanc::SystemToolbox::USleep(100000); + LOG(INFO) << "Thread1 is invalidating"; + ThreadingScenarioInvalidateCache.Invalidate("key1"); + } +} + +void ThreadingScenarioInvalidateThread2() +{ + Orthanc::MemoryStringCache::Accessor a(ThreadingScenarioInvalidateCache); + std::string v; + + // wait until thread 1 has completed its "Fetch" but not added yet + while (ThreadingScenarioInvalidateStep < 1) + { + Orthanc::SystemToolbox::USleep(10000); + } + + ThreadingScenarioInvalidateStep = 2; + LOG(INFO) << "Thread2 will fetch and wait for thread1 to add"; + // this should wait until thread 1 has added + if (!a.Fetch(v, "key1")) + { + LOG(INFO) << "Thread2 has been awaken because thread1 has invalidated the key"; + } +} + + +TEST(MemoryStringCache, ThreadingScenarioInvalidate) +{ + boost::thread thread1 = boost::thread(ThreadingScenarioInvalidateThread1); + boost::thread thread2 = boost::thread(ThreadingScenarioInvalidateThread2); + + thread1.join(); + thread2.join(); +} diff --git a/OrthancFramework/UnitTestsSources/RestApiTests.cpp b/OrthancFramework/UnitTestsSources/RestApiTests.cpp new file mode 100644 index 0000000..284fd83 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp @@ -0,0 +1,1386 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/ChunkedBuffer.h" +#include "../Sources/Compression/ZlibCompressor.h" +#include "../Sources/HttpServer/HttpContentNegociation.h" +#include "../Sources/HttpServer/MultipartStreamReader.h" +#include "../Sources/HttpServer/StringMatcher.h" +#include "../Sources/Logging.h" +#include "../Sources/OrthancException.h" +#include "../Sources/RestApi/RestApiHierarchy.h" +#include "../Sources/WebServiceParameters.h" + +#include +#include +#include + +#if ORTHANC_SANDBOXED != 1 +# include "../Sources/RestApi/RestApi.h" +#endif + + +using namespace Orthanc; + +#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS) && (ORTHANC_SANDBOXED != 1) +# error UNIT_TESTS_WITH_HTTP_CONNEXIONS is not defined +#endif + +#if !defined(ORTHANC_ENABLE_SSL) +# error ORTHANC_ENABLE_SSL is not defined +#endif + +#if ORTHANC_SANDBOXED != 1 +# include "../Sources/HttpClient.h" +# include "../Sources/SystemToolbox.h" +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(HttpClient, Basic) +{ + HttpClient c; + ASSERT_FALSE(c.IsVerbose()); + c.SetVerbose(true); + ASSERT_TRUE(c.IsVerbose()); + c.SetVerbose(false); + ASSERT_FALSE(c.IsVerbose()); + ASSERT_TRUE(c.IsRedirectionFollowed()); + c.SetRedirectionFollowed(false); + ASSERT_FALSE(c.IsRedirectionFollowed()); + +#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 + // The "http://httpbin.org/get" URL does not automatically redirect + // to HTTPS, so we can use it even if the OpenSSL/HTTPS support is + // disabled in curl + + const std::string URL = "http://httpbin.org/get"; + + Json::Value v; + c.SetUrl(URL); + + c.Apply(v); + ASSERT_TRUE(v.type() == Json::objectValue); + ASSERT_TRUE(v.isMember("url")); + ASSERT_EQ(URL, v["url"].asString()); +#endif +} +#endif + + +#if (UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1) && (ORTHANC_ENABLE_SSL == 1) && (ORTHANC_SANDBOXED != 1) + +/** + The HTTPS CA certificates for Github were extracted as follows: + + (1) We retrieve the URI of the root CA of Github: + + # echo | openssl s_client -servername raw.githubusercontent.com -connect raw.githubusercontent.com:443 2>/dev/null | openssl x509 -text | grep "CA Issuers" + + (2) Once we get the URL to the CA certificate, we convert it to a C + macro that can be used by libcurl: + + # cd UnitTestsSources + # python2 ../Resources/RetrieveCACertificates.py GITHUB_CERTIFICATES http://crt.sectigo.com/SectigoRSADomainValidationSecureServerCA.crt > GithubCACertificates.h +**/ + +#include "GithubCACertificates.h" + +TEST(HttpClient, Ssl) +{ + SystemToolbox::WriteFile(GITHUB_CERTIFICATES, "UnitTestsResults/github.cert"); + + /*{ + std::string s; + SystemToolbox::ReadFile(s, "/etc/ssl/certs/ca-certificates.crt"); + SystemToolbox::WriteFile(s, "UnitTestsResults/bitbucket.cert"); + }*/ + + HttpClient c; + //c.SetVerbose(true); + c.SetHttpsVerifyPeers(true); + c.SetHttpsCACertificates("UnitTestsResults/github.cert"); + + // Test file modified on 2020-04-20, in order to use a git + // repository on BitBucket instead of a Mercurial repository + // (because Mercurial support disappears on 2020-05-31) + c.SetUrl("https://raw.githubusercontent.com/orthanc-server/orthanc-setup-samples/refs/heads/master/docker/serve-folders/orthanc/serve-folders.json"); + + Json::Value v; + c.Apply(v); + ASSERT_TRUE(v.isMember("ServeFolders")); +} + +TEST(HttpClient, SslNoVerification) +{ + HttpClient c; + c.SetHttpsVerifyPeers(false); + c.SetUrl("https://raw.githubusercontent.com/orthanc-server/orthanc-setup-samples/refs/heads/master/docker/serve-folders/orthanc/serve-folders.json"); + + Json::Value v; + c.Apply(v); + ASSERT_TRUE(v.isMember("ServeFolders")); +} + +#endif + + +TEST(ChunkedBuffer, Basic) +{ + for (unsigned int i = 0; i < 2; i++) + { + ChunkedBuffer b; + + if (i == 0) + { + b.SetPendingBufferSize(0); + ASSERT_EQ(0u, b.GetPendingBufferSize()); + } + else + { + ASSERT_EQ(16u * 1024u, b.GetPendingBufferSize()); + } + + ASSERT_EQ(0u, b.GetNumBytes()); + + b.AddChunk("hello", 5); + ASSERT_EQ(5u, b.GetNumBytes()); + + b.AddChunk("world", 5); + ASSERT_EQ(10u, b.GetNumBytes()); + + std::string s; + b.Flatten(s); + ASSERT_EQ("helloworld", s); + } +} + + +TEST(RestApi, ParseCookies) +{ + HttpToolbox::Arguments headers; + HttpToolbox::Arguments cookies; + + headers["cookie"] = "a=b;c=d;;;e=f;;g=h;"; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(4u, cookies.size()); + ASSERT_EQ("b", cookies["a"]); + ASSERT_EQ("d", cookies["c"]); + ASSERT_EQ("f", cookies["e"]); + ASSERT_EQ("h", cookies["g"]); + + headers["cookie"] = " name = value ; name2=value2"; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(2u, cookies.size()); + ASSERT_EQ("value", cookies["name"]); + ASSERT_EQ("value2", cookies["name2"]); + + headers["cookie"] = " ;;; "; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(0u, cookies.size()); + + headers["cookie"] = " ; n=v ;; "; + HttpToolbox::ParseCookies(cookies, headers); + ASSERT_EQ(1u, cookies.size()); + ASSERT_EQ("v", cookies["n"]); +} + + +TEST(RestApi, RestApiPath) +{ + HttpToolbox::Arguments args; + UriComponents trail; + + { + RestApiPath uri("/coucou/{abc}/d/*"); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + ASSERT_EQ("e", trail[0]); + ASSERT_EQ("f", trail[1]); + ASSERT_EQ("g", trail[2]); + + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/")); + ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d")); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi")); + + ASSERT_EQ(3u, uri.GetLevelCount()); + ASSERT_TRUE(uri.IsUniversalTrailing()); + + ASSERT_EQ("coucou", uri.GetLevelName(0)); + ASSERT_THROW(uri.GetWildcardName(0), OrthancException); + + ASSERT_EQ("abc", uri.GetWildcardName(1)); + ASSERT_THROW(uri.GetLevelName(1), OrthancException); + + ASSERT_EQ("d", uri.GetLevelName(2)); + ASSERT_THROW(uri.GetWildcardName(2), OrthancException); + } + + { + RestApiPath uri("/coucou/{abc}/d"); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(0u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + + ASSERT_EQ(3u, uri.GetLevelCount()); + ASSERT_FALSE(uri.IsUniversalTrailing()); + + ASSERT_EQ("coucou", uri.GetLevelName(0)); + ASSERT_THROW(uri.GetWildcardName(0), OrthancException); + + ASSERT_EQ("abc", uri.GetWildcardName(1)); + ASSERT_THROW(uri.GetLevelName(1), OrthancException); + + ASSERT_EQ("d", uri.GetLevelName(2)); + ASSERT_THROW(uri.GetWildcardName(2), OrthancException); + } + + { + RestApiPath uri("/*"); + ASSERT_TRUE(uri.Match(args, trail, "/a/b/c")); + ASSERT_EQ(0u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("a", trail[0]); + ASSERT_EQ("b", trail[1]); + ASSERT_EQ("c", trail[2]); + + ASSERT_EQ(0u, uri.GetLevelCount()); + ASSERT_TRUE(uri.IsUniversalTrailing()); + } +} + + + + + + +static int testValue; + +template +static void SetValue(RestApiGetCall& get) +{ + testValue = value; +} + + +static bool GetDirectory(Json::Value& target, + RestApiHierarchy& hierarchy, + const std::string& uri) +{ + UriComponents p; + Toolbox::SplitUriComponents(p, uri); + return hierarchy.GetDirectory(target, p); +} + + + +namespace +{ + class MyVisitor : public RestApiHierarchy::IVisitor + { + public: + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + bool hasTrailing, + const HttpToolbox::Arguments& components, + const UriComponents& trailing) ORTHANC_OVERRIDE + { + return resource.Handle(*(RestApiGetCall*) NULL); + } + }; +} + + +static bool HandleGet(RestApiHierarchy& hierarchy, + const std::string& uri) +{ + UriComponents p; + Toolbox::SplitUriComponents(p, uri); + MyVisitor visitor; + return hierarchy.LookupResource(p, visitor); +} + + +TEST(RestApi, RestApiHierarchy) +{ + RestApiHierarchy root; + root.Register("/hello/world/test", SetValue<1>); + root.Register("/hello/world/test2", SetValue<2>); + root.Register("/hello/{world}/test3/test4", SetValue<3>); + root.Register("/hello2/*", SetValue<4>); + + Json::Value m; + root.CreateSiteMap(m); + + std::string s; + Toolbox::WriteStyledJson(s, m); + + Json::Value d; + ASSERT_FALSE(GetDirectory(d, root, "/hello")); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/a")); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("test3", d[0].asString()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/world")); + ASSERT_EQ(2u, d.size()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/a/test3")); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("test4", d[0].asString()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test")); + ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test2")); + ASSERT_FALSE(GetDirectory(d, root, "/hello2")); + + testValue = 0; + ASSERT_TRUE(HandleGet(root, "/hello/world/test")); + ASSERT_EQ(testValue, 1); + ASSERT_TRUE(HandleGet(root, "/hello/world/test2")); + ASSERT_EQ(testValue, 2); + ASSERT_TRUE(HandleGet(root, "/hello/b/test3/test4")); + ASSERT_EQ(testValue, 3); + ASSERT_FALSE(HandleGet(root, "/hello/b/test3/test")); + ASSERT_EQ(testValue, 3); + ASSERT_TRUE(HandleGet(root, "/hello2/a/b")); + ASSERT_EQ(testValue, 4); +} + + + + + +namespace +{ + class AcceptHandler : public HttpContentNegociation::IHandler + { + private: + std::string type_; + std::string subtype_; + HttpContentNegociation::Dictionary parameters_; + + public: + AcceptHandler() + { + Reset(); + } + + void Reset() + { + HttpContentNegociation::Dictionary parameters; + Handle("nope", "nope", parameters); + } + + const std::string& GetType() const + { + return type_; + } + + const std::string& GetSubType() const + { + return subtype_; + } + + HttpContentNegociation::Dictionary& GetParameters() + { + return parameters_; + } + + virtual void Handle(const std::string& type, + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE + { + type_ = type; + subtype_ = subtype; + parameters_ = parameters; + } + }; +} + + +TEST(RestApi, HttpContentNegociation) +{ + // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + + AcceptHandler h; + + { + HttpContentNegociation d; + d.Register("audio/mp3", h); + d.Register("audio/basic", h); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("basic", h.GetSubType()); + ASSERT_EQ(0u, h.GetParameters().size()); + + ASSERT_TRUE(d.Apply("audio/*; q=0.2 ; type = test ; hello , audio/nope")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ("mp3", h.GetSubType()); + ASSERT_EQ(3u, h.GetParameters().size()); + ASSERT_EQ("0.2", h.GetParameters() ["q"]); + ASSERT_EQ("test", h.GetParameters() ["type"]); + ASSERT_EQ("", h.GetParameters() ["hello"]); + + ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf")); + + ASSERT_TRUE(d.Apply("*/*; hello=world, application/*; q=0.2, application/pdf")); + ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("world", h.GetParameters() ["hello"]); + } + + // "This would be interpreted as "text/html and text/x-c are the + // preferred media types, but if they do not exist, then send the + // text/x-dvi entity, and if that does not exist, send the + // text/plain entity."" + const std::string T1 = "text/plain; q=0.5, text/html ; hello = \"world\" , text/x-dvi; q=0.8, text/x-c"; + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/html", h); + d.Register("text/x-dvi", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("html", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("world", h.GetParameters() ["hello"]); + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + d.Register("text/x-c", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-c", h.GetSubType()); + ASSERT_EQ(0u, h.GetParameters().size()); + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + d.Register("text/x-c", h); + d.Register("text/html", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_TRUE(h.GetSubType() == "x-c" || h.GetSubType() == "html"); + if (h.GetSubType() == "x-c") + { + ASSERT_EQ(0u, h.GetParameters().size()); + } + else + { + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("world", h.GetParameters() ["hello"]); + } + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + d.Register("text/x-dvi", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("x-dvi", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); + } + + { + HttpContentNegociation d; + d.Register("text/plain", h); + ASSERT_TRUE(d.Apply(T1)); + ASSERT_EQ("text", h.GetType()); + ASSERT_EQ("plain", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.5", h.GetParameters() ["q"]); + } + + // Below are the tests from issue 216: + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=216 + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("image/webp, */*;q=0.8, text/html, application/xhtml+xml, application/xml;q=0.9")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); + } + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("image/webp, */*; q = \"0.8\" , text/html, application/xhtml+xml, application/xml;q=0.9")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); + } + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("text/html, application/xhtml+xml, application/xml, image/webp, */*;q=0.8")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); + } + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ(".2", h.GetParameters() ["q"]); + } +} + + +TEST(WebServiceParameters, Serialization) +{ + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + ASSERT_EQ(v, v2); + + WebServiceParameters p2(v2); + ASSERT_EQ("http://localhost:8042/", p2.GetUrl()); + ASSERT_TRUE(p2.GetUsername().empty()); + ASSERT_TRUE(p2.GetPassword().empty()); + ASSERT_TRUE(p2.GetCertificateFile().empty()); + ASSERT_TRUE(p2.GetCertificateKeyFile().empty()); + ASSERT_TRUE(p2.GetCertificateKeyPassword().empty()); + ASSERT_FALSE(p2.IsPkcs11Enabled()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + v.append("user"); + v.append("pass"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + ASSERT_EQ("http://localhost:8042/", p.GetUrl()); + ASSERT_EQ("user", p.GetUsername()); + ASSERT_EQ("pass", p.GetPassword()); + ASSERT_TRUE(p.GetCertificateFile().empty()); + ASSERT_TRUE(p.GetCertificateKeyFile().empty()); + ASSERT_TRUE(p.GetCertificateKeyPassword().empty()); + ASSERT_FALSE(p.IsPkcs11Enabled()); + + Json::Value v2; + p.Serialize(v2, false, true); + ASSERT_EQ(v, v2); + + p.Serialize(v2, false, false /* no password */); + ASSERT_EQ(Json::arrayValue, v2.type()); + ASSERT_EQ(3u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2[0u].asString()); + ASSERT_EQ("user", v2[1u].asString()); + ASSERT_TRUE(v2[2u].asString().empty()); + + WebServiceParameters p2(v2); // Test decoding + ASSERT_EQ("http://localhost:8042/", p2.GetUrl()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + p.SetPkcs11Enabled(true); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + + ASSERT_EQ(Json::objectValue, v2.type()); + ASSERT_EQ(4u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2["Url"].asString()); + ASSERT_TRUE(v2["Pkcs11"].asBool()); + ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type()); + ASSERT_EQ(0u, v2["HttpHeaders"].size()); + ASSERT_EQ(0, v2["Timeout"].asInt()); + + WebServiceParameters p2(v2); // Test decoding + ASSERT_EQ("http://localhost:8042/", p2.GetUrl()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + p.SetClientCertificate("a", "b", "c"); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + + ASSERT_EQ(Json::objectValue, v2.type()); + ASSERT_EQ(7u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2["Url"].asString()); + ASSERT_EQ("a", v2["CertificateFile"].asString()); + ASSERT_EQ("b", v2["CertificateKeyFile"].asString()); + ASSERT_EQ("c", v2["CertificateKeyPassword"].asString()); + ASSERT_FALSE(v2["Pkcs11"].asBool()); + ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type()); + ASSERT_EQ(0u, v2["HttpHeaders"].size()); + ASSERT_EQ(0, v2["Timeout"].asInt()); + + WebServiceParameters p2(v2); // Test decoding + ASSERT_EQ("http://localhost:8042/", p2.GetUrl()); + } + + { + Json::Value v = Json::arrayValue; + v.append("http://localhost:8042/"); + + WebServiceParameters p(v); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + p.AddHttpHeader("a", "b"); + p.AddHttpHeader("c", "d"); + p.SetTimeout(42); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + Json::Value v2; + p.Serialize(v2, false, true); + WebServiceParameters p2(v2); + + ASSERT_EQ(Json::objectValue, v2.type()); + ASSERT_EQ(4u, v2.size()); + ASSERT_EQ("http://localhost:8042/", v2["Url"].asString()); + ASSERT_FALSE(v2["Pkcs11"].asBool()); + ASSERT_EQ(Json::objectValue, v2["HttpHeaders"].type()); + ASSERT_EQ(2u, v2["HttpHeaders"].size()); + ASSERT_EQ("b", v2["HttpHeaders"]["a"].asString()); + ASSERT_EQ("d", v2["HttpHeaders"]["c"].asString()); + ASSERT_EQ(42, v2["Timeout"].asInt()); + + std::set a; + p2.ListHttpHeaders(a); + ASSERT_EQ(2u, a.size()); + ASSERT_TRUE(a.find("a") != a.end()); + ASSERT_TRUE(a.find("c") != a.end()); + + std::string s; + ASSERT_TRUE(p2.LookupHttpHeader(s, "a")); ASSERT_EQ("b", s); + ASSERT_TRUE(p2.LookupHttpHeader(s, "c")); ASSERT_EQ("d", s); + ASSERT_FALSE(p2.LookupHttpHeader(s, "nope")); + } +} + + +TEST(WebServiceParameters, UserProperties) +{ + Json::Value v = Json::nullValue; + + { + WebServiceParameters p; + p.SetUrl("http://localhost:8042/"); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + + ASSERT_THROW(p.AddUserProperty("Url", "nope"), OrthancException); + p.AddUserProperty("Hello", "world"); + p.AddUserProperty("a", "b"); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + + p.Serialize(v, false, true); + + p.ClearUserProperties(); + ASSERT_FALSE(p.IsAdvancedFormatNeeded()); + } + + { + WebServiceParameters p(v); + ASSERT_TRUE(p.IsAdvancedFormatNeeded()); + ASSERT_TRUE(p.GetHttpHeaders().empty()); + + std::set tmp; + p.ListUserProperties(tmp); + ASSERT_EQ(2u, tmp.size()); + ASSERT_TRUE(tmp.find("a") != tmp.end()); + ASSERT_TRUE(tmp.find("Hello") != tmp.end()); + ASSERT_TRUE(tmp.find("hello") == tmp.end()); + + std::string s; + ASSERT_TRUE(p.LookupUserProperty(s, "a")); ASSERT_TRUE(s == "b"); + ASSERT_TRUE(p.LookupUserProperty(s, "Hello")); ASSERT_TRUE(s == "world"); + ASSERT_FALSE(p.LookupUserProperty(s, "hello")); + } +} + + +TEST(StringMatcher, Basic) +{ + StringMatcher matcher("---"); + + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + + { + const std::string s = ""; + ASSERT_FALSE(matcher.Apply(s)); + } + + { + const std::string s = "abc----def"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin())); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + } + + { + const std::string s = "abc---"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin())); + ASSERT_EQ(s.end(), matcher.GetMatchEnd()); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + ASSERT_EQ("", std::string(matcher.GetMatchEnd(), s.end())); + } + + { + const std::string s = "abc--def"; + ASSERT_FALSE(matcher.Apply(s)); + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + ASSERT_THROW(matcher.GetMatchEnd(), OrthancException); + } + + { + std::string s(10u, '\0'); // String with null values + ASSERT_EQ(10u, s.size()); + ASSERT_FALSE(matcher.Apply(s)); + + s[9] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[8] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[7] = '-'; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(s.c_str() + 7, matcher.GetPointerBegin()); + ASSERT_EQ(s.c_str() + 10, matcher.GetPointerEnd()); + ASSERT_EQ(s.end() - 3, matcher.GetMatchBegin()); + ASSERT_EQ(s.end(), matcher.GetMatchEnd()); + } +} + + +TEST(CStringMatcher, Basic) +{ + CStringMatcher matcher("---"); + + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + + { + ASSERT_FALSE(matcher.Apply(NULL, 0)); + + const std::string s = ""; + ASSERT_FALSE(matcher.Apply(s)); + } + + { + const char* s = "abc---def"; + ASSERT_TRUE(matcher.Apply(s, s + 9)); + + ASSERT_EQ('a', matcher.GetMatchBegin()[-3]); + ASSERT_EQ('b', matcher.GetMatchBegin()[-2]); + ASSERT_EQ('c', matcher.GetMatchBegin()[-1]); + ASSERT_EQ('-', matcher.GetMatchBegin()[0]); + ASSERT_EQ('-', matcher.GetMatchBegin()[1]); + ASSERT_EQ('-', matcher.GetMatchBegin()[2]); + ASSERT_EQ('d', matcher.GetMatchBegin()[3]); + ASSERT_EQ('e', matcher.GetMatchBegin()[4]); + ASSERT_EQ('f', matcher.GetMatchBegin()[5]); + ASSERT_EQ('\0', matcher.GetMatchBegin()[6]); + + ASSERT_EQ('a', matcher.GetMatchEnd()[-6]); + ASSERT_EQ('b', matcher.GetMatchEnd()[-5]); + ASSERT_EQ('c', matcher.GetMatchEnd()[-4]); + ASSERT_EQ('-', matcher.GetMatchEnd()[-3]); + ASSERT_EQ('-', matcher.GetMatchEnd()[-2]); + ASSERT_EQ('-', matcher.GetMatchEnd()[-1]); + ASSERT_EQ('d', matcher.GetMatchEnd()[0]); + ASSERT_EQ('e', matcher.GetMatchEnd()[1]); + ASSERT_EQ('f', matcher.GetMatchEnd()[2]); + ASSERT_EQ('\0', matcher.GetMatchEnd()[3]); + } + + { + const std::string s = "abc----def"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.c_str(), matcher.GetMatchBegin())); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + } + + { + const std::string s = "abc---"; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(3, std::distance(s.c_str(), matcher.GetMatchBegin())); + ASSERT_EQ(s.c_str() + s.size(), matcher.GetMatchEnd()); + ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd())); + ASSERT_EQ("", std::string(matcher.GetMatchEnd(), s.c_str() + s.size())); + } + + { + const std::string s = "abc--def"; + ASSERT_FALSE(matcher.Apply(s)); + ASSERT_THROW(matcher.GetMatchBegin(), OrthancException); + ASSERT_THROW(matcher.GetMatchEnd(), OrthancException); + } + + { + std::string s(10u, '\0'); // String with null values + ASSERT_EQ(10u, s.size()); + ASSERT_FALSE(matcher.Apply(s)); + + s[9] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[8] = '-'; + ASSERT_FALSE(matcher.Apply(s)); + + s[7] = '-'; + ASSERT_TRUE(matcher.Apply(s)); + ASSERT_EQ(s.c_str() + 7, matcher.GetMatchBegin()); + ASSERT_EQ(s.c_str() + 10, matcher.GetMatchEnd()); + ASSERT_EQ(s.c_str() + s.size() - 3, matcher.GetMatchBegin()); + ASSERT_EQ(s.c_str() + s.size(), matcher.GetMatchEnd()); + } +} + + +class MultipartTester : public MultipartStreamReader::IHandler +{ +private: + struct Part + { + MultipartStreamReader::HttpHeaders headers_; + std::string data_; + + Part(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) : + headers_(headers), + data_(reinterpret_cast(part), size) + { + } + }; + + std::vector parts_; + +public: + virtual void HandlePart(const MultipartStreamReader::HttpHeaders& headers, + const void* part, + size_t size) + { + parts_.push_back(Part(headers, part, size)); + } + + unsigned int GetCount() const + { + return parts_.size(); + } + + MultipartStreamReader::HttpHeaders& GetHeaders(size_t i) + { + return parts_[i].headers_; + } + + const std::string& GetData(size_t i) const + { + return parts_[i].data_; + } +}; + + +TEST(MultipartStreamReader, ParseHeaders) +{ + std::string ct, b, st, header; + + { + MultipartStreamReader::HttpHeaders h; + h["hello"] = "world"; + h["Content-Type"] = "world"; // Should be in lower-case + h["CONTENT-type"] = "world"; // Should be in lower-case + ASSERT_FALSE(MultipartStreamReader::GetMainContentType(header, h)); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "world"; + ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); + ASSERT_EQ(header, "world"); + ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header)); + } + + { + MultipartStreamReader::HttpHeaders h; + h["content-type"] = "multipart/related; dummy=value; boundary=1234; hello=world"; + ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); + ASSERT_EQ(header, h["content-type"]); + ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header)); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "1234"); + ASSERT_TRUE(st.empty()); + } + + { + ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType + (ct, st, b, "multipart/related; boundary=")); // Empty boundary + } + + { + ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType + (ct, st, b, "Multipart/Related; TYPE=Application/Dicom; Boundary=heLLO")); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "heLLO"); + ASSERT_EQ(st, "application/dicom"); + } + + { + ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType + (ct, st, b, "Multipart/Related; type=\"application/DICOM\"; Boundary=a")); + ASSERT_EQ(ct, "multipart/related"); + ASSERT_EQ(b, "a"); + ASSERT_EQ(st, "application/dicom"); + } +} + + +TEST(MultipartStreamReader, ParseHeaders2) +{ + std::string main; + std::map args; + + ASSERT_FALSE(MultipartStreamReader::ParseHeaderArguments(main, args, "")); + ASSERT_FALSE(MultipartStreamReader::ParseHeaderArguments(main, args, " ")); + ASSERT_FALSE(MultipartStreamReader::ParseHeaderArguments(main, args, " ; ")); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, "hello")); + ASSERT_EQ("hello", main); + ASSERT_TRUE(args.empty()); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, "hello ; a = \" b \";c=d ; e=f;")); + ASSERT_EQ("hello", main); + ASSERT_EQ(3u, args.size()); + ASSERT_EQ(" b ", args["a"]); + ASSERT_EQ("d", args["c"]); + ASSERT_EQ("f", args["e"]); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, " hello ;;;; ; ")); + ASSERT_EQ("hello", main); + ASSERT_TRUE(args.empty()); + + ASSERT_FALSE(MultipartStreamReader::ParseHeaderArguments(main, args, "hello;a=b;c=d;a=f")); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, "multipart/related; dummy=value; boundary=1234; hello=world")); + ASSERT_EQ("multipart/related", main); + ASSERT_EQ(3u, args.size()); + ASSERT_EQ("value", args["dummy"]); + ASSERT_EQ("1234", args["boundary"]); + ASSERT_EQ("world", args["hello"]); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, "multipart/related; boundary=")); + ASSERT_EQ("multipart/related", main); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ("", args["boundary"]); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, "multipart/related; boundary")); + ASSERT_EQ("multipart/related", main); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ("", args["boundary"]); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, "Multipart/Related; TYPE=Application/Dicom; Boundary=heLLO")); + ASSERT_EQ("multipart/related", main); + ASSERT_EQ(2u, args.size()); + ASSERT_EQ("Application/Dicom", args["type"]); + ASSERT_EQ("heLLO", args["boundary"]); + + ASSERT_TRUE(MultipartStreamReader::ParseHeaderArguments(main, args, "Multipart/Related; type=\"application/DICOM\"; Boundary=a")); + ASSERT_EQ("multipart/related", main); + ASSERT_EQ(2u, args.size()); + ASSERT_EQ("application/DICOM", args["type"]); + ASSERT_EQ("a", args["boundary"]); +} + + +TEST(MultipartStreamReader, BytePerByte) +{ + std::string stream = "GARBAGE"; + + std::string boundary = "123456789123456789"; + + { + for (size_t i = 0; i < 10; i++) + { + std::string f = "hello " + boost::lexical_cast(i); + + stream += "\r\n--" + boundary + "\r\n"; + if (i % 2 == 0) + stream += "Content-Length: " + boost::lexical_cast(f.size()) + "\r\n"; + stream += "Content-Type: toto " + boost::lexical_cast(i) + "\r\n\r\n"; + stream += f; + } + + stream += "\r\n--" + boundary + "--"; + stream += "GARBAGE"; + } + + for (unsigned int k = 0; k < 2; k++) + { + MultipartTester decoded; + + MultipartStreamReader reader(boundary); + reader.SetBlockSize(1); + reader.SetHandler(decoded); + + if (k == 0) + { + for (size_t i = 0; i < stream.size(); i++) + { + reader.AddChunk(&stream[i], 1); + } + } + else + { + reader.AddChunk(stream); + } + + reader.CloseStream(); + + ASSERT_EQ(10u, decoded.GetCount()); + + for (size_t i = 0; i < 10; i++) + { + ASSERT_EQ("hello " + boost::lexical_cast(i), decoded.GetData(i)); + ASSERT_EQ("toto " + boost::lexical_cast(i), decoded.GetHeaders(i)["content-type"]); + + if (i % 2 == 0) + { + ASSERT_EQ(2u, decoded.GetHeaders(i).size()); + ASSERT_TRUE(decoded.GetHeaders(i).find("content-length") != decoded.GetHeaders(i).end()); + } + } + } +} + + +TEST(MultipartStreamReader, Issue190) +{ + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=190 + // https://orthanc.uclouvain.be/hg/orthanc-dicomweb/rev/6dc2f79b5579 + + std::map headers; + headers["content-type"] = "multipart/related; type=application/dicom; boundary=0f3cf5c0-70e0-41ef-baef-c6f9f65ec3e1"; + + { + std::string tmp, contentType, subType, boundary; + ASSERT_TRUE(Orthanc::MultipartStreamReader::GetMainContentType(tmp, headers)); + ASSERT_TRUE(Orthanc::MultipartStreamReader::ParseMultipartContentType(contentType, subType, boundary, tmp)); + ASSERT_EQ("multipart/related", contentType); + ASSERT_EQ("application/dicom", subType); + ASSERT_EQ("0f3cf5c0-70e0-41ef-baef-c6f9f65ec3e1", boundary); + } + + headers["content-type"] = "multipart/related; type=\"application/dicom\"; boundary=\"0f3cf5c0-70e0-41ef-baef-c6f9f65ec3e1\""; + + { + std::string tmp, contentType, subType, boundary; + ASSERT_TRUE(Orthanc::MultipartStreamReader::GetMainContentType(tmp, headers)); + ASSERT_TRUE(Orthanc::MultipartStreamReader::ParseMultipartContentType(contentType, subType, boundary, tmp)); + ASSERT_EQ("multipart/related", contentType); + ASSERT_EQ("application/dicom", subType); + ASSERT_EQ("0f3cf5c0-70e0-41ef-baef-c6f9f65ec3e1", boundary); + } +} + + +TEST(WebServiceParameters, Url) +{ + WebServiceParameters w; + + ASSERT_THROW(w.SetUrl("ssh://coucou"), OrthancException); + w.SetUrl("http://coucou"); + w.SetUrl("https://coucou"); + ASSERT_THROW(w.SetUrl("httpss://coucou"), OrthancException); + ASSERT_THROW(w.SetUrl(""), OrthancException); + + // New in Orthanc 1.7.2: Allow relative URLs (for DICOMweb in Stone) + w.SetUrl("coucou"); + w.SetUrl("/coucou"); +} + + +TEST(ChunkedBuffer, DISABLED_Large) +{ + const size_t LARGE = 60 * 1024 * 1024; + + ChunkedBuffer b; + for (size_t i = 0; i < LARGE; i++) + { + b.AddChunk(boost::lexical_cast(i % 10)); + } + + std::string s; + b.Flatten(s); + ASSERT_EQ(LARGE, s.size()); + ASSERT_EQ(0u, b.GetNumBytes()); + + for (size_t i = 0; i < LARGE; i++) + { + ASSERT_EQ(static_cast('0' + (i % 10)), s[i]); + } + + b.Flatten(s); + ASSERT_EQ(0u, s.size()); +} + + +TEST(ChunkedBuffer, Pending) +{ + ChunkedBuffer b; + + for (size_t pendingSize = 0; pendingSize < 16; pendingSize++) + { + b.SetPendingBufferSize(pendingSize); + ASSERT_EQ(pendingSize, b.GetPendingBufferSize()); + + unsigned int pos = 0; + unsigned int iteration = 0; + + while (pos < 1024) + { + size_t chunkSize = (iteration % 17); + + std::string chunk; + chunk.resize(chunkSize); + for (size_t i = 0; i < chunkSize; i++) + { + chunk[i] = '0' + (pos % 10); + pos++; + } + + b.AddChunk(chunk); + + iteration ++; + } + + std::string s; + b.Flatten(s); + ASSERT_EQ(0u, b.GetNumBytes()); + ASSERT_EQ(pos, s.size()); + + for (size_t i = 0; i < s.size(); i++) + { + ASSERT_EQ(static_cast('0' + (i % 10)), s[i]); + } + } +} + + + +#if ORTHANC_SANDBOXED != 1 + + +namespace +{ + class TotoBody : public HttpClient::IRequestBody + { + private: + size_t size_; + size_t chunkSize_; + size_t pos_; + + public: + TotoBody(size_t size, + size_t chunkSize) : + size_(size), + chunkSize_(chunkSize), + pos_(0) + { + } + + virtual bool ReadNextChunk(std::string& chunk) ORTHANC_OVERRIDE + { + if (pos_ == size_) + { + return false; + } + + chunk.clear(); + chunk.resize(chunkSize_); + + size_t i = 0; + while (pos_ < size_ && + i < chunk.size()) + { + chunk[i] = '0' + (pos_ % 7); + pos_++; + i++; + } + + if (i < chunk.size()) + { + chunk.erase(i, chunk.size()); + } + + return true; + } + }; + + class TotoServer : public IHttpHandler + { + public: + virtual bool CreateChunkedRequestReader(std::unique_ptr& target, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool Handle(HttpOutput& output, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers, + const HttpToolbox::GetArguments& getArguments, + const void* bodyData, + size_t bodySize) ORTHANC_OVERRIDE + { + printf("received %d\n", static_cast(bodySize)); + + const uint8_t* b = reinterpret_cast(bodyData); + + for (size_t i = 0; i < bodySize; i++) + { + if (b[i] != ('0' + i % 7)) + { + throw; + } + } + + output.Answer("ok"); + return true; + } + }; +} + + + +#include "../Sources/HttpServer/HttpServer.h" + +TEST(HttpClient, DISABLED_Issue156_Slow) +{ + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=156 + + TotoServer handler; + HttpServer server; + server.SetPortNumber(5000); + server.Register(handler); + server.Start(); + + WebServiceParameters w; + w.SetUrl("http://localhost:5000"); + + // This is slow in Orthanc <= 1.5.8 (issue 156) + TotoBody body(600 * 1024 * 1024, 6 * 1024 * 1024 - 17); + + HttpClient c(w, "toto"); + c.SetMethod(HttpMethod_Post); + c.AddHeader("Expect", ""); + c.AddHeader("Transfer-Encoding", "chunked"); + c.SetBody(body); + + std::string s; + ASSERT_TRUE(c.Apply(s)); + ASSERT_EQ("ok", s); + + server.Stop(); +} + + +TEST(HttpClient, DISABLED_Issue156_Crash) +{ + TotoServer handler; + HttpServer server; + server.SetPortNumber(5000); + server.Register(handler); + server.Start(); + + WebServiceParameters w; + w.SetUrl("http://localhost:5000"); + + // This crashes Orthanc 1.6.0 to 1.7.2 + TotoBody body(32 * 1024, 1); + + HttpClient c(w, "toto"); + c.SetMethod(HttpMethod_Post); + c.AddHeader("Expect", ""); + c.AddHeader("Transfer-Encoding", "chunked"); + c.SetBody(body); + + std::string s; + ASSERT_TRUE(c.Apply(s)); + ASSERT_EQ("ok", s); + + server.Stop(); +} +#endif diff --git a/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp b/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp new file mode 100644 index 0000000..20a8ec1 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp @@ -0,0 +1,379 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Toolbox.h" +#include "../Sources/SQLite/Connection.h" +#include "../Sources/SQLite/Statement.h" +#include "../Sources/SQLite/Transaction.h" + +#include + + +using namespace Orthanc; +using namespace Orthanc::SQLite; + + +/******************************************************************** + ** Tests from + ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc + ********************************************************************/ + +namespace +{ + class SQLConnectionTest : public testing::Test + { + public: + SQLConnectionTest() + { + } + + virtual ~SQLConnectionTest() ORTHANC_OVERRIDE + { + } + + virtual void SetUp() ORTHANC_OVERRIDE + { + db_.OpenInMemory(); + } + + virtual void TearDown() ORTHANC_OVERRIDE + { + db_.Close(); + } + + Connection& db() + { + return db_; + } + + private: + Connection db_; + }; +} + + + +TEST_F(SQLConnectionTest, Execute) +{ + // Valid statement should return true. + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + EXPECT_EQ(SQLITE_OK, db().GetErrorCode()); + + // Invalid statement should fail. + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b")); + EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode()); +} + +TEST_F(SQLConnectionTest, ExecuteWithErrorCode) { + ASSERT_EQ(SQLITE_OK, + db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)")); + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE")); + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode( + "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)")); +} + +TEST_F(SQLConnectionTest, CachedStatement) { + StatementId id1("foo", 12); + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo(a, b) VALUES (12, 13)")); + + // Create a new cached statement. + { + Statement s(db(), id1, "SELECT a FROM foo"); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + } + + // The statement should be cached still. + EXPECT_TRUE(db().HasCachedStatement(id1)); + + { + // Get the same statement using different SQL. This should ignore our + // SQL and use the cached one (so it will be valid). + Statement s(db(), id1, "something invalid("); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + } + + // Make sure other statements aren't marked as cached. + EXPECT_FALSE(db().HasCachedStatement(SQLITE_FROM_HERE)); +} + +TEST_F(SQLConnectionTest, IsSQLValidTest) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo")); + ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo")); +} + + + +TEST_F(SQLConnectionTest, DoesStuffExist) { + // Test DoesTableExist. + EXPECT_FALSE(db().DoesTableExist("foo")); + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + EXPECT_TRUE(db().DoesTableExist("foo")); + + // Should be case sensitive. + EXPECT_FALSE(db().DoesTableExist("FOO")); + + // Test DoesColumnExist. + EXPECT_FALSE(db().DoesColumnExist("foo", "bar")); + EXPECT_TRUE(db().DoesColumnExist("foo", "a")); + + // Testing for a column on a nonexistent table. + EXPECT_FALSE(db().DoesColumnExist("bar", "b")); +} + +TEST_F(SQLConnectionTest, GetLastInsertRowId) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)")); + + ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)")); + + // Last insert row ID should be valid. + int64_t row = db().GetLastInsertRowId(); + EXPECT_LT(0, row); + + // It should be the primary key of the row we just inserted. + Statement s(db(), "SELECT value FROM foo WHERE id=?"); + s.BindInt64(0, row); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); +} + +TEST_F(SQLConnectionTest, Rollback) { + ASSERT_TRUE(db().BeginTransaction()); + ASSERT_TRUE(db().BeginTransaction()); + EXPECT_EQ(2, db().GetTransactionNesting()); + db().RollbackTransaction(); + EXPECT_FALSE(db().CommitTransaction()); + EXPECT_TRUE(db().BeginTransaction()); +} + + + + +/******************************************************************** + ** Tests from + ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc + ********************************************************************/ + +namespace Orthanc +{ + namespace SQLite + { + class SQLStatementTest : public SQLConnectionTest + { + }; + + TEST_F(SQLStatementTest, Run) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); + + Statement s(db(), "SELECT b FROM foo WHERE a=?"); + // Stepping it won't work since we haven't bound the value. + EXPECT_FALSE(s.Step()); + + // Run should fail since this produces output, and we should use Step(). This + // gets a bit wonky since sqlite says this is OK so succeeded is set. + s.Reset(true); + s.BindInt(0, 3); + EXPECT_FALSE(s.Run()); + EXPECT_EQ(SQLITE_ROW, db().GetErrorCode()); + + // Resetting it should put it back to the previous state (not runnable). + s.Reset(true); + + // Binding and stepping should produce one row. + s.BindInt(0, 3); + EXPECT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + EXPECT_FALSE(s.Step()); + } + + TEST_F(SQLStatementTest, BasicErrorCallback) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)")); + // Insert in the foo table the primary key. It is an error to insert + // something other than an number. This error causes the error callback + // handler to be called with SQLITE_MISMATCH as error code. + Statement s(db(), "INSERT INTO foo (a) VALUES (?)"); + s.BindCString(0, "bad bad"); + EXPECT_THROW(s.Run(), OrthancException); + } + + TEST_F(SQLStatementTest, Reset) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)")); + + Statement s(db(), "SELECT b FROM foo WHERE a = ? "); + s.BindInt(0, 3); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + ASSERT_FALSE(s.Step()); + + s.Reset(false); + // Verify that we can get all rows again. + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + EXPECT_FALSE(s.Step()); + + s.Reset(true); + ASSERT_FALSE(s.Step()); + } + } +} + + + + + + +/******************************************************************** + ** Tests from + ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc + ********************************************************************/ + +namespace +{ + class SQLTransactionTest : public SQLConnectionTest + { + public: + virtual void SetUp() ORTHANC_OVERRIDE + { + SQLConnectionTest::SetUp(); + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + } + + // Returns the number of rows in table "foo". + int CountFoo() + { + Statement count(db(), "SELECT count(*) FROM foo"); + count.Step(); + return count.ColumnInt(0); + } + }; +} + + +TEST_F(SQLTransactionTest, Commit) { + { + Transaction t(db()); + EXPECT_FALSE(t.IsOpen()); + t.Begin(); + EXPECT_TRUE(t.IsOpen()); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + + t.Commit(); + EXPECT_FALSE(t.IsOpen()); + } + + EXPECT_EQ(1, CountFoo()); +} + +TEST_F(SQLTransactionTest, Rollback) { + // Test some basic initialization, and that rollback runs when you exit the + // scope. + { + Transaction t(db()); + EXPECT_FALSE(t.IsOpen()); + t.Begin(); + EXPECT_TRUE(t.IsOpen()); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + } + + // Nothing should have been committed since it was implicitly rolled back. + EXPECT_EQ(0, CountFoo()); + + // Test explicit rollback. + Transaction t2(db()); + EXPECT_FALSE(t2.IsOpen()); + t2.Begin(); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + t2.Rollback(); + EXPECT_FALSE(t2.IsOpen()); + + // Nothing should have been committed since it was explicitly rolled back. + EXPECT_EQ(0, CountFoo()); +} + +// Rolling back any part of a transaction should roll back all of them. +TEST_F(SQLTransactionTest, NestedRollback) { + EXPECT_EQ(0, db().GetTransactionNesting()); + + // Outermost transaction. + { + Transaction outer(db()); + outer.Begin(); + EXPECT_EQ(1, db().GetTransactionNesting()); + + // The first inner one gets committed. + { + Transaction inner1(db()); + inner1.Begin(); + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + EXPECT_EQ(2, db().GetTransactionNesting()); + + inner1.Commit(); + EXPECT_EQ(1, db().GetTransactionNesting()); + } + + // One row should have gotten inserted. + EXPECT_EQ(1, CountFoo()); + + // The second inner one gets rolled back. + { + Transaction inner2(db()); + inner2.Begin(); + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + EXPECT_EQ(2, db().GetTransactionNesting()); + + inner2.Rollback(); + EXPECT_EQ(1, db().GetTransactionNesting()); + } + + // A third inner one will fail in Begin since one has already been rolled + // back. + EXPECT_EQ(1, db().GetTransactionNesting()); + { + Transaction inner3(db()); + EXPECT_THROW(inner3.Begin(), OrthancException); + EXPECT_EQ(1, db().GetTransactionNesting()); + } + } + EXPECT_EQ(0, db().GetTransactionNesting()); + EXPECT_EQ(0, CountFoo()); +} diff --git a/OrthancFramework/UnitTestsSources/SQLiteTests.cpp b/OrthancFramework/UnitTestsSources/SQLiteTests.cpp new file mode 100644 index 0000000..8d1cc0d --- /dev/null +++ b/OrthancFramework/UnitTestsSources/SQLiteTests.cpp @@ -0,0 +1,337 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/SystemToolbox.h" +#include "../Sources/SQLite/Connection.h" +#include "../Sources/SQLite/Statement.h" +#include "../Sources/SQLite/Transaction.h" + +#include + +using namespace Orthanc; + + +TEST(SQLite, Configuration) +{ + /** + * The system-wide version of SQLite under OS X uses + * SQLITE_THREADSAFE==2 (SQLITE_CONFIG_SERIALIZED), whereas the + * static builds of Orthanc use SQLITE_THREADSAFE==1 + * (SQLITE_CONFIG_MULTITHREAD). In any case, we wish to ensure that + * SQLITE_THREADSAFE!=0 (SQLITE_CONFIG_SINGLETHREAD). + **/ + ASSERT_NE(0, sqlite3_threadsafe()); +} + + +TEST(SQLite, Connection) +{ + SystemToolbox::RemoveFile("UnitTestsResults/coucou"); + SQLite::Connection c; + c.Open("UnitTestsResults/coucou"); + c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)"); + c.Execute("INSERT INTO c VALUES(NULL, 42);"); +} + + +TEST(SQLite, StatementReferenceBasic) +{ + sqlite3* db; + sqlite3_open(":memory:", &db); + + { + SQLite::StatementReference r(db, "SELECT * FROM sqlite_master"); + ASSERT_EQ(0u, r.GetReferenceCount()); + + { + SQLite::StatementReference r1(r); + ASSERT_EQ(1u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + + { + SQLite::StatementReference r2(r); + ASSERT_EQ(2u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + ASSERT_EQ(0u, r2.GetReferenceCount()); + + SQLite::StatementReference r3(r2); + ASSERT_EQ(3u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + ASSERT_EQ(0u, r2.GetReferenceCount()); + ASSERT_EQ(0u, r3.GetReferenceCount()); + } + + ASSERT_EQ(1u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + + { + SQLite::StatementReference r2(r); + ASSERT_EQ(2u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + ASSERT_EQ(0u, r2.GetReferenceCount()); + } + + ASSERT_EQ(1u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + } + + ASSERT_EQ(0u, r.GetReferenceCount()); + } + + sqlite3_close(db); +} + +TEST(SQLite, StatementBasic) +{ + SQLite::Connection c; + c.OpenInMemory(); + + SQLite::Statement s(c, "SELECT * from sqlite_master"); + s.Run(); + + for (unsigned int i = 0; i < 5; i++) + { + SQLite::Statement cs(c, SQLITE_FROM_HERE, "SELECT * from sqlite_master"); + cs.Step(); + } +} + + +namespace +{ + static bool destroyed; + + class MyFunc : public SQLite::IScalarFunction + { + public: + MyFunc() + { + destroyed = false; + } + + virtual ~MyFunc() ORTHANC_OVERRIDE + { + destroyed = true; + } + + virtual const char* GetName() const ORTHANC_OVERRIDE + { + return "MYFUNC"; + } + + virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE + { + context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1)); + } + }; + + class MyDelete : public SQLite::IScalarFunction + { + public: + std::set deleted_; + + virtual const char* GetName() const ORTHANC_OVERRIDE + { + return "MYDELETE"; + } + + virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE + { + return 1; + } + + virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE + { + deleted_.insert(context.GetIntValue(0)); + context.SetNullResult(); + } + }; +} + +TEST(SQLite, ScalarFunction) +{ + { + SQLite::Connection c; + c.OpenInMemory(); + c.Register(new MyFunc()); + c.Execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v1 INTEGER, v2 INTEGER);"); + c.Execute("INSERT INTO t VALUES(NULL, 2, 3);"); + c.Execute("INSERT INTO t VALUES(NULL, 4, 4);"); + c.Execute("INSERT INTO t VALUES(NULL, 6, 5);"); + SQLite::Statement t(c, "SELECT MYFUNC(v1, v2), v1, v2 FROM t"); + int i = 0; + while (t.Step()) + { + ASSERT_EQ(t.ColumnInt(0), 1000 + t.ColumnInt(1) * t.ColumnInt(2)); + i++; + } + ASSERT_EQ(3, i); + ASSERT_FALSE(destroyed); + } + ASSERT_TRUE(destroyed); +} + +TEST(SQLite, CascadedDeleteCallback) +{ + SQLite::Connection c; + c.OpenInMemory(); + MyDelete *func = new MyDelete(); + c.Register(func); + c.Execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, dummy INTEGER);"); + c.Execute("CREATE TABLE child(" + " id INTEGER PRIMARY KEY, " + " parent INTEGER REFERENCES parent(id) ON DELETE CASCADE, " + " value INTEGER);"); + c.Execute("CREATE TRIGGER childRemoved " + "AFTER DELETE ON child " + "FOR EACH ROW BEGIN " + " SELECT MYDELETE(old.value); " + "END;"); + + c.Execute("INSERT INTO parent VALUES(42, 100);"); + c.Execute("INSERT INTO parent VALUES(43, 101);"); + + c.Execute("INSERT INTO child VALUES(NULL, 42, 4200);"); + c.Execute("INSERT INTO child VALUES(NULL, 42, 4201);"); + + c.Execute("INSERT INTO child VALUES(NULL, 43, 4300);"); + c.Execute("INSERT INTO child VALUES(NULL, 43, 4301);"); + + // The following command deletes "parent(43, 101)", then in turns + // "child(NULL, 43, 4300/4301)", then calls the MyDelete on 4300 and + // 4301 + c.Execute("DELETE FROM parent WHERE dummy=101"); + + ASSERT_EQ(2u, func->deleted_.size()); + ASSERT_TRUE(func->deleted_.find(4300) != func->deleted_.end()); + ASSERT_TRUE(func->deleted_.find(4301) != func->deleted_.end()); +} + + +TEST(SQLite, EmptyTransactions) +{ + try + { + SQLite::Connection c; + c.OpenInMemory(); + + c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY);"); + c.Execute("INSERT INTO a VALUES(NULL)"); + + { + SQLite::Transaction t(c); + t.Begin(); + { + SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a"); + s.Step(); + } + //t.Commit(); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a"); + s.Step(); + } + } + catch (OrthancException& e) + { + fprintf(stderr, "Exception: [%s]\n", e.What()); + throw e; + } +} + + +TEST(SQLite, Types) +{ + SQLite::Connection c; + c.OpenInMemory(); + c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY, value)"); + + { + SQLite::Statement s(c, std::string("SELECT * FROM a")); + ASSERT_EQ(2, s.ColumnCount()); + ASSERT_FALSE(s.Step()); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a")); + ASSERT_FALSE(s.Step()); + ASSERT_EQ("SELECT * FROM a", s.GetOriginalSQLStatement()); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, "INSERT INTO a VALUES(NULL, ?);"); + s.BindNull(0); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindBool(0, true); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindInt(0, 42); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindInt64(0, 42ll); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindDouble(0, 42.5); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindCString(0, "Hello"); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindBlob(0, "Hello", 5); ASSERT_TRUE(s.Run()); s.Reset(); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a")); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_NULL, s.GetColumnType(1)); + ASSERT_TRUE(s.ColumnIsNull(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); + ASSERT_TRUE(s.ColumnBool(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); + ASSERT_EQ(42, s.ColumnInt(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); + ASSERT_EQ(42ll, s.ColumnInt64(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_FLOAT, s.GetColumnType(1)); + ASSERT_DOUBLE_EQ(42.5, s.ColumnDouble(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_TEXT, s.GetColumnType(1)); + ASSERT_EQ("Hello", s.ColumnString(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_BLOB, s.GetColumnType(1)); + ASSERT_EQ(5, s.ColumnByteLength(1)); + ASSERT_TRUE(!memcmp("Hello", s.ColumnBlob(1), 5)); + + std::string t; + ASSERT_TRUE(s.ColumnBlobAsString(1, &t)); + ASSERT_EQ("Hello", t); + + ASSERT_FALSE(s.Step()); + } +} diff --git a/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp b/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp new file mode 100644 index 0000000..5326158 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp @@ -0,0 +1,54 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +// This file is meant to be used only by ../SharedLibrary/CMakeLists.txt + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#else +# error This file must only be used if testing the Orthanc framework shared library +#endif + +#include "../Sources/Logging.h" +#include "../Sources/Toolbox.h" +#include "../Sources/SystemToolbox.h" + +#include + +int main(int argc, char **argv) +{ + Orthanc::InitializeFramework("", true); + + Orthanc::Logging::EnableInfoLevel(true); + Orthanc::Toolbox::DetectEndianness(); + Orthanc::SystemToolbox::MakeDirectory("UnitTestsResults"); + + ::testing::InitGoogleTest(&argc, argv); + int result = RUN_ALL_TESTS(); + + Orthanc::FinalizeFramework(); + + return result; +} diff --git a/OrthancFramework/UnitTestsSources/StreamTests.cpp b/OrthancFramework/UnitTestsSources/StreamTests.cpp new file mode 100644 index 0000000..61ea066 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/StreamTests.cpp @@ -0,0 +1,337 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Toolbox.h" +#include "../Sources/OrthancException.h" +#include "../Sources/HttpServer/BufferHttpSender.h" +#include "../Sources/HttpServer/HttpStreamTranscoder.h" +#include "../Sources/Compression/ZlibCompressor.h" +#include "../Sources/Compression/GzipCompressor.h" + +#if ORTHANC_SANDBOXED != 1 +# include "../Sources/HttpServer/FilesystemHttpSender.h" +# include "../Sources/SystemToolbox.h" +#endif + + +using namespace Orthanc; + + +TEST(Gzip, Basic) +{ + std::string s = "Hello world"; + + std::string compressed; + GzipCompressor c; + ASSERT_FALSE(c.HasPrefixWithUncompressedSize()); + IBufferCompressor::Compress(compressed, c, s); + + std::string uncompressed; + IBufferCompressor::Uncompress(uncompressed, c, compressed); + ASSERT_EQ(s.size(), uncompressed.size()); + ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size())); +} + + +TEST(Gzip, Empty) +{ + std::string s; + + std::string compressed; + GzipCompressor c; + ASSERT_FALSE(c.HasPrefixWithUncompressedSize()); + c.SetPrefixWithUncompressedSize(false); + IBufferCompressor::Compress(compressed, c, s); + + std::string uncompressed; + IBufferCompressor::Uncompress(uncompressed, c, compressed); + ASSERT_TRUE(uncompressed.empty()); +} + + +TEST(Gzip, BasicWithPrefix) +{ + std::string s = "Hello world"; + + std::string compressed; + GzipCompressor c; + c.SetPrefixWithUncompressedSize(true); + ASSERT_TRUE(c.HasPrefixWithUncompressedSize()); + IBufferCompressor::Compress(compressed, c, s); + + std::string uncompressed; + IBufferCompressor::Uncompress(uncompressed, c, compressed); + ASSERT_EQ(s.size(), uncompressed.size()); + ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size())); +} + + +TEST(Gzip, EmptyWithPrefix) +{ + std::string s; + + std::string compressed; + GzipCompressor c; + c.SetPrefixWithUncompressedSize(true); + ASSERT_TRUE(c.HasPrefixWithUncompressedSize()); + IBufferCompressor::Compress(compressed, c, s); + + std::string uncompressed; + IBufferCompressor::Uncompress(uncompressed, c, compressed); + ASSERT_TRUE(uncompressed.empty()); +} + + +TEST(Zlib, Basic) +{ + std::string s = Toolbox::GenerateUuid(); + s = s + s + s + s; + + std::string compressed; + ZlibCompressor c; + ASSERT_TRUE(c.HasPrefixWithUncompressedSize()); + IBufferCompressor::Compress(compressed, c, s); + + std::string uncompressed; + IBufferCompressor::Uncompress(uncompressed, c, compressed); + ASSERT_EQ(s.size(), uncompressed.size()); + ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size())); +} + + +TEST(Zlib, Level) +{ + std::string s = Toolbox::GenerateUuid(); + s = s + s + s + s; + + std::string compressed, compressed2; + ZlibCompressor c; + c.SetCompressionLevel(9); + IBufferCompressor::Compress(compressed, c, s); + + c.SetCompressionLevel(0); + IBufferCompressor::Compress(compressed2, c, s); + + ASSERT_TRUE(compressed.size() < compressed2.size()); +} + + +TEST(Zlib, DISABLED_Corrupted) // Disabled because it may result in a crash +{ + std::string s = Toolbox::GenerateUuid(); + s = s + s + s + s; + + std::string compressed; + ZlibCompressor c; + IBufferCompressor::Compress(compressed, c, s); + + ASSERT_FALSE(compressed.empty()); + compressed[compressed.size() - 1] = 'a'; + std::string u; + + ASSERT_THROW(IBufferCompressor::Uncompress(u, c, compressed), OrthancException); +} + + +TEST(Zlib, Empty) +{ + std::string s = ""; + + std::string compressed, compressed2; + ZlibCompressor c; + IBufferCompressor::Compress(compressed, c, s); + ASSERT_EQ(compressed, compressed2); + + std::string uncompressed; + IBufferCompressor::Uncompress(uncompressed, c, compressed); + ASSERT_TRUE(uncompressed.empty()); +} + + +#if ORTHANC_SANDBOXED != 1 +static bool ReadAllStream(std::string& result, + IHttpStreamAnswer& stream, + bool allowGzip = false, + bool allowDeflate = false) +{ + stream.SetupHttpCompression(allowGzip, allowDeflate); + + result.resize(static_cast(stream.GetContentLength())); + + size_t pos = 0; + while (stream.ReadNextChunk()) + { + size_t s = stream.GetChunkSize(); + if (pos + s > result.size()) + { + return false; + } + + memcpy(&result[pos], stream.GetChunkContent(), s); + pos += s; + } + + return pos == result.size(); +} +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(BufferHttpSender, Basic) +{ + const std::string s = "Hello world"; + std::string t; + + { + BufferHttpSender sender; + sender.SetChunkSize(1); + ASSERT_TRUE(ReadAllStream(t, sender)); + ASSERT_EQ(0u, t.size()); + } + + for (int cs = 0; cs < 5; cs++) + { + BufferHttpSender sender; + sender.SetChunkSize(cs); + sender.GetBuffer() = s; + ASSERT_TRUE(ReadAllStream(t, sender)); + ASSERT_EQ(s, t); + } +} +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(FilesystemHttpSender, Basic) +{ + const std::string& path = "UnitTestsResults/stream"; + const std::string s = "Hello world"; + std::string t; + + { + SystemToolbox::WriteFile(s, path); + FilesystemHttpSender sender(path); + ASSERT_TRUE(ReadAllStream(t, sender)); + ASSERT_EQ(s, t); + } + + { + SystemToolbox::WriteFile("", path); + FilesystemHttpSender sender(path); + ASSERT_TRUE(ReadAllStream(t, sender)); + ASSERT_EQ(0u, t.size()); + } +} +#endif + + +#if ORTHANC_SANDBOXED != 1 +TEST(HttpStreamTranscoder, Basic) +{ + ZlibCompressor compressor; + + const std::string s = "Hello world " + Toolbox::GenerateUuid(); + + std::string t; + IBufferCompressor::Compress(t, compressor, s); + + for (int cs = 0; cs < 5; cs++) + { + BufferHttpSender sender; + sender.SetChunkSize(cs); + sender.GetBuffer() = t; + std::string u; + ASSERT_TRUE(ReadAllStream(u, sender)); + + std::string v; + IBufferCompressor::Uncompress(v, compressor, u); + ASSERT_EQ(s, v); + } + + // Pass-through test, no decompression occurs + for (int cs = 0; cs < 5; cs++) + { + BufferHttpSender sender; + sender.SetChunkSize(cs); + sender.GetBuffer() = t; + + HttpStreamTranscoder transcode(sender, CompressionType_None); + + std::string u; + ASSERT_TRUE(ReadAllStream(u, transcode)); + + ASSERT_EQ(t, u); + } + + // Pass-through test, decompression occurs + for (int cs = 0; cs < 5; cs++) + { + BufferHttpSender sender; + sender.SetChunkSize(cs); + sender.GetBuffer() = t; + + HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize); + + std::string u; + ASSERT_TRUE(ReadAllStream(u, transcode, false, false)); + + ASSERT_EQ(s, u); + } + + // Pass-through test with zlib, no decompression occurs but deflate is sent + for (int cs = 0; cs < 16; cs++) + { + BufferHttpSender sender; + sender.SetChunkSize(cs); + sender.GetBuffer() = t; + + HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize); + + std::string u; + ASSERT_TRUE(ReadAllStream(u, transcode, false, true)); + + ASSERT_EQ(t.size() - sizeof(uint64_t), u.size()); + ASSERT_EQ(t.substr(sizeof(uint64_t)), u); + } + + for (int cs = 0; cs < 3; cs++) + { + BufferHttpSender sender; + sender.SetChunkSize(cs); + + HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize); + std::string u; + ASSERT_TRUE(ReadAllStream(u, transcode, false, true)); + + ASSERT_EQ(0u, u.size()); + } +} +#endif diff --git a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp new file mode 100644 index 0000000..e0fd2b3 --- /dev/null +++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp @@ -0,0 +1,410 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Compatibility.h" +#include "../Sources/IDynamicObject.h" +#include "../Sources/OrthancException.h" +#include "../Sources/Toolbox.h" + +using namespace Orthanc; + +TEST(Toolbox, Json) +{ + Json::Value a = Json::objectValue; + a["hello"] = "world"; + + std::string b = "{\"hello\" : \"world\"}"; + + Json::Value c; + ASSERT_TRUE(Toolbox::ReadJson(c, b)); + + std::string d, e; + Toolbox::WriteFastJson(d, a); + Toolbox::WriteFastJson(e, c); + ASSERT_EQ(d, e); + + std::string f, g; + Toolbox::WriteStyledJson(f, a); + Toolbox::WriteStyledJson(g, c); + ASSERT_EQ(f, g); + + /** + * Check compatibility with the serialized string generated by + * JsonCpp 1.7.4 (Ubuntu 18.04). "StripSpaces()" removes the + * trailing end-of-line character that was not present in the + * deprecated serialization classes of JsonCpp. + **/ + ASSERT_EQ(Toolbox::StripSpaces(d), "{\"hello\":\"world\"}"); + ASSERT_EQ(Toolbox::StripSpaces(f), "{\n \"hello\" : \"world\"\n}"); +} + +TEST(Toolbox, JsonComments) +{ + std::string a = "/* a */ { /* b */ \"hello\" : /* c */ \"world\" /* d */ } // e"; + + Json::Value b; + ASSERT_TRUE(Toolbox::ReadJsonWithoutComments(b, a)); + + std::string c; + Toolbox::WriteFastJson(c, b); + ASSERT_EQ(Toolbox::StripSpaces(c), "{\"hello\":\"world\"}"); + + Toolbox::WriteStyledJson(c, b); + ASSERT_EQ(Toolbox::StripSpaces(c), "{\n \"hello\" : \"world\"\n}"); +} + +TEST(Toolbox, Base64_allByteValues) +{ + std::string toEncode; + std::string base64Result; + std::string decodedResult; + + size_t size = 2*256; + toEncode.reserve(size); + for (size_t i = 0; i < size; i++) + toEncode.push_back(i % 256); + + Toolbox::EncodeBase64(base64Result, toEncode); + Toolbox::DecodeBase64(decodedResult, base64Result); + + ASSERT_EQ(toEncode, decodedResult); +} + +TEST(Toolbox, Base64_multipleSizes) +{ + std::string toEncode; + std::string base64Result; + std::string decodedResult; + + for (size_t size = 0; size <= 5; size++) + { + printf("base64, testing size %zu\n", size); + toEncode.clear(); + toEncode.reserve(size); + for (size_t i = 0; i < size; i++) + toEncode.push_back(i % 256); + + Toolbox::EncodeBase64(base64Result, toEncode); + Toolbox::DecodeBase64(decodedResult, base64Result); + + ASSERT_EQ(toEncode, decodedResult); + } +} + +static std::string EncodeBase64Bis(const std::string& s) +{ + std::string result; + Toolbox::EncodeBase64(result, s); + return result; +} + + +TEST(Toolbox, Base64) +{ + ASSERT_EQ("", EncodeBase64Bis("")); + ASSERT_EQ("YQ==", EncodeBase64Bis("a")); + + const std::string hello = "SGVsbG8gd29ybGQ="; + ASSERT_EQ(hello, EncodeBase64Bis("Hello world")); + + std::string decoded; + Toolbox::DecodeBase64(decoded, hello); + ASSERT_EQ("Hello world", decoded); + + // Invalid character + ASSERT_THROW(Toolbox::DecodeBase64(decoded, "?"), OrthancException); + + // All the allowed characters + Toolbox::DecodeBase64(decoded, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="); +} + + +#if 0 // enable only when compiling in Release with a C++ 11 compiler +#include // I had troubles to link with boost::chrono ... + +TEST(Toolbox, Base64_largeString) +{ + std::string toEncode; + std::string base64Result; + std::string decodedResult; + + size_t size = 10 * 1024 * 1024; + toEncode.reserve(size); + for (size_t i = 0; i < size; i++) + toEncode.push_back(i % 256); + + std::chrono::high_resolution_clock::time_point start; + std::chrono::high_resolution_clock::time_point afterEncoding; + std::chrono::high_resolution_clock::time_point afterDecoding; + + start = std::chrono::high_resolution_clock::now(); + Orthanc::Toolbox::EncodeBase64(base64Result, toEncode); + afterEncoding = std::chrono::high_resolution_clock::now(); + Orthanc::Toolbox::DecodeBase64(decodedResult, base64Result); + afterDecoding = std::chrono::high_resolution_clock::now(); + + ASSERT_EQ(toEncode, decodedResult); + + printf("encoding took %zu ms\n", (std::chrono::duration_cast(afterEncoding - start))); + printf("decoding took %zu ms\n", (std::chrono::duration_cast(afterDecoding - afterEncoding))); +} +#endif + + +TEST(Toolbox, LargeHexadecimalToDecimal) +{ + // https://stackoverflow.com/a/16967286/881731 + ASSERT_EQ( + "166089946137986168535368849184301740204613753693156360462575217560130904921953976324839782808018277000296027060873747803291797869684516494894741699267674246881622658654267131250470956587908385447044319923040838072975636163137212887824248575510341104029461758594855159174329892125993844566497176102668262139513", + Toolbox::LargeHexadecimalToDecimal("EC851A69B8ACD843164E10CFF70CF9E86DC2FEE3CF6F374B43C854E3342A2F1AC3E30C741CC41E679DF6D07CE6FA3A66083EC9B8C8BF3AF05D8BDBB0AA6Cb3ef8c5baa2a5e531ba9e28592f99e0fe4f95169a6c63f635d0197e325c5ec76219b907e4ebdcd401fb1986e4e3ca661ff73e7e2b8fd9988e753b7042b2bbca76679")); + + ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("")); + ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0")); + ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0000")); + ASSERT_EQ("255", Toolbox::LargeHexadecimalToDecimal("00000ff")); + + ASSERT_THROW(Toolbox::LargeHexadecimalToDecimal("g"), Orthanc::OrthancException); +} + + +TEST(Toolbox, GenerateDicomPrivateUniqueIdentifier) +{ + std::string s = Toolbox::GenerateDicomPrivateUniqueIdentifier(); + ASSERT_EQ("2.25.", s.substr(0, 5)); +} + + +TEST(Toolbox, UniquePtr) +{ + std::unique_ptr i(new int(42)); + ASSERT_EQ(42, *i); + + std::unique_ptr > j(new SingleValueObject(42)); + ASSERT_EQ(42, j->GetValue()); +} + +TEST(Toolbox, IsSetInSet) +{ + { + std::set needles; + std::set haystack; + std::set missings; + + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0u, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + haystack.insert(5); + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0u, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(5); + haystack.insert(5); + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0u, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(5); + + ASSERT_FALSE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(1u, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + ASSERT_TRUE(missings.count(5) == 1); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(6); + haystack.insert(5); + ASSERT_FALSE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(1u, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + ASSERT_TRUE(missings.count(6) == 1); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(5); + needles.insert(6); + haystack.insert(5); + haystack.insert(6); + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0u, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } +} + +TEST(Toolbox, GetSetIntersection) +{ + { + std::set target; + std::set a; + std::set b; + + Toolbox::GetIntersection(target, a, b); + ASSERT_EQ(0u, target.size()); + } + + { + std::set target; + std::set a; + std::set b; + + a.insert(1); + b.insert(1); + + Toolbox::GetIntersection(target, a, b); + ASSERT_EQ(1u, target.size()); + ASSERT_EQ(1u, target.count(1)); + } + + { + std::set target; + std::set a; + std::set b; + + a.insert(1); + a.insert(2); + b.insert(2); + + Toolbox::GetIntersection(target, a, b); + ASSERT_EQ(1u, target.size()); + ASSERT_EQ(0u, target.count(1)); + ASSERT_EQ(1u, target.count(2)); + } + +} + + +TEST(Toolbox, JoinStrings) +{ + { + std::set source; + std::string result; + + Toolbox::JoinStrings(result, source, ";"); + ASSERT_EQ("", result); + } + + { + std::set source; + source.insert("1"); + + std::string result; + + Toolbox::JoinStrings(result, source, ";"); + ASSERT_EQ("1", result); + } + + { + std::set source; + source.insert("2"); + source.insert("1"); + + std::string result; + + Toolbox::JoinStrings(result, source, ";"); + ASSERT_EQ("1;2", result); + } + + { + std::set source; + source.insert("2"); + source.insert("1"); + + std::string result; + + Toolbox::JoinStrings(result, source, "\\"); + ASSERT_EQ("1\\2", result); + } +} + +TEST(Toolbox, JoinUri) +{ + ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org", "path")); + ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org/", "path")); + ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org", "/path")); + ASSERT_EQ("https://test.org/path", Toolbox::JoinUri("https://test.org/", "/path")); + + ASSERT_EQ("http://test.org:8042", Toolbox::JoinUri("http://test.org:8042", "")); + ASSERT_EQ("http://test.org:8042/", Toolbox::JoinUri("http://test.org:8042/", "")); +} + +TEST(Toolbox, GetHumanFileSize) +{ + ASSERT_EQ("234bytes", Toolbox::GetHumanFileSize(234)); + ASSERT_EQ("2.29KB", Toolbox::GetHumanFileSize(2345)); + ASSERT_EQ("22.91KB", Toolbox::GetHumanFileSize(23456)); + ASSERT_EQ("229.07KB", Toolbox::GetHumanFileSize(234567)); + ASSERT_EQ("2.24MB", Toolbox::GetHumanFileSize(2345678)); + ASSERT_EQ("22.37MB", Toolbox::GetHumanFileSize(23456789)); + ASSERT_EQ("223.70MB", Toolbox::GetHumanFileSize(234567890)); + ASSERT_EQ("2.18GB", Toolbox::GetHumanFileSize(2345678901)); + ASSERT_EQ("21.33TB", Toolbox::GetHumanFileSize(23456789012345)); +} + +TEST(Toolbox, GetHumanDuration) +{ + ASSERT_EQ("234ns", Toolbox::GetHumanDuration(234)); + ASSERT_EQ("2.35us", Toolbox::GetHumanDuration(2345)); + ASSERT_EQ("23.46us", Toolbox::GetHumanDuration(23456)); + ASSERT_EQ("234.57us", Toolbox::GetHumanDuration(234567)); + ASSERT_EQ("2.35ms", Toolbox::GetHumanDuration(2345678)); + ASSERT_EQ("2.35s", Toolbox::GetHumanDuration(2345678901)); + ASSERT_EQ("23456.79s", Toolbox::GetHumanDuration(23456789012345)); +} + +TEST(Toolbox, GetHumanTransferSpeed) +{ + ASSERT_EQ("8.00Mbps", Toolbox::GetHumanTransferSpeed(false, 1000, 1000000)); + ASSERT_EQ("8.59Gbps", Toolbox::GetHumanTransferSpeed(false, 1024*1024*1024, 1000000000)); + ASSERT_EQ("1.00GB in 1.00s = 8.59Gbps", Toolbox::GetHumanTransferSpeed(true, 1024*1024*1024, 1000000000)); + ASSERT_EQ("976.56KB in 1.00s = 8.00Mbps", Toolbox::GetHumanTransferSpeed(true, 1000*1000, 1000000000)); +} \ No newline at end of file diff --git a/OrthancFramework/UnitTestsSources/ZipTests.cpp b/OrthancFramework/UnitTestsSources/ZipTests.cpp new file mode 100644 index 0000000..f6a06df --- /dev/null +++ b/OrthancFramework/UnitTestsSources/ZipTests.cpp @@ -0,0 +1,415 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1 +// Must be the first to be sure to use the Orthanc framework shared library +# include +#endif + +#include + +#include "../Sources/Compression/HierarchicalZipWriter.h" +#include "../Sources/Compression/ZipReader.h" +#include "../Sources/OrthancException.h" +#include "../Sources/SystemToolbox.h" +#include "../Sources/TemporaryFile.h" +#include "../Sources/Toolbox.h" + + +using namespace Orthanc; + +TEST(ZipWriter, Basic) +{ + Orthanc::ZipWriter w; + w.SetOutputPath("UnitTestsResults/hello.zip"); + w.Open(); + w.OpenFile("world/hello"); + w.Write("Hello world"); +} + + +TEST(ZipWriter, Basic64) +{ + Orthanc::ZipWriter w; + w.SetOutputPath("UnitTestsResults/hello64.zip"); + w.SetZip64(true); + w.Open(); + w.OpenFile("world/hello"); + w.Write("Hello world"); +} + + +TEST(ZipWriter, Exceptions) +{ + Orthanc::ZipWriter w; + ASSERT_THROW(w.Open(), Orthanc::OrthancException); + w.SetOutputPath("UnitTestsResults/hello3.zip"); + w.Open(); + ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException); +} + + +TEST(ZipWriter, Append) +{ + { + Orthanc::ZipWriter w; + w.SetAppendToExisting(false); + w.SetOutputPath("UnitTestsResults/append.zip"); + w.Open(); + w.OpenFile("world/hello"); + w.Write("Hello world 1"); + } + + { + Orthanc::ZipWriter w; + w.SetAppendToExisting(true); + w.SetOutputPath("UnitTestsResults/append.zip"); + w.Open(); + w.OpenFile("world/appended"); + w.Write("Hello world 2"); + } +} + + + + + +namespace Orthanc +{ + // The namespace is necessary + // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members + + TEST(HierarchicalZipWriter, Index) + { + HierarchicalZipWriter::Index i; + ASSERT_EQ("hello", i.OpenFile("hello")); + ASSERT_EQ("hello-2", i.OpenFile("hello")); + ASSERT_EQ("coucou", i.OpenFile("coucou")); + ASSERT_EQ("hello-3", i.OpenFile("hello")); + + i.OpenDirectory("coucou"); + + ASSERT_EQ("coucou-2/world", i.OpenFile("world")); + ASSERT_EQ("coucou-2/world-2", i.OpenFile("world")); + + i.OpenDirectory("world"); + + ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello")); + ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello")); + + i.CloseDirectory(); + + ASSERT_EQ("coucou-2/world-4", i.OpenFile("world")); + + i.CloseDirectory(); + + ASSERT_EQ("coucou-3", i.OpenFile("coucou")); + + ASSERT_THROW(i.CloseDirectory(), OrthancException); + } + + + TEST(HierarchicalZipWriter, Filenames) + { + ASSERT_EQ("trE hell", HierarchicalZipWriter::Index::KeepAlphanumeric(" ÊtrE hellô ")); + + // The "^" character is considered as a space in DICOM + ASSERT_EQ("Hel lo world", HierarchicalZipWriter::Index::KeepAlphanumeric(" Hel^^ ^\r\n\t^^lo \t ")); + } +} + + +TEST(HierarchicalZipWriter, Basic) +{ + static const std::string SPACES = " "; + + HierarchicalZipWriter w("UnitTestsResults/hello2.zip"); + + w.SetCompressionLevel(0); + + // Inside "/" + w.OpenFile("hello"); + w.Write(SPACES + "hello\n"); + w.OpenFile("hello"); + w.Write(SPACES + "hello-2\n"); + w.OpenDirectory("hello"); + + // Inside "/hello-3" + w.OpenFile("hello"); + w.Write(SPACES + "hello\n"); + w.OpenDirectory("hello"); + + w.SetCompressionLevel(9); + + // Inside "/hello-3/hello-2" + w.OpenFile("hello"); + w.Write(SPACES + "hello\n"); + w.OpenFile("hello"); + w.Write(SPACES + "hello-2\n"); + w.CloseDirectory(); + + // Inside "/hello-3" + w.OpenFile("hello"); + w.Write(SPACES + "hello-3\n"); + + /** + + TO CHECK THE CONTENT OF THE "hello2.zip" FILE: + + # unzip -v hello2.zip + + => There must be 6 files. The first 3 files must have a negative + compression ratio. + + **/ +} + + +TEST(ZipReader, Basic) +{ + TemporaryFile f; + + { + Orthanc::ZipWriter w; + ASSERT_EQ(0u, w.GetArchiveSize()); + + w.SetOutputPath(f.GetPath().c_str()); + w.Open(); + w.OpenFile("world/hello"); + w.Write("Hello world"); + + ASSERT_EQ(w.GetArchiveSize(), SystemToolbox::GetFileSize(f.GetPath())); + } + + ASSERT_TRUE(ZipReader::IsZipFile(f.GetPath())); + + std::unique_ptr reader(ZipReader::CreateFromFile(f.GetPath())); + + ASSERT_EQ(1u, reader->GetFilesCount()); + + std::string filename, content; + ASSERT_TRUE(reader->ReadNextFile(filename, content)); + ASSERT_EQ("world/hello", filename); + ASSERT_EQ("Hello world", content); + ASSERT_FALSE(reader->ReadNextFile(filename, content)); +} + + + +TEST(ZipWriter, Stream) +{ + std::string memory; + + std::string large; + large.resize(4 * 65536); + for (size_t i = 0; i < large.size(); i++) + { + large[i] = rand() % 256; + } + + for (int i = 0; i < 2; i++) + { + { + Orthanc::ZipWriter w; + w.SetMemoryOutput(memory, (i == 0) /* ZIP64? */); + w.Open(); + + w.OpenFile("world/hello"); + w.Write("Hello"); + w.CancelStream(); + } + + ASSERT_THROW(ZipReader::CreateFromMemory(memory), Orthanc::OrthancException); + + memory.clear(); + uint64_t archiveSize; + + { + Orthanc::ZipWriter w; + ASSERT_EQ(0u, w.GetArchiveSize()); + + w.SetMemoryOutput(memory, (i == 0) /* ZIP64? */); + w.Open(); + + ASSERT_EQ(0u, w.GetArchiveSize()); + + w.OpenFile("world/hello"); + w.Write(large); + w.OpenFile("world/hello2"); + w.Write(large); + w.OpenFile("world/hello3"); + w.Write("Hello world"); + w.OpenFile("world/hello4"); + w.Write(large); + + ASSERT_TRUE(memory.empty()); + + uint64_t s1 = w.GetArchiveSize(); + ASSERT_NE(0u, s1); + + w.Close(); + archiveSize = w.GetArchiveSize(); + + ASSERT_NE(archiveSize, s1); + ASSERT_EQ(archiveSize, w.GetArchiveSize()); + } + + ASSERT_EQ(archiveSize, memory.size()); + + std::unique_ptr reader(ZipReader::CreateFromMemory(memory)); + + ASSERT_EQ(4u, reader->GetFilesCount()); + + { + std::string filename, content; + ASSERT_TRUE(reader->ReadNextFile(filename, content)); + ASSERT_EQ("world/hello", filename); + ASSERT_EQ(large.size(), content.size()); + ASSERT_TRUE(memcmp(large.c_str(), content.c_str(), large.size()) == 0); + } + + { + std::string filename, content; + ASSERT_TRUE(reader->ReadNextFile(filename, content)); + ASSERT_EQ("world/hello2", filename); + ASSERT_EQ(large.size(), content.size()); + ASSERT_TRUE(memcmp(large.c_str(), content.c_str(), large.size()) == 0); + } + + { + std::string filename, content; + ASSERT_TRUE(reader->ReadNextFile(filename, content)); + ASSERT_EQ("world/hello3", filename); + ASSERT_EQ("Hello world", content); + } + + { + std::string filename, content; + ASSERT_TRUE(reader->ReadNextFile(filename, content)); + ASSERT_EQ("world/hello4", filename); + ASSERT_EQ(large.size(), content.size()); + ASSERT_TRUE(memcmp(large.c_str(), content.c_str(), large.size()) == 0); + } + + { + std::string filename, content; + ASSERT_FALSE(reader->ReadNextFile(filename, content)); + } + } +} + + +namespace Orthanc +{ + // The namespace is necessary because of FRIEND_TEST + // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members + + TEST(ZipWriter, BufferWithSeek) + { + ZipWriter::BufferWithSeek buffer; + ASSERT_EQ(0u, buffer.GetSize()); + + std::string s; + buffer.Flush(s); + ASSERT_TRUE(s.empty()); + + buffer.Write("hello"); + ASSERT_EQ(5u, buffer.GetSize()); + ASSERT_EQ(5u, buffer.GetPosition()); + buffer.Write("world"); + ASSERT_EQ(10u, buffer.GetSize()); + ASSERT_EQ(10u, buffer.GetPosition()); + buffer.Flush(s); + ASSERT_EQ("helloworld", s); + ASSERT_EQ(0u, buffer.GetSize()); + ASSERT_EQ(0u, buffer.GetPosition()); + + buffer.Write("hello world"); + buffer.Seek(4); + ASSERT_EQ(4u, buffer.GetPosition()); + buffer.Write("ab"); + ASSERT_EQ(6u, buffer.GetPosition()); + buffer.Flush(s); + ASSERT_EQ("hellabworld", s); + ASSERT_EQ(0u, buffer.GetPosition()); + + buffer.Seek(0); + ASSERT_EQ(0u, buffer.GetPosition()); + buffer.Write("abc"); + buffer.Write(""); + ASSERT_EQ(3u, buffer.GetPosition()); + buffer.Seek(3); + ASSERT_THROW(buffer.Seek(4), OrthancException); + ASSERT_EQ(3u, buffer.GetPosition()); + buffer.Write("de"); + buffer.Write(""); + ASSERT_EQ(5u, buffer.GetPosition()); + buffer.Seek(3); + buffer.Seek(3); + ASSERT_EQ(3u, buffer.GetPosition()); + ASSERT_THROW(buffer.Write("def"), OrthancException); + buffer.Write(""); + ASSERT_EQ(3u, buffer.GetPosition()); + buffer.Write("fg"); + ASSERT_EQ(5u, buffer.GetPosition()); + buffer.Write("hi"); + ASSERT_EQ(7u, buffer.GetPosition()); + buffer.Flush(s); + ASSERT_EQ("abcfghi", s); + ASSERT_EQ(0u, buffer.GetPosition()); + + buffer.Write("abc"); + ASSERT_EQ(3u, buffer.GetPosition()); + buffer.Seek(2); + ASSERT_EQ(2u, buffer.GetPosition()); + buffer.Write("z"); + ASSERT_EQ(3u, buffer.GetPosition()); + buffer.Seek(1); + ASSERT_EQ(1u, buffer.GetPosition()); + buffer.Write("y"); + ASSERT_EQ(2u, buffer.GetPosition()); + buffer.Flush(s); + ASSERT_EQ("ayz", s); + ASSERT_EQ(0u, buffer.GetPosition()); + + ASSERT_EQ(0u, buffer.GetPosition()); + buffer.Write("abc"); + ASSERT_EQ(3u, buffer.GetPosition()); + buffer.Seek(1); + ASSERT_EQ(1u, buffer.GetPosition()); + buffer.Write("z"); + ASSERT_EQ(2u, buffer.GetPosition()); + buffer.Seek(3); + ASSERT_EQ(3u, buffer.GetPosition()); + buffer.Write("y"); + ASSERT_EQ(4u, buffer.GetPosition()); + buffer.Flush(s); + ASSERT_EQ("azcy", s); + ASSERT_EQ(0u, buffer.GetPosition()); + + buffer.Flush(s); + ASSERT_TRUE(s.empty()); + ASSERT_EQ(0u, buffer.GetPosition()); + } +} diff --git a/OrthancServer/CMakeLists.txt b/OrthancServer/CMakeLists.txt new file mode 100644 index 0000000..d249bce --- /dev/null +++ b/OrthancServer/CMakeLists.txt @@ -0,0 +1,991 @@ +# 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 . + + +cmake_minimum_required(VERSION 2.8...4.0) +cmake_policy(SET CMP0058 NEW) + +project(Orthanc) + + +##################################################################### +## Generic parameters of the Orthanc framework +##################################################################### + +include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) + +# Enable all the optional components of the Orthanc framework +set(ENABLE_CRYPTO_OPTIONS ON) +set(ENABLE_DCMTK ON) +set(ENABLE_DCMTK_NETWORKING ON) +set(ENABLE_DCMTK_TRANSCODING ON) +set(ENABLE_GOOGLE_TEST ON) +set(ENABLE_JPEG ON) +set(ENABLE_LOCALE ON) +set(ENABLE_LUA ON) +set(ENABLE_OPENSSL_ENGINES ON) # OpenSSL engines are necessary for PKCS11 +set(ENABLE_PNG ON) +set(ENABLE_PUGIXML ON) +set(ENABLE_SQLITE ON) +set(ENABLE_WEB_CLIENT ON) +set(ENABLE_WEB_SERVER ON) +set(ENABLE_ZLIB ON) + + +##################################################################### +## CMake parameters tunable at the command line to configure the +## plugins, the companion tools, and the unit tests +##################################################################### + +# Parameters of the build +set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") +SET(BUILD_MODALITY_WORKLISTS ON CACHE BOOL "Whether to build the sample plugin to serve modality worklists") +SET(BUILD_RECOVER_COMPRESSED_FILE ON CACHE BOOL "Whether to build the companion tool to recover files compressed using Orthanc") +SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Whether to build the ServeFolders plugin") +SET(BUILD_CONNECTIVITY_CHECKS ON CACHE BOOL "Whether to build the ConnectivityChecks plugin") +SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin") +SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin") +SET(BUILD_MULTITENANT_DICOM ON CACHE BOOL "Whether to build the MultitenantDicom plugin") +SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") +SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") + + +##################################################################### +## Configuration of the Orthanc framework +##################################################################### + +if (ENABLE_PLUGINS) + set(ENABLE_PROTOBUF ON) + set(ENABLE_PROTOBUF_COMPILER ON) +endif() + +include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake) +include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) + +# To export the proper symbols in the sample plugins +include(${CMAKE_SOURCE_DIR}/Plugins/Samples/Common/OrthancPluginsExports.cmake) + + +##################################################################### +## List of source files +##################################################################### + +set(ORTHANC_SERVER_SOURCES + ${CMAKE_SOURCE_DIR}/Sources/Database/BaseCompatibilityTransaction.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/GenericFind.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResourceAndParent.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResources.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindRequest.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindResponse.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/MainDicomTagsRegistry.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/OrthancIdentifiers.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/VoidDatabaseListener.cpp + ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceOrigin.cpp + ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceToStore.cpp + ${CMAKE_SOURCE_DIR}/Sources/EmbeddedResourceHttpHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/ExportedResource.cpp + ${CMAKE_SOURCE_DIR}/Sources/LuaScripting.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancConfiguration.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancFindRequestHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancGetRequestHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancHttpHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancInitialization.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancMoveRequestHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestApi.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestArchive.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestChanges.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestModalities.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestResources.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp + ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraint.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraints.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseMetadataConstraint.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/HierarchicalMatcher.cpp + ${CMAKE_SOURCE_DIR}/Sources/Search/ISqlLookupFormatter.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerContext.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerEnumerations.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerIndex.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ArchiveJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/CleaningInstancesJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomModalityStoreJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomGetScuJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomMoveScuJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/LuaJobManager.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/MergeStudyJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/Operations/DeleteResourceOperation.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/Operations/StorePeerOperation.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/Operations/StoreScuOperation.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/Operations/SystemCallOperation.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/OrthancJobUnserializer.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/OrthancPeerStoreJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ResourceModificationJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/SplitStudyJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/StorageCommitmentScpJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerToolbox.cpp + ${CMAKE_SOURCE_DIR}/Sources/SimpleInstanceOrdering.cpp + ${CMAKE_SOURCE_DIR}/Sources/SliceOrdering.cpp + ${CMAKE_SOURCE_DIR}/Sources/StorageCommitmentReports.cpp + ) + + +set(ORTHANC_FRAMEWORK_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/DicomMapTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FileStorageTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FrameworkTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JobsTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LoggingTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LuaTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/RestApiTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/StreamTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ToolboxTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ZipTests.cpp + ) + +set(ORTHANC_SERVER_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/UnitTestsMain.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/VersionsTests.cpp + ) + + +if (ENABLE_PLUGINS) + include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include) + + list(APPEND ORTHANC_SERVER_SOURCES + ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabase.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV3.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV4.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPlugins.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer32.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer64.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsJob.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsManager.cpp + ) + + list(APPEND ORTHANC_SERVER_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp + ) +endif() + + +if (CMAKE_COMPILER_IS_GNUCXX + AND NOT CMAKE_CROSSCOMPILING + AND DCMTK_STATIC_VERSION STREQUAL "3.6.0") + # Add the "-pedantic" flag only on the Orthanc sources, and only if + # cross-compiling DCMTK 3.6.0 + set(ORTHANC_ALL_SOURCES + ${ORTHANC_CORE_SOURCES_INTERNAL} + ${ORTHANC_DICOM_SOURCES_INTERNAL} + ${ORTHANC_SERVER_SOURCES} + ${ORTHANC_FRAMEWORK_UNIT_TESTS} + ${ORTHANC_SERVER_UNIT_TESTS} + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/Plugin.cpp + ${CMAKE_SOURCE_DIR}/Sources/EmbeddedResourceHttpHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/main.cpp + ) + + set_source_files_properties(${ORTHANC_ALL_SOURCES} + PROPERTIES COMPILE_FLAGS -pedantic + ) +endif() + + +##################################################################### +## Autogeneration of files +##################################################################### + +set(ORTHANC_EMBEDDED_FILES + CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + INSTALL_REVISION_AND_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql + INSTALL_DELETED_FILES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallDeletedFiles.sql + INSTALL_KEY_VALUE_STORES_AND_QUEUES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStoresAndQueues.sql + ) + +if (STANDALONE_BUILD) + # We embed all the resources in the binaries for standalone builds + add_definitions( + -DORTHANC_STANDALONE=1 + ) + + list(APPEND ORTHANC_EMBEDDED_FILES + ORTHANC_EXPLORER ${CMAKE_SOURCE_DIR}/OrthancExplorer + ) +else() + add_definitions( + -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\" + -DORTHANC_STANDALONE=0 + ) +endif() + +EmbedResources( + --namespace=Orthanc.ServerResources + --target=OrthancServerResources + --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources + ${ORTHANC_EMBEDDED_FILES} + ) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/Orthanc.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND ORTHANC_RESOURCES ${AUTOGENERATED_DIR}/Orthanc.rc) +endif() + + + +##################################################################### +## Configuration of the C/C++ macros +##################################################################### + +check_symbol_exists(mallopt "malloc.h" HAVE_MALLOPT) +check_symbol_exists(malloc_trim "malloc.h" HAVE_MALLOC_TRIM) + +if (HAVE_MALLOPT) + add_definitions(-DHAVE_MALLOPT=1) +else() + add_definitions(-DHAVE_MALLOPT=0) +endif() + +if (HAVE_MALLOC_TRIM) + add_definitions(-DHAVE_MALLOC_TRIM=1) +else() + add_definitions(-DHAVE_MALLOC_TRIM=0) +endif() + +if (STATIC_BUILD) + add_definitions(-DORTHANC_STATIC=1) +else() + add_definitions(-DORTHANC_STATIC=0) +endif() + + +if (ENABLE_PLUGINS) + add_definitions(-DORTHANC_ENABLE_PLUGINS=1) +else() + add_definitions(-DORTHANC_ENABLE_PLUGINS=0) +endif() + + +if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) +else() + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) +endif() + + +add_definitions( + -DORTHANC_BUILD_UNIT_TESTS=1 + + # Macros for the plugins + -DHAS_ORTHANC_EXCEPTION=0 + ) + + +# Setup precompiled headers for Microsoft Visual Studio + +# WARNING: There must be NO MORE "add_definitions()", "include()" or +# "include_directories()" below, otherwise the generated precompiled +# headers might get broken! + +if (MSVC) + add_definitions(-DORTHANC_USE_PRECOMPILED_HEADERS=1) + + set(TMP + ${ORTHANC_CORE_SOURCES_INTERNAL} + ${ORTHANC_DICOM_SOURCES_INTERNAL} + ) + + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeaders.h" "${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/PrecompiledHeaders.cpp" + TMP ORTHANC_CORE_PCH) + + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeadersServer.h" "${CMAKE_SOURCE_DIR}/Sources/PrecompiledHeadersServer.cpp" + ORTHANC_SERVER_SOURCES ORTHANC_SERVER_PCH) + + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeadersUnitTests.h" "${CMAKE_SOURCE_DIR}/UnitTestsSources/PrecompiledHeadersUnitTests.cpp" + ORTHANC_SERVER_UNIT_TESTS ORTHANC_UNIT_TESTS_PCH) +endif() + + + +##################################################################### +## Build the core of Orthanc +##################################################################### + +add_custom_target(AutogeneratedTarget + DEPENDS + ${AUTOGENERATED_SOURCES} + ) + +# "CoreLibrary" contains all the third-party dependencies and the +# content of the "OrthancFramework" folder +add_library(CoreLibrary + STATIC + ${ORTHANC_CORE_PCH} + ${ORTHANC_CORE_SOURCES} + ${ORTHANC_DICOM_SOURCES} + ${AUTOGENERATED_SOURCES} + ) + +DefineSourceBasenameForTarget(CoreLibrary) + +add_dependencies(CoreLibrary AutogeneratedTarget) + +if (LIBICU_LIBRARIES) + target_link_libraries(CoreLibrary ${LIBICU_LIBRARIES}) +endif() + + +##################################################################### +## Build the Orthanc server +##################################################################### + +if (ENABLE_PLUGINS) + add_custom_command( + COMMAND + ${PROTOC_EXECUTABLE} ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto --cpp_out=${AUTOGENERATED_DIR} -I${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/Resources/PreventProtobufDirectoryLeaks.py ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc + DEPENDS + ProtobufCompiler + ${CMAKE_SOURCE_DIR}/Resources/PreventProtobufDirectoryLeaks.py + ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto + OUTPUT + ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc + ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.h + ) + + add_custom_target(OrthancDatabaseProtobuf + DEPENDS + ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.h + ) + + list(APPEND ORTHANC_SERVER_SOURCES + ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc + ) +else() + add_custom_target(OrthancDatabaseProtobuf) +endif() + +add_library(ServerLibrary + STATIC + ${ORTHANC_SERVER_PCH} + ${ORTHANC_SERVER_SOURCES} + ) + +DefineSourceBasenameForTarget(ServerLibrary) + +# Ensure autogenerated code is built before building ServerLibrary +add_dependencies(ServerLibrary CoreLibrary OrthancDatabaseProtobuf) + +add_executable(Orthanc + ${CMAKE_SOURCE_DIR}/Sources/main.cpp + ${ORTHANC_RESOURCES} + ) + +DefineSourceBasenameForTarget(Orthanc) + +target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES}) + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # The link flag below hides all the global functions so that a Linux + # Standard Base (LSB) build of Orthanc can load plugins that are not + # built using LSB (new in Orthanc 1.9.7) + set_property( + TARGET Orthanc + PROPERTY LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/Resources/VersionScriptOrthanc.map" + ) +endif() + +install( + TARGETS Orthanc + RUNTIME DESTINATION sbin + ) + + +##################################################################### +## Build the unit tests +##################################################################### + +add_executable(UnitTests + ${GOOGLE_TEST_SOURCES} + ${ORTHANC_UNIT_TESTS_PCH} + ${ORTHANC_FRAMEWORK_UNIT_TESTS} + ${ORTHANC_SERVER_UNIT_TESTS} + ${BOOST_EXTENDED_SOURCES} + ) + +DefineSourceBasenameForTarget(UnitTests) + +target_link_libraries(UnitTests + ServerLibrary + CoreLibrary + ${DCMTK_LIBRARIES} + ${GOOGLE_TEST_LIBRARIES} + ) + + +##################################################################### +## Static library to share third-party libraries between the plugins +##################################################################### + +if (ENABLE_PLUGINS AND + (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR + BUILD_DELAYED_DELETION OR BUILD_MULTITENANT_DICOM)) + set(PLUGINS_DEPENDENCIES_SOURCES + ${BOOST_SOURCES} + ${JSONCPP_SOURCES} + ${LIBICONV_SOURCES} + ${LIBICU_SOURCES} + ${PUGIXML_SOURCES} + ${UUID_SOURCES} + ${ZLIB_SOURCES} + + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/ThirdParty/base64/base64.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/ThirdParty/md5/md5.c + Plugins/Samples/Common/OrthancPluginCppWrapper.cpp + ) + + if (BUILD_DELAYED_DELETION) + list(APPEND PLUGINS_DEPENDENCIES_SOURCES + ${SQLITE_SOURCES} + ) + endif() + + if (BUILD_MULTITENANT_DICOM) + list(APPEND PLUGINS_DEPENDENCIES_SOURCES + ${DCMTK_SOURCES} + ${OPENSSL_SOURCES} + ${LIBJPEG_SOURCES} + ${LIBPNG_SOURCES} + ) + endif() + + add_library(PluginsDependencies STATIC + ${PLUGINS_DEPENDENCIES_SOURCES} + ) + + DefineSourceBasenameForTarget(PluginsDependencies) + + add_dependencies(PluginsDependencies AutogeneratedTarget) + + # Add the "-fPIC" option as this static library must be embedded + # inside shared libraries (important on UNIX) + set_target_properties( + PluginsDependencies + PROPERTIES POSITION_INDEPENDENT_CODE ON + ) +endif() + + +##################################################################### +## Build the "ServeFolders" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_SERVE_FOLDERS) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} ServeFolders ServeFolders.dll "Orthanc plugin to serve additional folders" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/ServeFolders.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND SERVE_FOLDERS_RESOURCES ${AUTOGENERATED_DIR}/ServeFolders.rc) + endif() + + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/Plugin.cpp + PROPERTIES COMPILE_DEFINITIONS "SERVE_FOLDERS_VERSION=\"${ORTHANC_VERSION}\"" + ) + + add_library(ServeFolders SHARED + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/Plugin.cpp + ${SERVE_FOLDERS_RESOURCES} + ) + + DefineSourceBasenameForTarget(ServeFolders) + + target_link_libraries(ServeFolders PluginsDependencies) + + set_target_properties( + ServeFolders PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS ServeFolders + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + + +##################################################################### +## Build the "ModalityWorklists" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_MODALITY_WORKLISTS) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} ModalityWorklists ModalityWorklists.dll "Sample Orthanc plugin to serve modality worklists" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/ModalityWorklists.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND MODALITY_WORKLISTS_RESOURCES ${AUTOGENERATED_DIR}/ModalityWorklists.rc) + endif() + + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp + PROPERTIES COMPILE_DEFINITIONS "MODALITY_WORKLISTS_VERSION=\"${ORTHANC_VERSION}\"" + ) + + add_library(ModalityWorklists SHARED + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp + ${MODALITY_WORKLISTS_RESOURCES} + ) + + DefineSourceBasenameForTarget(ModalityWorklists) + + target_link_libraries(ModalityWorklists PluginsDependencies) + + set_target_properties( + ModalityWorklists PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS ModalityWorklists + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + + +##################################################################### +## Build the "ConnectivityChecks" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_CONNECTIVITY_CHECKS) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} ConnectivityChecks ConnectivityChecks.dll "Orthanc plugin to show connectivity status" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/ConnectivityChecks.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND CONNECTIVITY_CHECKS_RESOURCES ${AUTOGENERATED_DIR}/ConnectivityChecks.rc) + endif() + + include(${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake) + + EmbedResources( + --target=ConnectivityChecksResources + --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources + WEB_RESOURCES ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/WebResources + LIBRARIES ${CONNECTIVITY_CHECKS_JAVASCRIPT_DIR} + ) + + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/Plugin.cpp + PROPERTIES COMPILE_DEFINITIONS "ORTHANC_PLUGIN_NAME=\"connectivity-checks\";ORTHANC_PLUGIN_VERSION=\"${ORTHANC_VERSION}\"" + ) + + # The "OrthancFrameworkDependencies.cpp" file is used to bypass the + # precompiled headers if compiling with Visual Studio + add_library(ConnectivityChecks SHARED + ${AUTOGENERATED_DIR}/ConnectivityChecksResources.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/Plugin.cpp + + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp + ${CONNECTIVITY_CHECKS_RESOURCES} + ) + + DefineSourceBasenameForTarget(ConnectivityChecks) + + target_link_libraries(ConnectivityChecks PluginsDependencies) + + set_target_properties( + ConnectivityChecks PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS ConnectivityChecks + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + +##################################################################### +## Build the "DelayedDeletion" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_DELAYED_DELETION) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} DelayedDeletion DelayedDeletion.dll "Orthanc plugin to delay deletion of files" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/DelayedDeletion.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND DELAYED_DELETION_RESOURCES ${AUTOGENERATED_DIR}/DelayedDeletion.rc) + endif() + + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/Plugin.cpp + PROPERTIES COMPILE_DEFINITIONS "ORTHANC_PLUGIN_NAME=\"delayed-deletion\";ORTHANC_PLUGIN_VERSION=\"${ORTHANC_VERSION}\"" + ) + + # The "OrthancFrameworkDependencies.cpp" file is used to bypass the + # precompiled headers if compiling with Visual Studio + add_library(DelayedDeletion SHARED + ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/Plugin.cpp + + ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp + ${DELAYED_DELETION_RESOURCES} + ) + + DefineSourceBasenameForTarget(DelayedDeletion) + + target_link_libraries(DelayedDeletion PluginsDependencies) + + set_target_properties( + DelayedDeletion PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS DelayedDeletion + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + +##################################################################### +## Build the "Housekeeper" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_HOUSEKEEPER) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} Housekeeper Housekeeper.dll "Sample Orthanc plugin to optimize/clean the DB/Storage" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/Housekeeper.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND HOUSEKEEPER_RESOURCES ${AUTOGENERATED_DIR}/Housekeeper.rc) + endif() + + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Plugins/Samples/Housekeeper/Plugin.cpp + PROPERTIES COMPILE_DEFINITIONS "HOUSEKEEPER_VERSION=\"${ORTHANC_VERSION}\"" + ) + + add_library(Housekeeper SHARED + ${CMAKE_SOURCE_DIR}/Plugins/Samples/Housekeeper/Plugin.cpp + ${HOUSEKEEPER_RESOURCES} + ) + + DefineSourceBasenameForTarget(Housekeeper) + + target_link_libraries(Housekeeper PluginsDependencies) + + set_target_properties( + Housekeeper PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS Housekeeper + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + +##################################################################### +## Build the "MultitenantDicom" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_MULTITENANT_DICOM) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} MultitenantDicom MultitenantDicom.dll "Orthanc plugin to provide a multitenant DICOM server" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/MultitenantDicom.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND MULTITENANT_DICOM_RESOURCES ${AUTOGENERATED_DIR}/MultitenantDicom.rc) + endif() + + EmbedResources( + --target=MultitenantDicomResources + --namespace=Orthanc.FrameworkResources + --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources + ${LIBICU_RESOURCES} + ${DCMTK_DICTIONARIES} + ) + + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/Plugin.cpp + PROPERTIES COMPILE_DEFINITIONS "ORTHANC_PLUGIN_VERSION=\"${ORTHANC_VERSION}\"" + ) + + # The "OrthancFrameworkDependencies.cpp" file is used to bypass the + # precompiled headers if compiling with Visual Studio + add_library(MultitenantDicom SHARED + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/DicomFilter.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/Plugin.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp + + ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp + ${AUTOGENERATED_DIR}/MultitenantDicomResources.cpp + ${MULTITENANT_DICOM_RESOURCES} + ) + + DefineSourceBasenameForTarget(MultitenantDicom) + + target_link_libraries(MultitenantDicom PluginsDependencies ${DCMTK_LIBRARIES}) + + set_target_properties( + MultitenantDicom PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS MultitenantDicom + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + +##################################################################### +## Build the companion tool to recover files compressed using Orthanc +##################################################################### + +if (BUILD_RECOVER_COMPRESSED_FILE) + set(RECOVER_COMPRESSED_SOURCES + ${CMAKE_SOURCE_DIR}/Resources/Samples/Tools/RecoverCompressedFile.cpp + ) + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} OrthancRecoverCompressedFile OrthancRecoverCompressedFile.exe + "Lightweight, RESTful DICOM server for medical imaging" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/OrthancRecoverCompressedFile.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND RECOVER_COMPRESSED_SOURCES + ${AUTOGENERATED_DIR}/OrthancRecoverCompressedFile.rc + ) + endif() + + add_executable(OrthancRecoverCompressedFile ${RECOVER_COMPRESSED_SOURCES}) + DefineSourceBasenameForTarget(OrthancRecoverCompressedFile) + + target_link_libraries(OrthancRecoverCompressedFile CoreLibrary) + + install( + TARGETS OrthancRecoverCompressedFile + RUNTIME DESTINATION bin + ) +endif() + + + +##################################################################### +## Generate the documentation if Doxygen is present +##################################################################### + +find_package(Doxygen) +if (DOXYGEN_FOUND) + configure_file( + ${CMAKE_SOURCE_DIR}/Resources/Orthanc.doxygen + ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen + @ONLY) + + configure_file( + ${CMAKE_SOURCE_DIR}/Resources/OrthancPlugin.doxygen + ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen + @ONLY) + + add_custom_target(doc + ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen + COMMENT "Generating internal documentation with Doxygen" VERBATIM + ) + + add_custom_command(TARGET Orthanc + POST_BUILD + COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating plugin documentation with Doxygen" VERBATIM + ) + + install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/OrthancPluginDocumentation/doc/ + DESTINATION share/doc/orthanc/OrthancPlugin + ) +else() + message("Doxygen not found. The documentation will not be built.") +endif() + + + +##################################################################### +## Install the plugin SDK +##################################################################### + +if (ENABLE_PLUGINS) + install( + FILES + ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCPlugin.h + ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCDatabasePlugin.h + ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto + DESTINATION include/orthanc + ) +endif() + + + +##################################################################### +## Prepare the "uninstall" target +## http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F +##################################################################### + +configure_file( + "${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/Uninstall.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" + IMMEDIATE @ONLY) + +add_custom_target(uninstall + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) diff --git a/OrthancServer/OrthancExplorer/explorer.css b/OrthancServer/OrthancExplorer/explorer.css new file mode 100644 index 0000000..06d1d44 --- /dev/null +++ b/OrthancServer/OrthancExplorer/explorer.css @@ -0,0 +1,85 @@ +/** + * 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 . + **/ + + +ul.tree ul { + margin-left: 36px; +} + +#progress { + position: relative; + /*height: 2em; */ + width: 100%; + background-color: grey; + height: 2.5em; +} + +#progress .label { + z-index: 10; + position: absolute; + left:0; + top: 0; + width: 100%; + font-weight: bold; + text-align: center; + text-shadow: none; + padding: .5em; + color: white; +} + +#progress .bar { + z-index: 0; + position: absolute; + left:0; + top: 0; + height: 100%; + width: 0%; + background-color: green; +} + +.ui-title a { + text-decoration: none; + color: white !important; +} + +.switch-container .ui-slider-switch { + width: 100%; +} + +.label { + display: inline-block; + background-color: gray; + margin: 5px; + padding: 5px; + border-radius: 10px; +} + +.label button { + background-color: transparent; + border: 0px; + cursor: pointer; + border-radius: 10px; +} + +.label button:hover { + background-color: lightgray; +} diff --git a/OrthancServer/OrthancExplorer/explorer.html b/OrthancServer/OrthancExplorer/explorer.html new file mode 100644 index 0000000..505af9c --- /dev/null +++ b/OrthancServer/OrthancExplorer/explorer.html @@ -0,0 +1,693 @@ + + + + + + + + Orthanc Explorer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Lookup studies

+
+ Lookup + Plugins +
+ +
+
+
+

+ + Orthanc + +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
 
+
+
+
+
+

Warning:

Your lookup led to many results! + Showing only ? studies to + avoid performance issue. Please make your query more + specific, then relaunch the lookup. +
+
 
+
+
    +
+
+
+
+ +
+
+

All patients

+
+ Lookup + Plugins +
+ +
+
+
+
+

Warning:

This is a large Orthanc server. Showing + only ? patients to avoid + performance issue. Make sure to use lookup if targeting + specific patients! +
+
 
+
+
    +
+
+
+ +
+
+

All studies

+
+ Lookup + Plugins +
+ +
+
+
+
+

Warning:

This is a large Orthanc server. Showing + only ? studies to avoid + performance issue. Make sure to use lookup if targeting + specific studies! +
+
 
+
+
    +
+
+
+ +
+
+

Upload DICOM files

+
+ Lookup + Plugins +
+
+
+
+ + +
+

+

+
+ +
+
+

+
+

Warning:

Orthanc issue #21: On Firefox, especially on + Linux & OSX systems, files might be missing if using + drag-and-drop. Please use the "Select files to upload" button + instead, or use the command-line "ImportDicomFiles.py" script. +
+
    +
  • Drag and drop DICOM files here
  • +
+
+
+ +
+
+

Patient

+
+ Lookup + Plugins +
+ +
+
+
+
+
+
    +
+

+

+ +
+

+ + + +
+
+
+
+
    +
+
+
+
+
+
+ +
+
+

+ + Patient » + Study +

+
+ Lookup + Plugins +
+ +
+
+
+ +
+
+
    +
+
+
+
+
+
+ +
+
+

+ + Patient » + Study » + Series +

+
+ Lookup + Plugins +
+ +
+ +
+ +
+
+

+ + Patient » + Study » + Series » + Instance +

+
+ Lookup + Plugins +
+ +
+
+
+ +
+
+
+

DICOM Tags

+

+ + +

+

Meta header

+
+

Dataset

+

+ Transfer syntax: +

+
+
+
+
+
+
+
+ +
+
+

Plugins

+
+ Lookup + Plugins +
+
+
+
    +
+
+
+ +
+
+

DICOM Query/Retrieve (1/4)

+
+ Lookup + Plugins +
+
+
+
+
+ + +
+ +
+
+ Field of interest: + + + + + + + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ Modalities: + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + +
+
+

DICOM Query/Retrieve (2/4)

+
+ Lookup + Plugins +
+ Query/Retrieve +
+
+
    +
+
+
+ + +
+
+

DICOM Query/Retrieve (3/4)

+
+ Lookup + Plugins +
+ Query/Retrieve +
+
+
    +
+
+
+ + +
+
+

DICOM Query/Retrieve (4/4)

+
+ Lookup + Plugins +
+ Query/Retrieve +
+ +
+
+
+ + +
+ +
+
+
+ +
+
+
+
+
+
+ + +
+
+

Jobs

+
+ Lookup + Plugins +
+
+
+
    +
+
+
+ +
+
+

Job

+
+ Lookup + Plugins +
+
+ Jobs +
+
+
+
    +
+ +
+
+
+ + + + +
+
+
+
+
+ + + + + + + + + + + + + diff --git a/OrthancServer/OrthancExplorer/explorer.js b/OrthancServer/OrthancExplorer/explorer.js new file mode 100644 index 0000000..10701c5 --- /dev/null +++ b/OrthancServer/OrthancExplorer/explorer.js @@ -0,0 +1,1869 @@ +/** + * 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://stackoverflow.com/questions/1663741/is-there-a-good-jquery-drag-and-drop-file-upload-plugin + + +// Forbid the access to IE +if ($.browser.msie) +{ + alert("Please use Mozilla Firefox or Google Chrome. Microsoft Internet Explorer is not supported."); +} + +// http://jquerymobile.com/demos/1.1.0/docs/api/globalconfig.html +//$.mobile.ajaxEnabled = false; +//$.mobile.page.prototype.options.addBackBtn = true; +//$.mobile.defaultPageTransition = 'slide'; + + +var LIMIT_RESOURCES = 100; + +var currentPage = ''; +var currentUuid = ''; + +var ACQUISITION_NUMBER = '0020,0012'; +var IMAGES_IN_ACQUISITION = '0020,1002'; +var IMAGE_ORIENTATION_PATIENT = '0020,0037'; +var IMAGE_POSITION_PATIENT = '0020,0032'; +var INSTANCE_CREATION_DATE = '0008,0012'; +var INSTANCE_CREATION_TIME = '0008,0013'; +var INSTANCE_NUMBER = '0020,0013'; +var MANUFACTURER = '0008,0070'; +var OTHER_PATIENT_IDS = '0010,1000'; +var PATIENT_BIRTH_DATE = '0010,0030'; +var PATIENT_NAME = '0010,0010'; +var SERIES_DATE = '0008,0021'; +var SERIES_DESCRIPTION = '0008,103e'; +var SERIES_INSTANCE_UID = '0020,000e'; +var SERIES_TIME = '0008,0031'; +var SOP_INSTANCE_UID = '0008,0018'; +var STUDY_DATE = '0008,0020'; +var STUDY_DESCRIPTION = '0008,1030'; +var STUDY_INSTANCE_UID = '0020,000d'; +var STUDY_TIME = '0008,0030'; + +var ANONYMIZED_FROM = 'AnonymizedFrom'; +var MODIFIED_FROM = 'ModifiedFrom'; + + +function IsAlphanumeric(s) +{ + return s.match(/^[0-9a-zA-Z]+$/); +} + +function IsValidLabelName(s) +{ + return s.match(/^[0-9a-zA-Z\-_]+$/); +} + + +function DeepCopy(obj) +{ + return jQuery.extend(true, {}, obj); +} + + +function ChangePage(page, options) +{ + var first = true; + var value; + + if (options) { + for (var key in options) { + value = options[key]; + if (first) { + page += '?'; + first = false; + } else { + page += '&'; + } + + page += key + '=' + value; + } + } + + window.location.replace('explorer.html#' + page); + /*$.mobile.changePage('#' + page, { + changeHash: true + });*/ +} + + +function Refresh() +{ + if (currentPage == 'patient') + RefreshPatient(); + else if (currentPage == 'study') + RefreshStudy(); + else if (currentPage == 'series') + RefreshSeries(); + else if (currentPage == 'instance') + RefreshInstance(); +} + + +$(document).ready(function() { + var trees = [ '#dicom-tree', '#dicom-metaheader' ]; + + for (var i = 0; i < trees.length; i++) { + $(trees[i]).tree({ + autoEscape: false + }); + + $(trees[i]).bind( + 'tree.click', + function(event) { + if (event.node.is_open) + $(trees[i]).tree('closeNode', event.node, true); + else + $(trees[i]).tree('openNode', event.node, true); + } + ); + } + + // Inject the template of the warning about insecure setup as the + // first child of each page + var insecure = $('#template-insecure').html(); + $('[data-role="page"]>[data-role="content"]').prepend(insecure); + + currentPage = $.mobile.pageData.active; + currentUuid = $.mobile.pageData.uuid; + if (!(typeof currentPage === 'undefined') && + !(typeof currentUuid === 'undefined') && + currentPage.length > 0 && + currentUuid.length > 0) + { + Refresh(); + } +}); + +function GetAuthorizationTokensFromUrl() { + var urlVariables = window.location.search.substring(1).split('&'); + var dict = {}; + + for (var i = 0; i < urlVariables.length; i++) { + var split = urlVariables[i].split('='); + + if (split.length == 2 && (split[0] == "token" || split[0] == "auth-token" || split[0] == "authorization")) { + dict[split[0]] = split[1]; + } + } + return dict; +}; + +var authorizationTokens = GetAuthorizationTokensFromUrl(); + +/* Copy the authoziation toekn from the url search parameters into HTTP headers in every request to the Rest API. +Thanks to this behaviour, you may specify a ?token=xxx in your url and this will be passed +as the "token" header in every request to the API allowing you to use the authorization plugin */ +$.ajaxSetup( + { + headers : authorizationTokens + } +); + + +function ParseDicomDate(s) +{ + y = parseInt(s.substr(0, 4), 10); + m = parseInt(s.substr(4, 2), 10) - 1; + d = parseInt(s.substr(6, 2), 10); + + if (y == null || m == null || d == null || + !isFinite(y) || !isFinite(m) || !isFinite(d)) + { + return null; + } + + if (y < 1900 || y > 2100 || + m < 0 || m >= 12 || + d <= 0 || d >= 32) + { + return null; + } + + return new Date(y, m, d); +} + + +function FormatDicomDate(s) +{ + if (s == undefined) + return "No date"; + + var d = ParseDicomDate(s); + if (d == null) + return '?'; + else + return d.toString('dddd, MMMM d, yyyy'); +} + +function FormatFloatSequence(s) +{ + if (s == undefined || s.length == 0) + return "-"; + + if (s.indexOf("\\") == -1) + return s; + + var oldValues = s.split("\\"); + var newValues = []; + for (var i = 0; i < oldValues.length; i++) + { + newValues.push(parseFloat(oldValues[i]).toFixed(3)); + } + return newValues.join("\\"); +} + +function Sort(arr, fieldExtractor, isInteger, reverse) +{ + var defaultValue; + if (isInteger) + defaultValue = 0; + else + defaultValue = ''; + + arr.sort(function(a, b) { + var ta = fieldExtractor(a); + var tb = fieldExtractor(b); + var order; + + if (ta == undefined) + ta = defaultValue; + + if (tb == undefined) + tb = defaultValue; + + if (isInteger) + { + ta = parseInt(ta, 10); + tb = parseInt(tb, 10); + order = ta - tb; + } + else + { + if (ta < tb) + order = -1; + else if (ta > tb) + order = 1; + else + order = 0; + } + + if (reverse) + return -order; + else + return order; + }); +} + + +function GetMainDicomTag(mainDicomTags, tag) +{ + if (tag in mainDicomTags) { + return mainDicomTags[tag].Value; + } else { + return ''; + } +} + + +function SortOnDicomTag(arr, tag, isInteger, reverse) +{ + return Sort(arr, function(a) { + return GetMainDicomTag(a.MainDicomTags, tag); + }, isInteger, reverse); +} + + + +function GetResource(uri, callback) +{ + $.ajax({ + url: '..' + uri, + dataType: 'json', + async: false, + cache: false, + success: function(s) { + callback(s); + } + }); +} + + +function CompleteFormatting(node, link, isReverse, count) +{ + if (count != null) + { + node = node.add($('') + .addClass('ui-li-count') + .text(count)); + } + + if (link != null && + link) + { + node = $('').attr('href', link).append(node); + + if (isReverse) + node.attr('data-direction', 'reverse') + } + + node = $('
  • ').append(node); + + if (isReverse) + node.attr('data-icon', 'back'); + + return node; +} + + +function FormatMainDicomTags(target, tags, tagsToIgnore) +{ + var v; + + for (var i in tags) + { + if (tagsToIgnore.indexOf(i) == -1) + { + v = GetMainDicomTag(tags, i); + + if (i == PATIENT_BIRTH_DATE || + i == STUDY_DATE || + i == SERIES_DATE || + i == INSTANCE_CREATION_DATE) + { + v = FormatDicomDate(v); + } + else if (i == STUDY_INSTANCE_UID || + i == SERIES_INSTANCE_UID || + i == SOP_INSTANCE_UID) + { + // Possibly split a long UID + // v = '' + s.substr(0, s.length / 2) + '' + s.substr(s.length / 2, s.length - s.length / 2) + ''; + } + else if (i == IMAGE_POSITION_PATIENT || + i == IMAGE_ORIENTATION_PATIENT) + { + v = FormatFloatSequence(v); + } + + target.append($('

    ') + .text(tags[i].Name + ': ') + .append($('').text(v))); + } + } +} + + +function FormatPatient(patient, link, isReverse) +{ + var node = $('

    ').append($('

    ').text(GetMainDicomTag(patient.MainDicomTags, PATIENT_NAME))); + + FormatMainDicomTags(node, patient.MainDicomTags, [ + PATIENT_NAME + //, OTHER_PATIENT_IDS + ]); + + return CompleteFormatting(node, link, isReverse, patient.Studies.length); +} + + + +function FormatStudy(study, link, isReverse, includePatient) +{ + var label; + var node; + + if (includePatient) { + label = study.Label; + } else { + label = GetMainDicomTag(study.MainDicomTags, STUDY_DESCRIPTION); + } + + node = $('
    ').append($('

    ').text(label)); + + if (includePatient) { + FormatMainDicomTags(node, study.PatientMainDicomTags, [ + PATIENT_NAME + ]); + } + + FormatMainDicomTags(node, study.MainDicomTags, [ + STUDY_DESCRIPTION, + STUDY_TIME + ]); + + return CompleteFormatting(node, link, isReverse, study.Series.length); +} + + + +function FormatSeries(series, link, isReverse) +{ + var c; + var node; + + if (series.ExpectedNumberOfInstances == null || + series.Instances.length == series.ExpectedNumberOfInstances) + { + c = series.Instances.length; + } + else + { + c = series.Instances.length + '/' + series.ExpectedNumberOfInstances; + } + + node = $('
    ') + .append($('

    ').text(GetMainDicomTag(series.MainDicomTags, SERIES_DESCRIPTION))) + .append($('

    ').append($('') + .text('Status: ') + .append($('').text(series.Status)))); + + FormatMainDicomTags(node, series.MainDicomTags, [ + SERIES_DESCRIPTION, + SERIES_TIME, + MANUFACTURER, + IMAGES_IN_ACQUISITION, + SERIES_DATE, + IMAGE_ORIENTATION_PATIENT + ]); + + return CompleteFormatting(node, link, isReverse, c); +} + + +function FormatInstance(instance, link, isReverse) +{ + var node = $('

    ').append($('

    ').text('Instance: ' + instance.IndexInSeries)); + + FormatMainDicomTags(node, instance.MainDicomTags, [ + ACQUISITION_NUMBER, + INSTANCE_NUMBER, + INSTANCE_CREATION_DATE, + INSTANCE_CREATION_TIME, + ]); + + return CompleteFormatting(node, link, isReverse); +} + + +$('[data-role="page"]').live('pagebeforeshow', function() { + $.ajax({ + url: '../system', + dataType: 'json', + async: false, + cache: false, + success: function(s) { + if (s.Name != "") { + $('.orthanc-name').empty(); + $('.orthanc-name').append($('') + .addClass('ui-link') + .attr('href', 'explorer.html') + .text(s.Name) + .append(' » ')); + } + + // New in Orthanc 1.5.8 + if ('IsHttpServerSecure' in s && + !s.IsHttpServerSecure) { + $('.warning-insecure').show(); + } else { + $('.warning-insecure').hide(); + } + + // New in Orthanc 1.12.0 + if ('HasLabels' in s && + s.HasLabels) { + $('#lookup-study-labels-div').show(); + } else { + $('#lookup-study-labels-div').hide(); + } + } + }); +}); + + + +$('#lookup').live('pagebeforeshow', function() { + // NB: "GenerateDicomDate()" is defined in "query-retrieve.js" + var target = $('#lookup-study-date'); + $('option', target).remove(); + target.append($('
  • Download attachment "' + key + '"
  • ') + } + } + target.listview('refresh'); + }); +} + + + +function RefreshLabels(nodeLabels, resourceLevel, resourceId) +{ + GetResource('/' + resourceLevel + '/' + resourceId + '/labels', function(labels) { + nodeLabels.empty(); + + if (labels.length > 0) { + nodeLabels.css('display', 'block'); + + for (var i = 0; i < labels.length; i++) { + var removeButton = $('