]> Creatis software - gdcm.git/blob - src/gdcmHeader.cxx
* src/gdcmHeader.cxx RecupLgr split in FindVR and RecupLgr. FindVR
[gdcm.git] / src / gdcmHeader.cxx
1 #include "gdcmlib.h"
2 extern "C" {
3 #include "glib.h"
4 }
5 #include <stdio.h>
6 // For nthos:
7 #ifdef _MSC_VER
8 #include <winsock.h>
9 #else
10 #include <netinet/in.h>
11 #endif
12 #include <map>
13 #include <sstream>
14 #include "gdcmUtil.h"
15
16 #define HEADER_LENGHT_TO_READ 256 // on ne lit plus que le debut
17
18 //FIXME: this looks dirty to me...
19 #define str2num(str, typeNum) *((typeNum *)(str))
20
21 VRHT * gdcmHeader::dicom_vr = (VRHT*)0;
22 gdcmDictSet* gdcmHeader::Dicts = new gdcmDictSet();
23
24 void gdcmHeader::Initialise(void) {
25         if (!gdcmHeader::dicom_vr)
26                 InitVRDict();
27         PixelPosition = (size_t)0;
28         PixelsTrouves = false;
29         RefPubDict = gdcmHeader::Dicts->GetDefaultPublicDict();
30         RefShaDict = (gdcmDict*)0;
31 }
32
33 gdcmHeader::gdcmHeader (char* InFilename) {
34         filename = InFilename;
35         Initialise();
36         fp=fopen(InFilename,"rw");
37         dbg.Error(!fp, "gdcmHeader::gdcmHeader cannot open file", InFilename);
38         BuildHeader();
39         fclose(fp);
40 }
41
42 gdcmHeader::~gdcmHeader (void) {
43         return;
44 }
45
46 void gdcmHeader::InitVRDict (void) {
47         if (dicom_vr) {
48                 dbg.Verbose(0, "gdcmHeader::InitVRDict:", "VR dictionary allready set");
49                 return;
50         }
51         VRHT *vr = new VRHT;
52         (*vr)["AE"] = "Application Entity";       // 16 car max
53         (*vr)["AS"] = "Age String";               // 4 car fixe
54         (*vr)["AT"] = "Attribute Tag";            // 2 unsigned short int
55         (*vr)["CS"] = "Code String";              // 16 car max
56         (*vr)["DA"] = "Date";                     // 8 car fixe
57         (*vr)["DS"] = "Decimal String";           // Decimal codé Binaire 16 max
58         (*vr)["DT"] = "Date Time";                // 26 car max
59         (*vr)["FL"] = "Floating Point Single";    // 4 octets IEEE 754:1985
60         (*vr)["FD"] = "Floating Point Double";    // 8 octets IEEE 754:1985
61         (*vr)["IS"] = "Integer String";           // en format externe 12 max
62         (*vr)["LO"] = "Long String";              // 64 octets max
63         (*vr)["LT"] = "Long Text";                // 10240 max
64         (*vr)["OB"] = "Other Byte String";
65         (*vr)["OW"] = "Other Word String";
66         (*vr)["PN"] = "Person Name";
67         (*vr)["SH"] = "Short String";             // 16 car max
68         (*vr)["SL"] = "Signed Long";
69         (*vr)["SQ"] = "Sequence of Items";        // Not Applicable
70         (*vr)["SS"] = "Signed Short";             // 2 octets
71         (*vr)["ST"] = "Short Text";               // 1024 car max
72         (*vr)["TM"] = "Time";                     // 16 car max
73         (*vr)["UI"] = "Unique Identifier";        // 64 car max
74         (*vr)["UN"] = "Unknown";
75         (*vr)["UT"] = "Unlimited Text";           //  2 puissance 32 -1 car max
76         (*vr)["UL"] = "Unsigned Long ";           // 4 octets fixe
77         (*vr)["US"] = "Unsigned Short ";          // 2 octets fixe
78    dicom_vr = vr;       
79 }
80
81 /**
82  * \ingroup gdcmHeader
83  * \brief   La seule maniere sure que l'on aie pour determiner 
84  *          si on est en   LITTLE_ENDIAN,       BIG-ENDIAN, 
85  *          BAD-LITTLE-ENDIAN, BAD-BIG-ENDIAN
86  *          est de trouver l'element qui donne la longueur d'un 'GROUP'
87  *          (on sait que la longueur de cet element vaut 0x00000004)
88  *          et de regarder comment cette longueur est codee en memoire  
89  *          
90  *          Le probleme vient de ce que parfois, il n'y en a pas ...
91  *          
92  *          On fait alors le pari qu'on a a faire a du LITTLE_ENDIAN propre.
93  *          (Ce qui est la norme -pas respectee- depuis ACR-NEMA)
94  *          Si ce n'est pas le cas, on ne peut rien faire.
95  *
96  *          (il faudrait avoir des fonctions auxquelles 
97  *          on passe le code Swap en parametre, pour faire des essais 'manuels')
98  */
99 void gdcmHeader::CheckSwap()
100 {
101         guint32  s;
102         guint32  x=4;  // x : pour ntohs
103         bool net2host; // true when HostByteOrder is the same as NetworkByteOrder
104          
105         int lgrLue;
106         char * entCur;
107         char deb[HEADER_LENGHT_TO_READ];
108          
109         // First, compare HostByteOrder and NetworkByteOrder in order to
110         // determine if we shall need to swap bytes (i.e. the Endian type).
111         if (x==ntohs(x))
112                 net2host = true;
113         else
114                 net2host = false;
115         
116         // The easiest case is the one of a DICOM header, since it possesses a
117         // file preamble where it suffice to look for the sting "DICM".
118         lgrLue = fread(deb, 1, HEADER_LENGHT_TO_READ, fp);
119         
120         entCur = deb + 128;
121         if(memcmp(entCur, "DICM", (size_t)4) == 0) {
122                 filetype = TrueDicom;
123                 dbg.Verbose(0, "gdcmHeader::CheckSwap:", "looks like DICOM Version3");
124         } else {
125                 filetype = Unknown;
126                 dbg.Verbose(0, "gdcmHeader::CheckSwap:", "not a DICOM Version3 file");
127         }
128
129         if(filetype == TrueDicom) {
130                 // Next, determine the value representation (VR). Let's skip to the
131                 // first element (0002, 0000) and check there if we find "UL", in
132                 // which case we (almost) know it is explicit VR.
133                 // WARNING: if it happens to be implicit VR then what we will read
134                 // is the length of the group. If this ascii representation of this
135                 // length happens to be "UL" then we shall believe it is explicit VR.
136                 // FIXME: in order to fix the above warning, we could read the next
137                 // element value (or a couple of elements values) in order to make
138                 // sure we are not commiting a big mistake.
139                 // We need to skip :
140                 // * the 128 bytes of File Preamble (often padded with zeroes),
141                 // * the 4 bytes of "DICM" string,
142                 // * the 4 bytes of the first tag (0002, 0000),
143                 // i.e. a total of  136 bytes.
144                 entCur = deb + 136;
145                 if(memcmp(entCur, "UL", (size_t)2) == 0) {
146                         filetype = ExplicitVR;
147                         dbg.Verbose(0, "gdcmHeader::CheckSwap:",
148                                     "explicit Value Representation");
149                 } else {
150                         filetype = ImplicitVR;
151                         dbg.Verbose(0, "gdcmHeader::CheckSwap:",
152                                     "not an explicit Value Representation");
153                 }
154
155                 if (net2host) {
156                         sw = 4321;
157                         dbg.Verbose(0, "gdcmHeader::CheckSwap:",
158                                        "HostByteOrder != NetworkByteOrder");
159                 } else {
160                         sw = 0;
161                         dbg.Verbose(0, "gdcmHeader::CheckSwap:",
162                                        "HostByteOrder = NetworkByteOrder");
163                 }
164                 
165                 // Position the file position indicator at first tag (i.e.
166                 // after the file preamble and the "DICM" string).
167                 rewind(fp);
168                 fseek (fp, 132L, SEEK_SET);
169                 return;
170         } // End of TrueDicom
171
172         // Alas, this is not a DicomV3 file and whatever happens there is no file
173         // preamble. We can reset the file position indicator to where the data
174         // is (i.e. the beginning of the file).
175         rewind(fp);
176
177         // Our next best chance would be to be considering a 'clean' ACR/NEMA file.
178         // By clean we mean that the length of the first tag is written down.
179         // If this is the case and since the length of the first group HAS to be
180         // four (bytes), then determining the proper swap code is straightforward.
181
182         entCur = deb + 4;
183         s = str2num(entCur, int);
184         
185         switch (s) {
186         case 0x00040000 :
187                 sw=3412;
188                 filetype = ACR;
189                 return;
190         case 0x04000000 :
191                 sw=4321;
192                 filetype = ACR;
193                 return;
194         case 0x00000400 :
195                 sw=2143;
196                 filetype = ACR;
197                 return;
198         case 0x00000004 :
199                 sw=0;
200                 filetype = ACR;
201                 return;
202         default :
203                 dbg.Verbose(0, "gdcmHeader::CheckSwap:",
204                                "ACE/NEMA unfound swap info (time to raise bets)");
205         }
206
207         // We are out of luck. It is not a DicomV3 nor a 'clean' ACR/NEMA file.
208         // It is time for despaired wild guesses. So, let's assume this file
209         // happens to be 'dirty' ACR/NEMA, i.e. the length of the group it
210         // not present. Then the only info we have is the net2host one.
211         //FIXME  Si c'est du RAW, ca degagera + tard
212         
213         if (! net2host )
214                 sw = 0;
215         else
216                 sw = 4321;
217         return;
218 }
219
220 /**
221  * \ingroup   gdcmHeader
222  * \brief     recupere la longueur d'un champ DICOM.
223  *            Preconditions:
224  *            1/ le fichier doit deja avoir ete ouvert,
225  *            2/ CheckSwap() doit avoir ete appele
226  *            3/ la  partie 'group'  ainsi que la  partie 'elem' 
227  *               de l'acr_element doivent avoir ete lues.
228  *
229  *            ACR-NEMA : we allways get
230  *                 GroupNumber   (2 Octets) 
231  *                 ElementNumber (2 Octets) 
232  *                 ElementSize   (4 Octets)
233  *            DICOM en implicit Value Representation :
234  *                 GroupNumber   (2 Octets) 
235  *                 ElementNumber (2 Octets) 
236  *                 ElementSize   (4 Octets)
237  *
238  *            DICOM en explicit Value Representation :
239  *                 GroupNumber         (2 Octets) 
240  *                 ElementNumber       (2 Octets) 
241  *                 ValueRepresentation (2 Octets) 
242  *                 ElementSize         (2 Octets)
243  *
244  *            ATTENTION : dans le cas ou ValueRepresentation = OB, OW, SQ, UN
245  *                 GroupNumber         (2 Octets) 
246  *                 ElementNumber       (2 Octets) 
247  *                 ValueRepresentation (2 Octets)
248  *                 zone reservee       (2 Octets) 
249  *                 ElementSize         (4 Octets)
250  *
251  * @param sw  code swap
252  * @param skippedLength  pointeur sur nombre d'octets que l'on a saute qd
253  *                       la lecture est finie
254  * @param longueurLue    pointeur sur longueur (en nombre d'octets) 
255  *                       effectivement lue
256  * @return               longueur retenue pour le champ 
257  */
258
259 void gdcmHeader::FindVR( ElValue *pleCourant) {
260         char VR[3];
261         int lgrLue;
262         long PositionOnEntry = ftell(fp);
263         
264         if (filetype != ExplicitVR)
265                 return;
266
267         lgrLue=fread (&VR, (size_t)2,(size_t)1, fp);
268         VR[2]=0;
269                 
270         // Warning: we believe this is explicit VR (Value Representation) because
271         // we used a heuristic that found "UL" in the first tag. Alas this
272         // doesn't guarantee that all the tags will be in explicit VR. In some
273         // cases (see e-film filtered files) one finds implicit VR tags mixed
274         // within an explicit VR file. Hence we make sure the present tag
275         // is in explicit VR and try to fix things if it happens not to be
276         // the case.
277
278         // FIXME There should be only one occurence returned. Avoid the
279         // first extraction by calling proper method.
280         VRAtr FoundVR = dicom_vr->find(string(VR))->first;
281         if ( ! FoundVR.empty()) {
282                 pleCourant->SetVR(FoundVR);
283                 return; 
284         }
285         
286         // We thought this was explicit VR, but we end up with an
287         // implicit VR tag. Let's backtrack.
288         pleCourant->SetVR("Implicit");
289         fseek(fp, PositionOnEntry, SEEK_SET);
290 }
291
292 void gdcmHeader::RecupLgr( ElValue *pleCourant) {
293         int lgrLue;
294         guint32 l_gr;
295         unsigned short int l_gr_2;
296         
297         string vr = pleCourant->GetVR();
298         
299         if ( (filetype == ExplicitVR) && (vr != "Implicit") ) {
300                 if (   ( vr == "OB" ) || ( vr == "OW" )
301                          || ( vr == "SQ" ) || ( vr == "UN" ) ) {
302                         
303                         // The following two bytes are reserved, so we skip them,
304                         // and we proceed on reading the length on 4 bytes.
305                         fseek(fp, 2L,SEEK_CUR);
306                         lgrLue=fread (&l_gr, (size_t)4,(size_t)1, fp);
307                         l_gr = SwapLong((guint32)l_gr);
308                         
309                 } else {
310                         //on lit la lgr sur DEUX octets
311                         lgrLue=fread (&l_gr_2, (size_t)2,(size_t)1, fp);
312                         
313                         l_gr_2 = SwapShort((unsigned short)l_gr_2);
314                         
315                         if ( l_gr_2 == 0xffff) {
316                                 l_gr = 0;
317                         } else {
318                                 l_gr = l_gr_2;
319                         }
320                 }
321         } else {
322                 // Explicit VR = 0
323                 //on lit la lgr sur QUATRE octets
324                 lgrLue=fread (&l_gr, (size_t)4,(size_t)1, fp);
325                 l_gr= SwapLong((long)l_gr);
326         }
327
328         // Traitement des curiosites sur la longueur
329         if ( (int)l_gr == 0xffffffff)
330                 l_gr=0; 
331         
332         pleCourant->SetLgrElem(l_gr);
333 }
334
335 /**
336  * \ingroup gdcmHeader
337  * \brief   remet les octets dans un ordre compatible avec celui du processeur
338
339  * @return  longueur retenue pour le champ 
340  */
341 guint32 gdcmHeader::SwapLong(guint32 a) {
342         // FIXME: il pourrait y avoir un pb pour les entiers negatifs ...
343         switch (sw) {
344         case    0 :
345                 break;
346         case 4321 :
347                 a=(   ((a<<24) & 0xff000000) | ((a<<8)  & 0x00ff0000)    | 
348                       ((a>>8)  & 0x0000ff00) | ((a>>24) & 0x000000ff) );
349                 break;
350         
351         case 3412 :
352                 a=(   ((a<<16) & 0xffff0000) | ((a>>16) & 0x0000ffff) );
353                 break;
354         
355         case 2143 :
356                 a=(    ((a<<8) & 0xff00ff00) | ((a>>8) & 0x00ff00ff)  );
357                 break;
358         default :
359                 dbg.Error(" gdcmHeader::SwapLong : unset swap code");
360                 a=0;
361         }
362         return(a);
363 }
364
365 /**
366  * \ingroup gdcmHeader
367  * \brief   remet les octets dans un ordre compatible avec celui du processeur
368
369  * @return  longueur retenue pour le champ 
370  */
371 short int gdcmHeader::SwapShort(short int a) {
372         if ( (sw==4321)  || (sw==2143) )
373                 a =(((a<<8) & 0x0ff00) | ((a>>8)&0x00ff));
374         return (a);
375 }
376
377 /**
378  * \ingroup       gdcmHeader
379  * \brief         lit le dicom_element suivant.
380  *                      (le fichier doit deja avoir ete ouvert,
381  *                       _IdAcrCheckSwap(ID_DCM_HDR *e) avoir ete appele)
382  * @param e      ID_DCM_HDR  dans lequel effectuer la recherche.
383  * @param sw            code swap.
384  * @return              En cas de succes, 1 
385  *                      0 en cas d'echec.
386  */
387
388 ElValue * gdcmHeader::ReadNextElement(void) {
389         unsigned short g;
390         unsigned short n;
391         guint32 l;
392         size_t lgrLue;
393         ElValue * nouvDcmElem;
394         
395         // ------------------------- Lecture Num group : g
396         lgrLue=fread (&g, (size_t)2,(size_t)1, fp);
397         
398         if (feof(fp))  {
399                 dbg.Verbose(1, "ReadNextElement: EOF encountered");
400                 return (NULL);
401         }
402         if (ferror(fp)){
403                 dbg.Verbose(1, "ReadNextElement: failed to read NumGr");
404                 return (NULL);
405         }
406         
407         if (sw) g= SwapShort(((short)g));
408         
409         // ------------------------- Lecture Num Elem : n
410         lgrLue=fread (&n, (size_t)2,(size_t)1, fp);
411         
412         if (feof(fp))  {
413                 dbg.Verbose(1, "ReadNextElement: EOF encountered");
414                 return (NULL);
415         }
416         if (ferror(fp)){
417                 dbg.Verbose(1, "ReadNextElement: failed to read NumElem");
418                 return (NULL);
419         }
420         
421         if(sw) n= SwapShort(((short)n));
422
423         // Find out if the tag we encountered is in the dictionaries:
424         gdcmDictEntry * NewTag = IsInDicts(g, n);
425         if (!NewTag)
426                 NewTag = new gdcmDictEntry(g, n, "Unknown", "Unknown", "Unkown");
427
428         nouvDcmElem = new ElValue(NewTag);
429         if (!nouvDcmElem) {
430                 dbg.Verbose(1, "ReadNextElement: failed to allocate ElValue");
431                 return(NULL);
432         }
433
434         // ------------------------- Lecture longueur element : l
435         
436         FindVR(nouvDcmElem);
437         RecupLgr(nouvDcmElem);
438         nouvDcmElem->SetOffset(ftell(fp));
439         l = nouvDcmElem->GetLgrElem();
440
441         //FIXMEif(!memcmp( VR,"SQ",(size_t)2 )) { // ca annonce une SEQUENCE d'items ?!
442         //FIXME l_gr=0;                         // on lira donc les items de la sequence 
443         //FIXME}
444         //FIXMEreturn(l_gr);
445
446
447         if(g==0xfffe) l=0;  // pour sauter les indicateurs de 'SQ'
448         
449         
450         // ------------------------- Lecture Valeur element 
451         
452         // FIXME The exact size should be l if we move to strings or whatever
453         // CLEAN ME NEWValue used to be nouvDcmElem->valeurElem
454         char* NewValue = (char*)g_malloc(l+1);
455         if(NewValue) {
456                 NewValue[l]= 0;
457         } else {
458                 return (NULL);
459         }
460         
461         // FIXME les elements trop long (seuil a fixer a la main) ne devraient
462         // pas etre charge's !!!! Voir TODO.
463         lgrLue=fread (NewValue, (size_t)l,(size_t)1, fp);
464         
465
466         // ------------------------- Doit-on le Swapper ?
467         
468         if ((n==0) && sw)  {  // n=0 : lgr du groupe : guint32
469                 *(guint32 *) NewValue = SwapLong ((*(guint32 *) NewValue));  
470         } else {
471                 if(sw) {
472                         if ( (g/2)*2-g==0) { /* on ne teste pas les groupes impairs */
473                                 
474                                 if ((l==4)||(l==2)) {  // pour eviter de swapper les chaines 
475                                                             // de lgr 2 ou 4
476                                         
477                                         // FIXME make reference to nouvDcmElem->GetTag
478                                         string VR = NewTag->GetVR();
479                                         if (   (VR == "UL") || (VR == "US")
480                                             || (VR == "SL") || (VR == "SS")
481                                             || (g == 0x0028 && ( n == 0x0005 || n == 0x0200) )) {
482                                                 // seuls (28,5) de vr RET et (28,200) sont des entiers
483                                                 // ... jusqu'a preuve du contraire
484                                                 
485                                                 if(l==4) { 
486                                                         *(guint32 *) NewValue =
487                                                             SwapLong  ((*(guint32 *) NewValue)); 
488                                                 } else {
489                                                         if(l==2) 
490                                                     *(unsigned short *) NewValue =
491                                                         SwapShort ((*(unsigned short *)NewValue));
492                                                  }
493                                         }
494                                 } /* fin if l==2 ==4 */
495                         } /* fin if g pair */
496                 } /* fin sw */  
497         }
498         nouvDcmElem->value = NewValue;
499         return nouvDcmElem;
500 }
501
502 /**
503  * \ingroup gdcmHeader
504  * \brief   If we encountered the offset of the pixels in the file
505  *          (Pixel Data) then keep the info aside.
506  */
507 void gdcmHeader::SetAsidePixelData(ElValue* elem) {
508           /// FIXME this wall process is bizarre:
509           // on peut pas lire pixel data et pixel location puis
510           // a la fin de la lecture aller interpreter ce qui existe ?
511           // penser a nettoyer les variables globales associes genre
512           // PixelsTrouve ou grPixelTrouve...
513           //
514         // They are two cases :
515         // * the pixel data (i.e. the image or the volume) is pointed by it's
516         //   default official tag (0x7fe0,0x0010),
517         // * the writer of this file decided to put the image "address" (i.e the
518         //   offset from the begining of the file) at a different tag.
519         //   Then the "Pixel Data" offset might be found by indirection through
520         //   the "Image Location" tag (0x0028,  0x0200). In other terms the Image
521         //   Location tag contains the group where the "Pixel Data" offset is and
522         //   inside this group the element is conventionally at element 0x0010
523         //   (when the norm is respected).
524         // 
525         // Hence getting our hands on the Pixel Data is a two stage process:
526         //  1/ * find if the "Pixel Data" tag exists.
527         //     * if it does not exist, look for the "Pixel Location" tag.
528         //  2/ look at the proper tag ("Pixel Data" or "Pixel Location" when
529         //     it exists) what the offset is.
530         guint16 g;
531         guint16 n;
532         g = elem->GetGroup();
533         n = elem->GetElement();
534         if (!grPixelTrouve) {   // on n a pas encore trouve les pixels
535                 if (g > 0x0028) {
536                         if (n > 0x0200 || g == 0x7FE0 ) {  // on a depasse (28,200)
537                                 grPixel  = 0x7FE0;
538                                 numPixel = 0x0010;
539                                 grPixelTrouve = true;
540                         }
541                 } else {         // on est sur (28,200)
542                         if (g == 0x0028) {
543                                 if (n == 0x0200) {
544                                         grPixelTrouve = 1;
545                                         char* NewValue = (char*)g_malloc(elem->GetLgrElem()+1);
546                                         // FIXME: not very elegant conversion
547                                         for(int i=0;i<4;i++)
548                                                 *((char*)(&grPixel)+i) = *(NewValue+i); 
549                                         elem->SetValue(NewValue);
550                                         
551                                         if (grPixel != 0x7FE0)   // Vieux pb Philips
552                                                 numPixel = 0x1010;    // encore utile ??
553                                         else
554                                                 numPixel = 0x0010;
555                                 }
556                         }
557                 }
558         } else {     // on vient de trouver les pixels
559                 if (g == grPixel) {
560                         if (n == numPixel) {
561                                 PixelPosition = elem->Offset; 
562                                 PixelsTrouves = true;
563                                 dbg.Verbose(0, "gdcmHeader::SetAsidePixelData:",
564                                             "Pixel data found");
565                         }
566                 }
567         }
568 }
569
570 gdcmDictEntry * gdcmHeader::IsInDicts(guint32 group, guint32 element) {
571         gdcmDictEntry * found = (gdcmDictEntry*)0;
572         if (!RefPubDict && !RefShaDict) {
573                 //FIXME build a default dictionary !
574                 printf("FIXME in gdcmHeader::IsInDicts\n");
575         }
576         if (RefPubDict) {
577                 found = RefPubDict->GetTag(group, element);
578                 if (found)
579                         return found;
580         }
581         if (RefShaDict) {
582                 found = RefShaDict->GetTag(group, element);
583                 if (found)
584                         return found;
585         }
586         return found;
587 }
588
589 string gdcmHeader::GetPubElValByNumber(unsigned short, unsigned short) {
590 }
591
592 /**
593  * \ingroup       gdcmHeader
594  * \brief         renvoie un pointeur sur le ID_DCM_HDR correspondant au fichier
595  * @param filename      Nom du fichier ACR / LibIDO / DICOM
596  * @return       le ID_DCM_HDR 
597  */
598  
599 void gdcmHeader::BuildHeader(void) {
600         ElValue * newElValue = (ElValue *)0;
601         
602         rewind(fp);
603         rewind(fp);
604         CheckSwap();
605         while ( (newElValue = ReadNextElement()) ) {
606                 PubElVals.Add(newElValue);
607         }
608 }
609
610 int gdcmHeader::PrintPubElVal(ostream & os) {
611         return PubElVals.Print(os);
612 }
613
614 void gdcmHeader::PrintPubDict(ostream & os) {
615         RefPubDict->Print(os);
616 }