/*========================================================================= Program: gdcm Module: $RCSfile: gdcmFileHelper.cxx,v $ Language: C++ Date: $Date: 2006/10/18 13:40:18 $ Version: $Revision: 1.110 $ Copyright (c) CREATIS (Centre de Recherche et d'Applications en Traitement de l'Image). All rights reserved. See Doc/License.txt or http://www.creatis.insa-lyon.fr/Public/Gdcm/License.html for details. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notices for more information. =========================================================================*/ #include "gdcmFileHelper.h" #include "gdcmGlobal.h" #include "gdcmTS.h" #include "gdcmDocument.h" #include "gdcmDebug.h" #include "gdcmUtil.h" #include "gdcmSeqEntry.h" #include "gdcmSQItem.h" #include "gdcmDataEntry.h" #include "gdcmDocEntry.h" #include "gdcmFile.h" #include "gdcmPixelReadConvert.h" #include "gdcmPixelWriteConvert.h" #include "gdcmDocEntryArchive.h" #include "gdcmDictSet.h" #include "gdcmOrientation.h" #if defined(__BORLANDC__) #include // for memset #endif #include /* // ----------------------------- WARNING ------------------------- These lines will be moved to the document-to-be 'User's Guide' // To read an image, user needs a gdcm::File gdcm::File *f = new gdcm::File(fileName); // or (advanced) : // user may also decide he doesn't want to load some parts of the header gdcm::File *f = new gdcm::File(); f->SetFileName(fileName); f->SetLoadMode(LD_NOSEQ); // or f->SetLoadMode(LD_NOSHADOW); // or f->SetLoadMode(LD_NOSEQ | LD_NOSHADOW); // or f->SetLoadMode(LD_NOSHADOWSEQ); f->Load(); // user can now check some values std::string v = f->GetEntryValue(groupNb,ElementNb); // to get the pixels, user needs a gdcm::FileHelper gdcm::FileHelper *fh = new gdcm::FileHelper(f); // user may ask not to convert Palette to RGB uint8_t *pixels = fh->GetImageDataRaw(); int imageLength = fh->GetImageDataRawSize(); // He can now use the pixels, create a new image, ... uint8_t *userPixels = ... To re-write the image, user re-uses the gdcm::FileHelper fh->SetImageData( userPixels, userPixelsLength); fh->SetTypeToRaw(); // Even if it was possible to convert Palette to RGB // (WriteMode is set) fh->SetWriteTypeToDcmExpl(); // he wants Explicit Value Representation // Little Endian is the default // no other value is allowed (-->SetWriteType(ExplicitVR);) -->WriteType = ExplicitVR; fh->Write(newFileName); // overwrites the file, if any // or : fh->WriteDcmExplVR(newFileName); // ----------------------------- WARNING ------------------------- These lines will be moved to the document-to-be 'Developer's Guide' WriteMode : WMODE_RAW / WMODE_RGB WriteType : ImplicitVR, ExplicitVR, ACR, ACR_LIBIDO fh1->Write(newFileName); SetWriteFileTypeToImplicitVR() / SetWriteFileTypeToExplicitVR(); (modifies TransferSyntax) SetWriteToRaw(); / SetWriteToRGB(); (modifies, when necessary : photochromatic interpretation, samples per pixel, Planar configuration, bits allocated, bits stored, high bit -ACR 24 bits- Pixels element VR, pushes out the LUT ) CheckWriteIntegrity(); (checks user given pixels length) FileInternal->Write(fileName,WriteType) fp = opens file(fileName); ComputeGroup0002Length( ); BitsAllocated 12->16 RemoveEntry(palettes, etc) Document::WriteContent(fp, writetype); RestoreWrite(); (moves back to the File all the archived elements) RestoreWriteFileType(); (pushes back group 0002, with TransferSyntax) */ namespace gdcm { typedef std::map GroupHT; // Hash Table //------------------------------------------------------------------------- // Constructor / Destructor /** * \brief Constructor dedicated to deal with the *pixels* area of a ACR/DICOMV3 * file (gdcm::File only deals with the ... header) * Opens (in read only and when possible) an existing file and checks * for DICOM compliance. Returns NULL on failure. * It will be up to the user to load the pixels into memory * ( GetImageDataSize() + GetImageData() methods) * \note the in-memory representation of all available tags found in * the DICOM header is post-poned to first header information access. * This avoid a double parsing of public part of the header when * one sets an a posteriori shadow dictionary (efficiency can be * seen as a side effect). */ FileHelper::FileHelper( ) { FileInternal = File::New( ); Initialize(); } /** * \brief Constructor dedicated to deal with the *pixels* area of a ACR/DICOMV3 * file (File only deals with the ... header) * Opens (in read only and when possible) an existing file and checks * for DICOM compliance. Returns NULL on failure. * It will be up to the user to load the pixels into memory * ( GetImageDataSize() + GetImageData() methods) * \note the in-memory representation of all available tags found in * the DICOM header is post-poned to first header information access. * This avoid a double parsing of public part of the header when * user sets an a posteriori shadow dictionary (efficiency can be * seen as a side effect). * @param header already built Header */ FileHelper::FileHelper(File *header) { gdcmAssertMacro(header); FileInternal = header; FileInternal->Register(); Initialize(); if ( FileInternal->IsReadable() ) { PixelReadConverter->GrabInformationsFromFile( FileInternal, this ); } } /** * \brief canonical destructor * \note If the header (gdcm::File) was created by the FileHelper constructor, * it is destroyed by the FileHelper */ FileHelper::~FileHelper() { if ( PixelReadConverter ) { delete PixelReadConverter; } if ( PixelWriteConverter ) { delete PixelWriteConverter; } if ( Archive ) { delete Archive; } FileInternal->Unregister(); } //----------------------------------------------------------------------------- // Public /** * \brief Sets the LoadMode of the internal gdcm::File as a boolean string. * NO_SEQ, NO_SHADOW, NO_SHADOWSEQ ... (nothing more, right now) * WARNING : before using NO_SHADOW, be sure *all* your files * contain accurate values in the 0x0000 element (if any) * of *each* Shadow Group. The parser will fail if the size is wrong ! * @param loadMode Load mode to be used */ void FileHelper::SetLoadMode(int loadMode) { GetFile()->SetLoadMode( loadMode ); } /** * \brief Sets the LoadMode of the internal gdcm::File * @param fileName name of the file to be open */ void FileHelper::SetFileName(std::string const &fileName) { FileInternal->SetFileName( fileName ); } /** * \brief Loader * @return false if file cannot be open or no swap info was found, * or no tag was found. */ bool FileHelper::Load() { if ( !FileInternal->Load() ) return false; PixelReadConverter->GrabInformationsFromFile( FileInternal, this ); return true; } /** * \brief Accesses an existing DataEntry through it's (group, element) * and modifies it's content with the given value. * @param content new value (string) to substitute with * @param group group number of the Dicom Element to modify * @param elem element number of the Dicom Element to modify * \return false if DataEntry not found */ bool FileHelper::SetEntryString(std::string const &content, uint16_t group, uint16_t elem) { return FileInternal->SetEntryString(content, group, elem); } /** * \brief Accesses an existing DataEntry through it's (group, element) * and modifies it's content with the given value. * @param content new value (void* -> uint8_t*) to substitute with * @param lgth new value length * @param group group number of the Dicom Element to modify * @param elem element number of the Dicom Element to modify * \return false if DataEntry not found */ bool FileHelper::SetEntryBinArea(uint8_t *content, int lgth, uint16_t group, uint16_t elem) { return FileInternal->SetEntryBinArea(content, lgth, group, elem); } /** * \brief Modifies the value of a given DataEntry when it exists. * Creates it with the given value when unexistant. * @param content (string) value to be set * @param group Group number of the Entry * @param elem Element number of the Entry * \return pointer to the modified/created DataEntry (NULL when creation * failed). */ DataEntry *FileHelper::InsertEntryString(std::string const &content, uint16_t group, uint16_t elem, VRKey const &vr ) { return FileInternal->InsertEntryString(content, group, elem, vr); } /** * \brief Modifies the value of a given DataEntry when it exists. * Creates it with the given value when unexistant. * A copy of the binArea is made to be kept in the Document. * @param binArea (binary)value to be set * @param lgth new value length * @param group Group number of the Entry * @param elem Element number of the Entry * @param vr Value Represenation of the DataElement to be inserted * \return pointer to the modified/created DataEntry (NULL when creation * failed). */ DataEntry *FileHelper::InsertEntryBinArea(uint8_t *binArea, int lgth, uint16_t group, uint16_t elem, VRKey const &vr ) { return FileInternal->InsertEntryBinArea(binArea, lgth, group, elem, vr); } /** * \brief Adds an empty SeqEntry * (remove any existing entry with same group,elem) * @param group Group number of the Entry * @param elem Element number of the Entry * \return pointer to the created SeqEntry (NULL when creation * failed). */ SeqEntry *FileHelper::InsertSeqEntry(uint16_t group, uint16_t elem) { return FileInternal->InsertSeqEntry(group, elem); } /** * \brief Get the size of the image data * If the image can be RGB (with a lut or by default), the size * corresponds to the RGB image * (use GetImageDataRawSize if you want to be sure to get *only* * the size of the pixels) * @return The image size */ size_t FileHelper::GetImageDataSize() { if ( PixelWriteConverter->GetUserData() ) { return PixelWriteConverter->GetUserDataSize(); } return PixelReadConverter->GetRGBSize(); } /** * \brief Get the size of the image data. * If the image could be converted to RGB using a LUT, * this transformation is not taken into account by GetImageDataRawSize * (use GetImageDataSize if you wish) * @return The raw image size */ size_t FileHelper::GetImageDataRawSize() { if ( PixelWriteConverter->GetUserData() ) { return PixelWriteConverter->GetUserDataSize(); } return PixelReadConverter->GetRawSize(); } /** * \brief brings pixels into memory : * - Allocates necessary memory, * - Reads the pixels from disk (uncompress if necessary), * - Transforms YBR pixels, if any, into RGB pixels, * - Transforms 3 planes R, G, B, if any, into a single RGB Plane * - Transforms single Grey plane + 3 Palettes into a RGB Plane * - Copies the pixel data (image[s]/volume[s]) to newly allocated zone. * @return Pointer to newly allocated pixel data. * (uint8_t is just for prototyping. feel free to cast) * NULL if alloc fails */ uint8_t *FileHelper::GetImageData() { if ( PixelWriteConverter->GetUserData() ) { return PixelWriteConverter->GetUserData(); } if ( ! GetRaw() ) { // If the decompression failed nothing can be done. return 0; } if ( FileInternal->HasLUT() && PixelReadConverter->BuildRGBImage() ) { return PixelReadConverter->GetRGB(); } else { // When no LUT or LUT conversion fails, return the Raw return PixelReadConverter->GetRaw(); } } /** * \brief brings pixels into memory : * - Allocates necessary memory, * - Transforms YBR pixels (if any) into RGB pixels * - Transforms 3 planes R, G, B (if any) into a single RGB Plane * - Copies the pixel data (image[s]/volume[s]) to newly allocated zone. * - DOES NOT transform Grey plane + 3 Palettes into a RGB Plane * @return Pointer to newly allocated pixel data. * (uint8_t is just for prototyping. feel free to cast) * NULL if alloc fails */ uint8_t *FileHelper::GetImageDataRaw () { return GetRaw(); } //#ifndef GDCM_LEGACY_REMOVE /* * \brief Useless function, since PixelReadConverter forces us * copy the Pixels anyway. * Reads the pixels from disk (uncompress if necessary), * Transforms YBR pixels, if any, into RGB pixels * Transforms 3 planes R, G, B, if any, into a single RGB Plane * Transforms single Grey plane + 3 Palettes into a RGB Plane * Copies at most MaxSize bytes of pixel data to caller allocated * memory space. * \warning This function allows people that want to build a volume * from an image stack *not to* have, first to get the image pixels, * and then move them to the volume area. * It's absolutely useless for any VTK user since vtk chooses * to invert the lines of an image, that is the last line comes first * (for some axis related reasons?). Hence he will have * to load the image line by line, starting from the end. * VTK users have to call GetImageData * * @param destination Address (in caller's memory space) at which the * pixel data should be copied * @param maxSize Maximum number of bytes to be copied. When MaxSize * is not sufficient to hold the pixel data the copy is not * executed (i.e. no partial copy). * @return On success, the number of bytes actually copied. Zero on * failure e.g. MaxSize is lower than necessary. */ /* size_t FileHelper::GetImageDataIntoVector (void *destination, size_t maxSize) { if ( ! GetRaw() ) { // If the decompression failed nothing can be done. return 0; } if ( FileInternal->HasLUT() && PixelReadConverter->BuildRGBImage() ) { if ( PixelReadConverter->GetRGBSize() > maxSize ) { gdcmWarningMacro( "Pixel data bigger than caller's expected MaxSize"); return 0; } memcpy( destination, (void*)PixelReadConverter->GetRGB(), PixelReadConverter->GetRGBSize() ); return PixelReadConverter->GetRGBSize(); } // Either no LUT conversion necessary or LUT conversion failed if ( PixelReadConverter->GetRawSize() > maxSize ) { gdcmWarningMacro( "Pixel data bigger than caller's expected MaxSize"); return 0; } memcpy( destination, (void *)PixelReadConverter->GetRaw(), PixelReadConverter->GetRawSize() ); return PixelReadConverter->GetRawSize(); } */ //#endif /** * \brief Points the internal pointer to the callers inData * image representation, BUT WITHOUT COPYING THE DATA. * 'image' Pixels are presented as C-like 2D arrays : line per line. * 'volume'Pixels are presented as C-like 3D arrays : plane per plane * \warning Since the pixels are not copied, it is the caller's responsability * not to deallocate its data before gdcm uses them (e.g. with * the Write() method ) * @param inData user supplied pixel area (uint8_t* is just for the compiler. * user is allowed to pass any kind of pixelsn since the size is * given in bytes) * @param expectedSize total image size, *in Bytes* */ void FileHelper::SetImageData(uint8_t *inData, size_t expectedSize) { SetUserData(inData, expectedSize); } /** * \brief Set the image data defined by the user * \warning When writting the file, this data are get as default data to write * @param inData user supplied pixel area (uint8_t* is just for the compiler. * user is allowed to pass any kind of pixels since the size is * given in bytes) * @param expectedSize total image size, *in Bytes* */ void FileHelper::SetUserData(uint8_t *inData, size_t expectedSize) { PixelWriteConverter->SetUserData(inData, expectedSize); } /** * \brief Get the image data defined by the user * \warning When writting the file, this data are get as default data to write */ uint8_t *FileHelper::GetUserData() { return PixelWriteConverter->GetUserData(); } /** * \brief Get the image data size defined by the user * \warning When writting the file, this data are get as default data to write */ size_t FileHelper::GetUserDataSize() { return PixelWriteConverter->GetUserDataSize(); } /** * \brief Get the image data from the file. * If a LUT is found, the data are expanded to be RGB */ uint8_t *FileHelper::GetRGBData() { return PixelReadConverter->GetRGB(); } /** * \brief Get the image data size from the file. * If a LUT is found, the data are expanded to be RGB */ size_t FileHelper::GetRGBDataSize() { return PixelReadConverter->GetRGBSize(); } /** * \brief Get the image data from the file. * Even when a LUT is found, the data are not expanded to RGB! */ uint8_t *FileHelper::GetRawData() { return PixelReadConverter->GetRaw(); } /** * \brief Get the image data size from the file. * Even when a LUT is found, the data are not expanded to RGB! */ size_t FileHelper::GetRawDataSize() { return PixelReadConverter->GetRawSize(); } /** * \brief Access to the underlying \ref PixelReadConverter RGBA LUT */ uint8_t* FileHelper::GetLutRGBA() { if ( PixelReadConverter->GetLutRGBA() ==0 ) PixelReadConverter->BuildLUTRGBA(); return PixelReadConverter->GetLutRGBA(); } /** * \brief Access to the underlying \ref PixelReadConverter RGBA LUT Item Number */ int FileHelper::GetLutItemNumber() { return PixelReadConverter->GetLutItemNumber(); } /** * \brief Access to the underlying \ref PixelReadConverter RGBA LUT Item Size */ int FileHelper::GetLutItemSize() { return PixelReadConverter->GetLutItemSize(); } /** * \brief Writes on disk A SINGLE Dicom file * NO test is performed on processor "Endiannity". * It's up to the user to call his Reader properly * @param fileName name of the file to be created * (any already existing file is over written) * @return false if write fails */ bool FileHelper::WriteRawData(std::string const &fileName) { std::ofstream fp1(fileName.c_str(), std::ios::out | std::ios::binary ); if (!fp1) { gdcmWarningMacro( "Fail to open (write) file:" << fileName.c_str()); return false; } if ( PixelWriteConverter->GetUserData() ) { fp1.write( (char *)PixelWriteConverter->GetUserData(), PixelWriteConverter->GetUserDataSize() ); } else if ( PixelReadConverter->GetRGB() ) { fp1.write( (char *)PixelReadConverter->GetRGB(), PixelReadConverter->GetRGBSize()); } else if ( PixelReadConverter->GetRaw() ) { fp1.write( (char *)PixelReadConverter->GetRaw(), PixelReadConverter->GetRawSize()); } else { gdcmErrorMacro( "Nothing written." ); } fp1.close(); return true; } /** * \brief Writes on disk A SINGLE Dicom file, * using the Implicit Value Representation convention * NO test is performed on processor "Endianity". * @param fileName name of the file to be created * (any already existing file is overwritten) * @return false if write fails */ bool FileHelper::WriteDcmImplVR (std::string const &fileName) { SetWriteTypeToDcmImplVR(); return Write(fileName); } /** * \brief Writes on disk A SINGLE Dicom file, * using the Explicit Value Representation convention * NO test is performed on processor "Endiannity". * @param fileName name of the file to be created * (any already existing file is overwritten) * @return false if write fails */ bool FileHelper::WriteDcmExplVR (std::string const &fileName) { SetWriteTypeToDcmExplVR(); return Write(fileName); } /** * \brief Writes on disk A SINGLE Dicom file, * using the ACR-NEMA convention * NO test is performed on processor "Endiannity". * (a l'attention des logiciels cliniques * qui ne prennent en entrée QUE des images ACR ... * \warning if a DICOM_V3 header is supplied, * groups < 0x0008 and shadow groups are ignored * \warning NO TEST is performed on processor "Endiannity". * @param fileName name of the file to be created * (any already existing file is overwritten) * @return false if write fails */ bool FileHelper::WriteAcr (std::string const &fileName) { SetWriteTypeToAcr(); return Write(fileName); } /** * \brief Writes on disk A SINGLE Dicom file, * @param fileName name of the file to be created * (any already existing file is overwritten) * @return false if write fails */ bool FileHelper::Write(std::string const &fileName) { CheckMandatoryElements(); //called once, here ! bool flag = false; DocEntry *e; switch(WriteType) { case ImplicitVR: SetWriteFileTypeToImplicitVR(); break; case Unknown: // should never happen; ExplicitVR is the default value case ExplicitVR: // User should ask gdcm to write an image in Explicit VR mode // only when he is sure *all* the VR of *all* the DataElements is known. // i.e : when there are *only* Public Groups // or *all* the Shadow Groups are fully described in the relevant Shadow // Dictionnary // Let's just *dream* about it; *never* trust a user ! // We turn to Implicit VR if at least the VR of one element is unknown. e = FileInternal->GetFirstEntry(); while (e != 0) { if (e->GetVR() == " ") { SetWriteTypeToDcmImplVR(); SetWriteFileTypeToImplicitVR(); flag = true; break; } e = FileInternal->GetNextEntry(); } if (!flag) { SetWriteFileTypeToExplicitVR(); } break; SetWriteFileTypeToExplicitVR(); // to see JPRx break; case ACR: case ACR_LIBIDO: // NOTHING is done here just for LibIDO. // Just to avoid further trouble if user creates a file ex-nihilo, // wants to write it as an ACR-NEMA file, // and forgets to create any Entry belonging to group 0008 // (shame on him !) // We add Recognition Code (RET) if ( ! FileInternal->GetDataEntry(0x0008, 0x0010) ) FileInternal->InsertEntryString("ACR-NEMA V1.0 ", 0x0008, 0x0010, "LO"); SetWriteFileTypeToACR(); // SetWriteFileTypeToImplicitVR(); // ACR IS implicit VR ! break; /// \todo FIXME : JPEG may be either ExplicitVR or ImplicitVR case JPEG: SetWriteFileTypeToJPEG(); break; case JPEG2000: SetWriteFileTypeToJPEG2000(); break; } // -------------------------------------------------------------- // Special Patch to allow gdcm to re-write ACR-LibIDO formated images // // if recognition code tells us we dealt with a LibIDO image // we reproduce on disk the switch between lineNumber and columnNumber // just before writting ... /// \todo the best trick would be *change* the recognition code /// but pb expected if user deals with, e.g. COMPLEX images if ( WriteType == ACR_LIBIDO ) { SetWriteToLibido(); } else { SetWriteToNoLibido(); } // ----------------- End of Special Patch ---------------- switch(WriteMode) { case WMODE_RAW : SetWriteToRaw(); // modifies and pushes to the archive, when necessary break; case WMODE_RGB : SetWriteToRGB(); // modifies and pushes to the archive, when necessary break; } bool check = CheckWriteIntegrity(); // verifies length if (WriteType == JPEG || WriteType == JPEG2000) check = true; if (check) { check = FileInternal->Write(fileName,WriteType); } RestoreWrite(); // RestoreWriteFileType(); // RestoreWriteMandatory(); // -------------------------------------------------------------- // Special Patch to allow gdcm to re-write ACR-LibIDO formated images // // ...and we restore the header to be Dicom Compliant again // just after writting RestoreWriteOfLibido(); // ----------------- End of Special Patch ---------------- return check; } //----------------------------------------------------------------------------- // Protected /** * \brief Checks the write integrity * * The tests made are : * - verify the size of the image to write with the possible write * when the user set an image data * @return true if check is successfull */ bool FileHelper::CheckWriteIntegrity() { if ( PixelWriteConverter->GetUserData() ) { int numberBitsAllocated = FileInternal->GetBitsAllocated(); if ( numberBitsAllocated == 0 || numberBitsAllocated == 12 ) { gdcmWarningMacro( "numberBitsAllocated changed from " << numberBitsAllocated << " to 16 " << " for consistency purpose" ); numberBitsAllocated = 16; } size_t decSize = FileInternal->GetXSize() * FileInternal->GetYSize() * FileInternal->GetZSize() * FileInternal->GetTSize() * FileInternal->GetSamplesPerPixel() * ( numberBitsAllocated / 8 ); size_t rgbSize = decSize; if ( FileInternal->HasLUT() ) rgbSize = decSize * 3; switch(WriteMode) { case WMODE_RAW : if ( decSize!=PixelWriteConverter->GetUserDataSize() ) { gdcmWarningMacro( "Data size (Raw) is incorrect. Should be " << decSize << " / Found :" << PixelWriteConverter->GetUserDataSize() ); return false; } break; case WMODE_RGB : if ( rgbSize!=PixelWriteConverter->GetUserDataSize() ) { gdcmWarningMacro( "Data size (RGB) is incorrect. Should be " << decSize << " / Found " << PixelWriteConverter->GetUserDataSize() ); return false; } break; } } return true; } /** * \brief Updates the File to write RAW data (as opposed to RGB data) * (modifies, when necessary, photochromatic interpretation, * bits allocated, Pixels element VR) */ void FileHelper::SetWriteToRaw() { if ( FileInternal->GetNumberOfScalarComponents() == 3 && !FileInternal->HasLUT() ) { SetWriteToRGB(); } else { DataEntry *photInt = CopyDataEntry(0x0028,0x0004,"CS"); if (FileInternal->HasLUT() ) { photInt->SetString("PALETTE COLOR "); } else { photInt->SetString("MONOCHROME2 "); } PixelWriteConverter->SetReadData(PixelReadConverter->GetRaw(), PixelReadConverter->GetRawSize()); std::string vr = "OB"; if ( FileInternal->GetBitsAllocated()>8 ) vr = "OW"; if ( FileInternal->GetBitsAllocated()==24 ) // For RGB ACR files vr = "OB"; // For non RAW data. Mainly JPEG if( WriteType == JPEG || WriteType == JPEG2000) { vr = "OW"; } DataEntry *pixel = CopyDataEntry(GetFile()->GetGrPixel(),GetFile()->GetNumPixel(),vr); pixel->SetFlag(DataEntry::FLAG_PIXELDATA); pixel->SetBinArea(PixelWriteConverter->GetData(),false); pixel->SetLength(PixelWriteConverter->GetDataSize()); Archive->Push(photInt); Archive->Push(pixel); photInt->Delete(); pixel->Delete(); } } /** * \brief Updates the File to write RGB data (as opposed to RAW data) * (modifies, when necessary, photochromatic interpretation, * samples per pixel, Planar configuration, * bits allocated, bits stored, high bit -ACR 24 bits- * Pixels element VR, pushes out the LUT, ) */ void FileHelper::SetWriteToRGB() { if ( FileInternal->GetNumberOfScalarComponents()==3 ) { PixelReadConverter->BuildRGBImage(); DataEntry *spp = CopyDataEntry(0x0028,0x0002,"US"); spp->SetString("3 "); DataEntry *planConfig = CopyDataEntry(0x0028,0x0006,"US"); planConfig->SetString("0 "); DataEntry *photInt = CopyDataEntry(0x0028,0x0004,"CS"); photInt->SetString("RGB "); if ( PixelReadConverter->GetRGB() ) { PixelWriteConverter->SetReadData(PixelReadConverter->GetRGB(), PixelReadConverter->GetRGBSize()); } else // Raw data { PixelWriteConverter->SetReadData(PixelReadConverter->GetRaw(), PixelReadConverter->GetRawSize()); } std::string vr = "OB"; if ( FileInternal->GetBitsAllocated()>8 ) vr = "OW"; if ( FileInternal->GetBitsAllocated()==24 ) // For RGB ACR files vr = "OB"; DataEntry *pixel = CopyDataEntry(GetFile()->GetGrPixel(),GetFile()->GetNumPixel(),vr); pixel->SetFlag(DataEntry::FLAG_PIXELDATA); pixel->SetBinArea(PixelWriteConverter->GetData(),false); pixel->SetLength(PixelWriteConverter->GetDataSize()); Archive->Push(spp); Archive->Push(planConfig); Archive->Push(photInt); Archive->Push(pixel); spp->Delete(); planConfig->Delete(); photInt->Delete(); pixel->Delete(); // Remove any LUT Archive->Push(0x0028,0x1101); Archive->Push(0x0028,0x1102); Archive->Push(0x0028,0x1103); Archive->Push(0x0028,0x1201); Archive->Push(0x0028,0x1202); Archive->Push(0x0028,0x1203); // push out Palette Color Lookup Table UID, if any Archive->Push(0x0028,0x1199); // For old '24 Bits' ACR-NEMA // Thus, we have a RGB image and the bits allocated = 24 and // samples per pixels = 1 (in the read file) if ( FileInternal->GetBitsAllocated()==24 ) { DataEntry *bitsAlloc = CopyDataEntry(0x0028,0x0100,"US"); bitsAlloc->SetString("8 "); DataEntry *bitsStored = CopyDataEntry(0x0028,0x0101,"US"); bitsStored->SetString("8 "); DataEntry *highBit = CopyDataEntry(0x0028,0x0102,"US"); highBit->SetString("7 "); Archive->Push(bitsAlloc); Archive->Push(bitsStored); Archive->Push(highBit); bitsAlloc->Delete(); bitsStored->Delete(); highBit->Delete(); } } else { SetWriteToRaw(); } } /** * \brief Restore the File write mode */ void FileHelper::RestoreWrite() { Archive->Restore(0x0028,0x0002); Archive->Restore(0x0028,0x0004); Archive->Restore(0x0028,0x0006); Archive->Restore(GetFile()->GetGrPixel(),GetFile()->GetNumPixel()); // For old ACR-NEMA (24 bits problem) Archive->Restore(0x0028,0x0100); Archive->Restore(0x0028,0x0101); Archive->Restore(0x0028,0x0102); // For the LUT Archive->Restore(0x0028,0x1101); Archive->Restore(0x0028,0x1102); Archive->Restore(0x0028,0x1103); Archive->Restore(0x0028,0x1201); Archive->Restore(0x0028,0x1202); Archive->Restore(0x0028,0x1203); // For the Palette Color Lookup Table UID Archive->Restore(0x0028,0x1203); // group 0002 may be pushed out for ACR-NEMA writting purposes Archive->Restore(0x0002,0x0000); Archive->Restore(0x0002,0x0001); Archive->Restore(0x0002,0x0002); Archive->Restore(0x0002,0x0003); Archive->Restore(0x0002,0x0010); Archive->Restore(0x0002,0x0012); Archive->Restore(0x0002,0x0013); Archive->Restore(0x0002,0x0016); Archive->Restore(0x0002,0x0100); Archive->Restore(0x0002,0x0102); } /** * \brief Pushes out the whole group 0002 * FIXME : better, set a flag to tell the writer not to write it ... * FIXME : method should probably have an other name ! * SetWriteFileTypeToACR is NOT opposed to * SetWriteFileTypeToExplicitVR and SetWriteFileTypeToImplicitVR */ void FileHelper::SetWriteFileTypeToACR() { Archive->Push(0x0002,0x0000); Archive->Push(0x0002,0x0001); Archive->Push(0x0002,0x0002); Archive->Push(0x0002,0x0003); Archive->Push(0x0002,0x0010); Archive->Push(0x0002,0x0012); Archive->Push(0x0002,0x0013); Archive->Push(0x0002,0x0016); Archive->Push(0x0002,0x0100); Archive->Push(0x0002,0x0102); } /** * \brief Sets in the File the TransferSyntax to 'JPEG2000' */ void FileHelper::SetWriteFileTypeToJPEG2000() { std::string ts = Util::DicomString( Global::GetTS()->GetSpecialTransferSyntax(TS::JPEG2000Lossless) ); DataEntry *tss = CopyDataEntry(0x0002,0x0010,"UI"); tss->SetString(ts); Archive->Push(tss); tss->Delete(); } /** * \brief Sets in the File the TransferSyntax to 'JPEG' */ void FileHelper::SetWriteFileTypeToJPEG() { std::string ts = Util::DicomString( Global::GetTS()->GetSpecialTransferSyntax(TS::JPEGBaselineProcess1) ); DataEntry *tss = CopyDataEntry(0x0002,0x0010,"UI"); tss->SetString(ts); Archive->Push(tss); tss->Delete(); } /** * \brief Sets in the File the TransferSyntax to 'Explicit VR Little Endian" */ void FileHelper::SetWriteFileTypeToExplicitVR() { std::string ts = Util::DicomString( Global::GetTS()->GetSpecialTransferSyntax(TS::ExplicitVRLittleEndian) ); DataEntry *tss = CopyDataEntry(0x0002,0x0010,"UI"); tss->SetString(ts); Archive->Push(tss); tss->Delete(); } /** * \brief Sets in the File the TransferSyntax to 'Implicit VR Little Endian" */ void FileHelper::SetWriteFileTypeToImplicitVR() { std::string ts = Util::DicomString( Global::GetTS()->GetSpecialTransferSyntax(TS::ImplicitVRLittleEndian) ); DataEntry *tss = CopyDataEntry(0x0002,0x0010,"UI"); tss->SetString(ts); Archive->Push(tss); tss->Delete(); } /** * \brief Restore in the File the initial group 0002 */ void FileHelper::RestoreWriteFileType() { } /** * \brief Set the Write not to Libido format */ void FileHelper::SetWriteToLibido() { DataEntry *oldRow = FileInternal->GetDataEntry(0x0028, 0x0010); DataEntry *oldCol = FileInternal->GetDataEntry(0x0028, 0x0011); if ( oldRow && oldCol ) { std::string rows, columns; //DataEntry *newRow=DataEntry::New(oldRow->GetDictEntry()); //DataEntry *newCol=DataEntry::New(oldCol->GetDictEntry()); DataEntry *newRow=DataEntry::New(0x0028, 0x0010, "US"); DataEntry *newCol=DataEntry::New(0x0028, 0x0011, "US"); newRow->Copy(oldCol); newCol->Copy(oldRow); newRow->SetString(oldCol->GetString()); newCol->SetString(oldRow->GetString()); Archive->Push(newRow); Archive->Push(newCol); newRow->Delete(); newCol->Delete(); } DataEntry *libidoCode = CopyDataEntry(0x0008,0x0010,"LO"); libidoCode->SetString("ACRNEMA_LIBIDO_1.1"); Archive->Push(libidoCode); libidoCode->Delete(); } /** * \brief Set the Write not to No Libido format */ void FileHelper::SetWriteToNoLibido() { DataEntry *recCode = FileInternal->GetDataEntry(0x0008,0x0010); if ( recCode ) { if ( recCode->GetString() == "ACRNEMA_LIBIDO_1.1" ) { DataEntry *libidoCode = CopyDataEntry(0x0008,0x0010,"LO"); libidoCode->SetString(""); Archive->Push(libidoCode); libidoCode->Delete(); } } } /** * \brief Restore the Write format */ void FileHelper::RestoreWriteOfLibido() { Archive->Restore(0x0028,0x0010); Archive->Restore(0x0028,0x0011); Archive->Restore(0x0008,0x0010); // Restore 'LibIDO-special' entries, if any Archive->Restore(0x0028,0x0015); Archive->Restore(0x0028,0x0016); Archive->Restore(0x0028,0x0017); Archive->Restore(0x0028,0x00199); } /** * \brief Duplicates a DataEntry or creates it. * @param group Group number of the Entry * @param elem Element number of the Entry * @param vr Value Representation of the Entry * \return pointer to the new Bin Entry (NULL when creation failed). */ DataEntry *FileHelper::CopyDataEntry(uint16_t group, uint16_t elem, const VRKey &vr) { DocEntry *oldE = FileInternal->GetDocEntry(group, elem); DataEntry *newE; if ( oldE && vr != GDCM_VRUNKNOWN ) if ( oldE->GetVR() != vr ) oldE = NULL; if ( oldE ) { //newE = DataEntry::New(oldE->GetDictEntry()); newE = DataEntry::New(group, elem, vr); newE->Copy(oldE); } else { newE = GetFile()->NewDataEntry(group, elem, vr); } return newE; } /** * \brief This method is called automatically, just before writting * in order to produce a 'True Dicom V3' image. * * We cannot know *how* the user made the File : * (reading an old ACR-NEMA file or a not very clean DICOM file ...) * Just before writting : * - we check the Entries * - we create the mandatory entries if they are missing * - we modify the values if necessary * - we push the sensitive entries to the Archive * The writing process will restore the entries as they where before * entering FileHelper::CheckMandatoryElements, so the user will always * see the entries just as they were before he decided to write. * * \note * - Entries whose type is 1 are mandatory, with a mandatory value * - Entries whose type is 1c are mandatory-inside-a-Sequence, * with a mandatory value * - Entries whose type is 2 are mandatory, with an optional value * - Entries whose type is 2c are mandatory-inside-a-Sequence, * with an optional value * - Entries whose type is 3 are optional * * \todo * - warn the user if we had to add some entries : * even if a mandatory entry is missing, we add it, with a default value * (we don't want to give up the writting process if user forgot to * specify Lena's Patient ID, for instance ...) * - read the whole PS 3.3 Part of DICOM (890 pages) * and write a *full* checker (probably one method per Modality ...) * Any contribution is welcome. * - write a user callable full checker, to allow post reading * and/or pre writting image consistency check. */ /* ------------------------------------------------------------------------------------- To be moved to User's guide / WIKI ? We have to deal with 4 *very* different cases : -1) user created ex nihilo his own image and wants to write it as a Dicom image. USER_OWN_IMAGE -2) user modified the pixels of an existing image. FILTERED_IMAGE -3) user created a new image, using a set of existing images (eg MIP, MPR, cartography image) CREATED_IMAGE -4) user modified/added some tags *without processing* the pixels (anonymization.. UNMODIFIED_PIXELS_IMAGE -Probabely some more to be added gdcm::FileHelper::CheckMandatoryElements() deals automatically with these cases. 1)2)3)4) 0008 0012 Instance Creation Date 0008 0013 Instance Creation Time 0008 0018 SOP Instance UID are *always* created with the current values; user has *no* possible intervention on them. 'Serie Instance UID'(0x0020,0x000e) 'Study Instance UID'(0x0020,0x000d) are kept as is if already exist, created if it doesn't. The user is allowed to create his own Series/Studies, keeping the same 'Serie Instance UID' / 'Study Instance UID' for various images Warning : The user shouldn't add any image to a 'Manufacturer Serie' but there is no way no to allow him to do that None of the 'shadow elements' are droped out. 1) 'Conversion Type (0x0008,0x0064) is forced to 'SYN' (Synthetic Image). 1)3) 'Media Storage SOP Class UID' (0x0002,0x0002) 'SOP Class UID' (0x0008,0x0016) are set to [Secondary Capture Image Storage] 'Image Type' (0x0008,0x0008) is forced to "DERIVED\PRIMARY" Conversion Type (0x0008,0x0064) is forced to 'SYN' (Synthetic Image) 2)4) If 'SOP Class UID' exists in the native image ('true DICOM' image) we create the 'Source Image Sequence' SeqEntry (0x0008, 0x2112) --> 'Referenced SOP Class UID' (0x0008, 0x1150) whose value is the original 'SOP Class UID' --> 'Referenced SOP Instance UID' (0x0008, 0x1155) whose value is the original 'SOP Class UID' 3) TODO : find a trick to allow user to pass to the writter the list of the Dicom images or the Series, (or the Study ?) he used to created his image (MIP, MPR, cartography image, ...) These info should be stored (?) 0008 1110 SQ 1 Referenced Study Sequence 0008 1115 SQ 1 Referenced Series Sequence 0008 1140 SQ 1 Referenced Image Sequence 4) When user *knows* he didn't modified the pixels, we keep some informations unchanged : 'Media Storage SOP Class UID' (0x0002,0x0002) 'SOP Class UID' (0x0008,0x0016) 'Image Type' (0x0008,0x0008) 'Conversion Type' (0x0008,0x0064) Bellow follows the full description (hope so !) of the consistency checks performed by gdcm::FileHelper::CheckMandatoryElements() -->'Media Storage SOP Class UID' (0x0002,0x0002) -->'SOP Class UID' (0x0008,0x0016) are defaulted to [Secondary Capture Image Storage] --> 'Image Type' (0x0008,0x0008) is forced to "DERIVED\PRIMARY" (The written image is no longer an 'ORIGINAL' one) Except if user knows he didn't modify the image (e.g. : he just anonymized the file) --> Conversion Type (0x0008,0x0064) is defaulted to 'SYN' (Synthetic Image) when *he* knows he created his own image ex nihilo --> 'Modality' (0x0008,0x0060) is defaulted to "OT" (other) if missing. (a fully user created image belongs to *no* modality) --> 'Media Storage SOP Instance UID' (0x0002,0x0003) --> 'Implementation Class UID' (0x0002,0x0012) are automatically generated; no user intervention possible --> 'Serie Instance UID'(0x0020,0x000e) --> 'Study Instance UID'(0x0020,0x000d) are kept as is if already exist created if it doesn't. The user is allowed to create his own Series/Studies, keeping the same 'Serie Instance UID' / 'Study Instance UID' for various images Warning : The user shouldn't add any image to a 'Manufacturer Serie' but there is no way no to allowed him to do that --> If 'SOP Class UID' exists in the native image ('true DICOM' image) we create the 'Source Image Sequence' SeqEntry (0x0008, 0x2112) --> 'Referenced SOP Class UID' (0x0008, 0x1150) whose value is the original 'SOP Class UID' --> 'Referenced SOP Instance UID' (0x0008, 0x1155) whose value is the original 'SOP Class UID' --> Bits Stored, Bits Allocated, Hight Bit Position are checked for consistency --> Pixel Spacing (0x0028,0x0030) is defaulted to "1.0\1.0" --> Samples Per Pixel (0x0028,0x0002) is defaulted to 1 (grayscale) --> Imager Pixel Spacing (0x0018,0x1164) : defaulted to Pixel Spacing value --> Instance Creation Date, Instance Creation Time are forced to current Date and Time --> Study Date, Study Time are defaulted to current Date and Time (they remain unchanged if they exist) --> Patient Orientation : (0x0020,0x0020), if not present, is deduced from Image Orientation (Patient) : (0020|0037) or from Image Orientation (RET) : (0020 0035) --> Study ID, Series Number, Instance Number, Patient Orientation (Type 2) are created, with empty value if there are missing. --> Manufacturer, Institution Name, Patient's Name, (Type 2) are defaulted with a 'gdcm' value. --> Patient ID, Patient's Birth Date, Patient's Sex, (Type 2) --> Referring Physician's Name (Type 2) are created, with empty value if there are missing. -------------------------------------------------------------------------------------*/ void FileHelper::CheckMandatoryElements() { std::string sop = Util::CreateUniqueUID(); // --------------------- For Meta Elements --------------------- // just to remember : 'official' 0002 group if ( WriteType != ACR && WriteType != ACR_LIBIDO ) { // Group 000002 (Meta Elements) already pushed out //0002 0000 UL 1 Meta Group Length //0002 0001 OB 1 File Meta Information Version //0002 0002 UI 1 Media Stored SOP Class UID //0002 0003 UI 1 Media Stored SOP Instance UID //0002 0010 UI 1 Transfer Syntax UID //0002 0012 UI 1 Implementation Class UID //0002 0013 SH 1 Implementation Version Name //0002 0016 AE 1 Source Application Entity Title //0002 0100 UI 1 Private Information Creator //0002 0102 OB 1 Private Information // Push out 'ACR-NEMA-special' entries, if any Archive->Push(0x0008,0x0001); // Length to End Archive->Push(0x0008,0x0010); // Recognition Code Archive->Push(0x0028,0x0005); // Image Dimension // Create them if not found // Always modify the value // Push the entries to the archive. CopyMandatoryEntry(0x0002,0x0000,"0","UL"); DataEntry *e_0002_0001 = CopyDataEntry(0x0002,0x0001, "OB"); e_0002_0001->SetBinArea((uint8_t*)Util::GetFileMetaInformationVersion(), false); e_0002_0001->SetLength(2); Archive->Push(e_0002_0001); e_0002_0001->Delete(); if ( ContentType == FILTERED_IMAGE || ContentType == UNMODIFIED_PIXELS_IMAGE) { // we keep the original 'Media Storage SOP Class UID', we default it if missing CheckMandatoryEntry(0x0002,0x0002,"1.2.840.10008.5.1.4.1.1.7","UI"); } else { // It's *not* an image comming straight from a source. We force // 'Media Storage SOP Class UID' --> [Secondary Capture Image Storage] CopyMandatoryEntry(0x0002,0x0002,"1.2.840.10008.5.1.4.1.1.7","UI"); } // 'Media Storage SOP Instance UID' CopyMandatoryEntry(0x0002,0x0003,sop,"UI"); // 'Implementation Class UID' // FIXME : in all examples we have, 0x0002,0x0012 is not so long : // seems to be Root UID + 4 digits (?) CopyMandatoryEntry(0x0002,0x0012,Util::CreateUniqueUID(),"UI"); // 'Implementation Version Name' std::string version = "GDCM "; version += Util::GetVersion(); CopyMandatoryEntry(0x0002,0x0013,version,"SH"); } // --------------------- For DataSet --------------------- if ( ContentType != USER_OWN_IMAGE) // when it's not a user made image { gdcmDebugMacro( "USER_OWN_IMAGE (1)"); // If 'SOP Class UID' exists ('true DICOM' image) // we create the 'Source Image Sequence' SeqEntry // to hold informations about the Source Image DataEntry *e_0008_0016 = FileInternal->GetDataEntry(0x0008, 0x0016); if ( e_0008_0016 ) { // Create 'Source Image Sequence' SeqEntry // SeqEntry *sis = SeqEntry::New ( // Global::GetDicts()->GetDefaultPubDict()->GetEntry(0x0008, 0x2112) ); SeqEntry *sis = SeqEntry::New (0x0008, 0x2112); SQItem *sqi = SQItem::New(1); // (we assume 'SOP Instance UID' exists too) // create 'Referenced SOP Class UID' // DataEntry *e_0008_1150 = DataEntry::New( // Global::GetDicts()->GetDefaultPubDict()->GetEntry(0x0008, 0x1150) ); DataEntry *e_0008_1150 = DataEntry::New(0x0008, 0x1150, "UI"); e_0008_1150->SetString( e_0008_0016->GetString()); sqi->AddEntry(e_0008_1150); e_0008_1150->Delete(); // create 'Referenced SOP Instance UID' DataEntry *e_0008_0018 = FileInternal->GetDataEntry(0x0008, 0x0018); // DataEntry *e_0008_1155 = DataEntry::New( // Global::GetDicts()->GetDefaultPubDict()->GetEntry(0x0008, 0x1155) ); DataEntry *e_0008_1155 = DataEntry::New(0x0008, 0x1155, "UI"); e_0008_1155->SetString( e_0008_0018->GetString()); sqi->AddEntry(e_0008_1155); e_0008_1155->Delete(); sis->AddSQItem(sqi,1); sqi->Delete(); // temporarily replaces any previous 'Source Image Sequence' Archive->Push(sis); sis->Delete(); // FIXME : is 'Image Type' *really* depending on the presence of 'SOP Class UID'? if ( ContentType == FILTERED_IMAGE) // the user *knows* he just modified the pixels // the image is no longer an 'Original' one CopyMandatoryEntry(0x0008,0x0008,"DERIVED\\PRIMARY","CS"); } } if ( ContentType == FILTERED_IMAGE || ContentType == UNMODIFIED_PIXELS_IMAGE) { // we keep the original 'Media Storage SOP Class UID', we default it if missing (it should be present !) CheckMandatoryEntry(0x0008,0x0016,"1.2.840.10008.5.1.4.1.1.7","UI"); } else { // It's *not* an image comming straight from a source. We force // 'Media Storage SOP Class UID' --> [Secondary Capture Image Storage] CopyMandatoryEntry(0x0008,0x0016,"1.2.840.10008.5.1.4.1.1.7", "UI"); } Archive->Push(0x0028,0x005); // [Image Dimensions (RET) // Push out 'LibIDO-special' entries, if any Archive->Push(0x0028,0x0015); Archive->Push(0x0028,0x0016); Archive->Push(0x0028,0x0017); Archive->Push(0x0028,0x0198); // very old versions Archive->Push(0x0028,0x0199); // Replace deprecated 0028 0012 US Planes // by new 0028 0008 IS Number of Frames ///\todo : find if there is a rule! DataEntry *e_0028_0012 = FileInternal->GetDataEntry(0x0028, 0x0012); if ( e_0028_0012 ) { CopyMandatoryEntry(0x0028, 0x0008,e_0028_0012->GetString(),"IS"); Archive->Push(0x0028,0x0012); } // Deal with the pb of (Bits Stored = 12) // - we're gonna write the image as Bits Stored = 16 if ( FileInternal->GetEntryString(0x0028,0x0100) == "12") { CopyMandatoryEntry(0x0028,0x0100,"16","US"); } // Check if user wasn't drunk ;-) std::ostringstream s; // check 'Bits Allocated' vs decent values int nbBitsAllocated = FileInternal->GetBitsAllocated(); if ( nbBitsAllocated == 0 || nbBitsAllocated > 32 || ( nbBitsAllocated > 8 && nbBitsAllocated <16) ) { CopyMandatoryEntry(0x0028,0x0100,"16","US"); gdcmWarningMacro("(0028,0100) changed from " << nbBitsAllocated << " to 16 for consistency purpose"); nbBitsAllocated = 16; } // check 'Bits Stored' vs 'Bits Allocated' int nbBitsStored = FileInternal->GetBitsStored(); if ( nbBitsStored == 0 || nbBitsStored > nbBitsAllocated ) { s.str(""); s << nbBitsAllocated; CopyMandatoryEntry(0x0028,0x0101,s.str(),"US"); gdcmWarningMacro("(0028,0101) changed from " << nbBitsStored << " to " << nbBitsAllocated << " for consistency purpose" ); nbBitsStored = nbBitsAllocated; } // check 'Hight Bit Position' vs 'Bits Allocated' and 'Bits Stored' int highBitPosition = FileInternal->GetHighBitPosition(); if ( highBitPosition == 0 || highBitPosition > nbBitsAllocated-1 || highBitPosition < nbBitsStored-1 ) { s.str(""); s << nbBitsStored - 1; CopyMandatoryEntry(0x0028,0x0102,s.str(),"US"); gdcmWarningMacro("(0028,0102) changed from " << highBitPosition << " to " << nbBitsAllocated-1 << " for consistency purpose"); } // check Pixel Representation (default it as 0 -unsigned-) DataEntry *e_0028_0103 = FileInternal->GetDataEntry(0x0028, 0x0103); if ( !e_0028_0103 ) { gdcmWarningMacro("PixelRepresentation (0028,0103) is supposed to be mandatory"); CopyMandatoryEntry(0x0028, 0x0103,"0","US"); } else { int sign = (int)e_0028_0103->GetValue(0); if (sign !=1 && sign !=0) { gdcmWarningMacro("PixelRepresentation (0028,0103) is supposed to be =1 or =0"); CopyMandatoryEntry(0x0028, 0x0103,"0","US"); } } std::string pixelSpacing = FileInternal->GetEntryString(0x0028,0x0030); if ( pixelSpacing == GDCM_UNFOUND ) { pixelSpacing = "1.0\\1.0"; // if missing, Pixel Spacing forced to "1.0\1.0" CopyMandatoryEntry(0x0028,0x0030,pixelSpacing,"DS"); } // 'Imager Pixel Spacing' : defaulted to 'Pixel Spacing' // --> This one is the *legal* one ! if ( ContentType != USER_OWN_IMAGE) // we write it only when we are *sure* the image comes from // an imager (see also 0008,0x0064) CheckMandatoryEntry(0x0018,0x1164,pixelSpacing,"DS"); /* ///Exact meaning of RETired fields // See page 73 of ACR-NEMA_300-1988.pdf ! // 0020,0020 : Patient Orientation : Patient direction of the first row and column of the images. The first entry id the direction of the raws, given by the direction of the last pixel in the first row from the first pixel in tha row. the second entry is the direction of the columns, given by the direction of the last pixel in the first column from the first pixel in that column. L : Left, F : Feet, A : Anterior, P : Posterior. Up to 3 letters can be used in combination to indicate oblique planes. //0020,0030 Image Position (RET) x,y,z coordinates im mm of the first pixel in the image // 0020,0035 Image Orientation (RET) Direction cosines of the R axis of the image system with respect to the equipment coordinate axes x,y,z, followed by direction cosines of the C axis of the image system with respect to the same axes //0020,0050 Location An image location reference, standard for the modality (such as CT bed position), used to indicate position. Calculation of position for other purposes is only from (0020,0030) and (0020,0035) */ /* // if imagePositionPatient not found, default it with imagePositionRet, if any // if imageOrientationPatient not found, default it with imageOrientationRet, if any std::string imagePositionRet = FileInternal->GetEntryString(0x0020,0x0030); std::string imageOrientationRet = FileInternal->GetEntryString(0x0020,0x0035); std::string imagePositionPatient = FileInternal->GetEntryString(0x0020,0x0032); std::string imageOrientationPatient = FileInternal->GetEntryString(0x0020,0x0037); if( imagePositionPatient == GDCM_UNFOUND && imageOrientationPatient == GDCM_UNFOUND && imagePositionRet != GDCM_UNFOUND && imageOrientationRet != GDCM_UNFOUND) { CopyMandatoryEntry(0x0020, 0x0032,imagePositionRet,"DS"); Archive->Push(0x0020,0x0030); CopyMandatoryEntry(0x0020, 0x0037,imageOrientationRet,"DS"); Archive->Push(0x0020,0x0035); } */ // Samples Per Pixel (type 1) : default to grayscale CheckMandatoryEntry(0x0028,0x0002,"1","US"); // --- Check UID-related Entries --- // At the end, not to overwrite the original ones, // needed by 'Referenced SOP Instance UID', 'Referenced SOP Class UID' // 'SOP Instance UID' CopyMandatoryEntry(0x0008,0x0018,sop,"UI"); if ( ContentType == USER_OWN_IMAGE) { gdcmDebugMacro( "USER_OWN_IMAGE (2)"); // Conversion Type. // Other possible values are : // See PS 3.3, Page 408 // DV = Digitized Video // DI = Digital Interface // DF = Digitized Film // WSD = Workstation // SD = Scanned Document // SI = Scanned Image // DRW = Drawing // SYN = Synthetic Image CheckMandatoryEntry(0x0008,0x0064,"SYN","CS"); // Why not? } /* if ( ContentType == CREATED_IMAGE) { /// \todo : find a trick to pass the Media Storage SOP Instance UID of the images used to create the current image } */ // ---- The user will never have to take any action on the following ---- // new value for 'SOP Instance UID' //SetMandatoryEntry(0x0008,0x0018,Util::CreateUniqueUID()); // Instance Creation Date const std::string &date = Util::GetCurrentDate(); CopyMandatoryEntry(0x0008,0x0012,date,"DA"); // Instance Creation Time const std::string &time = Util::GetCurrentTime(); CopyMandatoryEntry(0x0008,0x0013,time,"TM"); // Study Date CheckMandatoryEntry(0x0008,0x0020,date,"DA"); // Study Time CheckMandatoryEntry(0x0008,0x0030,time,"TM"); // Accession Number //CopyMandatoryEntry(0x0008,0x0050,""); CheckMandatoryEntry(0x0008,0x0050,"","SH"); // ----- Add Mandatory Entries if missing --- // Entries whose type is 1 are mandatory, with a mandatory value // Entries whose type is 1c are mandatory-inside-a-Sequence, // with a mandatory value // Entries whose type is 2 are mandatory, with an optional value // Entries whose type is 2c are mandatory-inside-a-Sequence, // with an optional value // Entries whose type is 3 are optional // 'Study Instance UID' // Keep the value if exists // The user is allowed to create his own Study, // keeping the same 'Study Instance UID' for various images // The user may add images to a 'Manufacturer Study', // adding new Series to an already existing Study CheckMandatoryEntry(0x0020,0x000d,Util::CreateUniqueUID(),"UI"); // 'Serie Instance UID' // Keep the value if exists // The user is allowed to create his own Series, // keeping the same 'Serie Instance UID' for various images // The user shouldn't add any image to a 'Manufacturer Serie' // but there is no way no to prevent him for doing that CheckMandatoryEntry(0x0020,0x000e,Util::CreateUniqueUID(),"UI"); // Study ID CheckMandatoryEntry(0x0020,0x0010,"","SH"); // Series Number CheckMandatoryEntry(0x0020,0x0011,"","IS"); // Instance Number CheckMandatoryEntry(0x0020,0x0013,"","IS"); // Patient Orientation // Can be computed from (0020|0037) : Image Orientation (Patient) gdcm::Orientation *o = gdcm::Orientation::New(); std::string ori = o->GetOrientation ( FileInternal ); o->Delete(); if (ori != "\\" && ori != GDCM_UNFOUND) CheckMandatoryEntry(0x0020,0x0020,ori,"CS"); else CheckMandatoryEntry(0x0020,0x0020,"","CS"); // Default Patient Position to HFS CheckMandatoryEntry(0x0018,0x5100,"HFS","CS"); // Modality : if missing we set it to 'OTher' CheckMandatoryEntry(0x0008,0x0060,"OT","CS"); // Manufacturer : if missing we set it to 'GDCM Factory' CheckMandatoryEntry(0x0008,0x0070,"GDCM Factory","LO"); // Institution Name : if missing we set it to 'GDCM Hospital' CheckMandatoryEntry(0x0008,0x0080,"GDCM Hospital","LO"); // Patient's Name : if missing, we set it to 'GDCM^Patient' CheckMandatoryEntry(0x0010,0x0010,"GDCM^Patient","PN"); // Patient ID : some clinical softwares *demand* it although it's a 'type 2' entry. CheckMandatoryEntry(0x0010,0x0020,"gdcm ID","LO"); // Patient's Birth Date : 'type 2' entry -> must exist, value not mandatory CheckMandatoryEntry(0x0010,0x0030,"","DA"); // Patient's Sex :'type 2' entry -> must exist, value not mandatory CheckMandatoryEntry(0x0010,0x0040,"","CS"); // Referring Physician's Name :'type 2' entry -> must exist, value not mandatory CheckMandatoryEntry(0x0008,0x0090,"","PN"); /* // Deal with element 0x0000 (group length) of each group. // First stage : get all the different Groups GroupHT grHT; DocEntry *d = FileInternal->GetFirstEntry(); while(d) { grHT[d->GetGroup()] = 0; d=FileInternal->GetNextEntry(); } // Second stage : add the missing ones (if any) for (GroupHT::iterator it = grHT.begin(); it != grHT.end(); ++it) { CheckMandatoryEntry(it->first, 0x0000, "0"); } // Third stage : update all 'zero level' groups length */ } void FileHelper::CheckMandatoryEntry(uint16_t group,uint16_t elem,std::string value,const VRKey &vr ) { DataEntry *entry = FileInternal->GetDataEntry(group,elem); if ( !entry ) { //entry = DataEntry::New(Global::GetDicts()->GetDefaultPubDict()->GetEntry(group,elem)); entry = DataEntry::New(group,elem,vr); entry->SetString(value); Archive->Push(entry); entry->Delete(); } } /// \todo : what is it used for ? (FileHelper::SetMandatoryEntry) void FileHelper::SetMandatoryEntry(uint16_t group,uint16_t elem,std::string value,const VRKey &vr) { //DataEntry *entry = DataEntry::New(Global::GetDicts()->GetDefaultPubDict()->GetEntry(group,elem)); DataEntry *entry = DataEntry::New(group,elem,vr); entry->SetString(value); Archive->Push(entry); entry->Delete(); } void FileHelper::CopyMandatoryEntry(uint16_t group,uint16_t elem,std::string value,const VRKey &vr) { DataEntry *entry = CopyDataEntry(group,elem,vr); entry->SetString(value); Archive->Push(entry); entry->Delete(); } /** * \brief Restore in the File the initial group 0002 */ void FileHelper::RestoreWriteMandatory() { // group 0002 may be pushed out for ACR-NEMA writting purposes Archive->Restore(0x0002,0x0000); Archive->Restore(0x0002,0x0001); Archive->Restore(0x0002,0x0002); Archive->Restore(0x0002,0x0003); Archive->Restore(0x0002,0x0010); Archive->Restore(0x0002,0x0012); Archive->Restore(0x0002,0x0013); Archive->Restore(0x0002,0x0016); Archive->Restore(0x0002,0x0100); Archive->Restore(0x0002,0x0102); // FIXME : Check if none is missing ! Archive->Restore(0x0008,0x0012); Archive->Restore(0x0008,0x0013); Archive->Restore(0x0008,0x0016); Archive->Restore(0x0008,0x0018); Archive->Restore(0x0008,0x0060); Archive->Restore(0x0008,0x0070); Archive->Restore(0x0008,0x0080); Archive->Restore(0x0008,0x0090); Archive->Restore(0x0008,0x2112); Archive->Restore(0x0010,0x0010); Archive->Restore(0x0010,0x0030); Archive->Restore(0x0010,0x0040); Archive->Restore(0x0020,0x000d); Archive->Restore(0x0020,0x000e); } /** * \brief CallStartMethod */ void FileHelper::CallStartMethod() { Progress = 0.0f; Abort = false; CommandManager::ExecuteCommand(this,CMD_STARTPROGRESS); } /** * \brief CallProgressMethod */ void FileHelper::CallProgressMethod() { CommandManager::ExecuteCommand(this,CMD_PROGRESS); } /** * \brief CallEndMethod */ void FileHelper::CallEndMethod() { Progress = 1.0f; CommandManager::ExecuteCommand(this,CMD_ENDPROGRESS); } //----------------------------------------------------------------------------- // Private /** * \brief Factorization for various forms of constructors. */ void FileHelper::Initialize() { UserFunction = 0; ContentType = USER_OWN_IMAGE; WriteMode = WMODE_RAW; WriteType = ExplicitVR; PixelReadConverter = new PixelReadConvert; PixelWriteConverter = new PixelWriteConvert; Archive = new DocEntryArchive( FileInternal ); } /** * \brief Reads/[decompresses] the pixels, * *without* making RGB from Palette Colors * @return the pixels area, whatever its type * (uint8_t is just for prototyping : feel free to Cast it) */ uint8_t *FileHelper::GetRaw() { PixelReadConverter->SetUserFunction( UserFunction ); uint8_t *raw = PixelReadConverter->GetRaw(); if ( ! raw ) { // The Raw image migth not be loaded yet: std::ifstream *fp = FileInternal->OpenFile(); PixelReadConverter->ReadAndDecompressPixelData( fp ); if ( fp ) FileInternal->CloseFile(); raw = PixelReadConverter->GetRaw(); if ( ! raw ) { gdcmWarningMacro( "Read/decompress of pixel data apparently went wrong."); return 0; } } return raw; } //----------------------------------------------------------------------------- /** * \brief Prints the common part of DataEntry, SeqEntry * @param os ostream we want to print in * @param indent (unused) */ void FileHelper::Print(std::ostream &os, std::string const &) { FileInternal->SetPrintLevel(PrintLevel); FileInternal->Print(os); if ( FileInternal->IsReadable() ) { PixelReadConverter->SetPrintLevel(PrintLevel); PixelReadConverter->Print(os); } } //----------------------------------------------------------------------------- } // end namespace gdcm /* Probabely something to be added to use Rescale Slope/Intercept Have a look ,at ITK code ! // Internal function to rescale pixel according to Rescale Slope/Intercept template void RescaleFunction(TBuffer* buffer, TSource *source, double slope, double intercept, size_t size) { size /= sizeof(TSource); if (slope != 1.0 && intercept != 0.0) { // Duff's device. Instead of this code: // // for(unsigned int i=0; i 0); } } else if (slope == 1.0 && intercept != 0.0) { // Duff's device. Instead of this code: // // for(unsigned int i=0; i 0); } } else if (slope != 1.0 && intercept == 0.0) { // Duff's device. Instead of this code: // // for(unsigned int i=0; i 0); } } else { // Duff's device. Instead of this code: // // for(unsigned int i=0; i 0); } } } template void RescaleFunction(ImageIOBase::IOComponentType bufferType, void* buffer, TSource *source, double slope, double intercept, size_t size) { switch (bufferType) { case ImageIOBase::UCHAR: RescaleFunction( (unsigned char *)buffer, source, slope, intercept, size); break; case ImageIOBase::CHAR: RescaleFunction( (char *)buffer, source, slope, intercept, size); break; case ImageIOBase::USHORT: RescaleFunction( (unsigned short *)buffer, source, slope, intercept,size); break; case ImageIOBase::SHORT: RescaleFunction( (short *)buffer, source, slope, intercept, size); break; case ImageIOBase::UINT: RescaleFunction( (unsigned int *)buffer, source, slope, intercept, size); break; case ImageIOBase::INT: RescaleFunction( (int *)buffer, source, slope, intercept, size); break; case ImageIOBase::FLOAT: RescaleFunction( (float *)buffer, source, slope, intercept, size); break; case ImageIOBase::DOUBLE: RescaleFunction( (double *)buffer, source, slope, intercept, size); break; default: ::itk::OStringStream message; message << "itk::ERROR: GDCMImageIO: Unknown component type : " << bufferType; ::itk::ExceptionObject e(__FILE__, __LINE__, message.str().c_str(),ITK_LOCATION); throw e; } } */