1 /*
2  *
3  *  Copyright (C) 2000-2016, OFFIS e.V.
4  *  All rights reserved.  See COPYRIGHT file for details.
5  *
6  *  This software and supporting documentation were developed by
7  *
8  *    OFFIS e.V.
9  *    R&D Division Health
10  *    Escherweg 2
11  *    D-26121 Oldenburg, Germany
12  *
13  *
14  *  Module: dcmsr
15  *
16  *  Author: Joerg Riesmeier
17  *
18  *  Purpose:
19  *    render the contents of a DICOM structured reporting file in HTML format
20  *
21  */
22 
23 
24 #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
25 
26 #include "dcmtk/dcmsr/dsrdoc.h"       /* for main interface class DSRDocument */
27 
28 #include "dcmtk/dcmdata/dctk.h"       /* for typical set of "dcmdata" headers */
29 
30 #include "dcmtk/ofstd/ofstream.h"
31 #include "dcmtk/ofstd/ofconapp.h"
32 
33 #ifdef WITH_ZLIB
34 #include <zlib.h>                     /* for zlibVersion() */
35 #endif
36 #ifdef DCMTK_ENABLE_CHARSET_CONVERSION
37 #include "dcmtk/ofstd/ofchrenc.h"     /* for OFCharacterEncoding */
38 #endif
39 
40 #define OFFIS_CONSOLE_APPLICATION "dsr2html"
41 
42 static OFLogger dsr2htmlLogger = OFLog::getLogger("dcmtk.apps." OFFIS_CONSOLE_APPLICATION);
43 
44 static char rcsid[] = "$dcmtk: " OFFIS_CONSOLE_APPLICATION " v"
45   OFFIS_DCMTK_VERSION " " OFFIS_DCMTK_RELEASEDATE " $";
46 
47 
48 // ********************************************
49 
50 
renderFile(STD_NAMESPACE ostream & out,const char * ifname,const char * cssName,const char * defaultCharset,const E_FileReadMode readMode,const E_TransferSyntax xfer,const size_t readFlags,const size_t renderFlags,const OFBool checkAllStrings,const OFBool convertToUTF8)51 static OFCondition renderFile(STD_NAMESPACE ostream &out,
52                               const char *ifname,
53                               const char *cssName,
54                               const char *defaultCharset,
55                               const E_FileReadMode readMode,
56                               const E_TransferSyntax xfer,
57                               const size_t readFlags,
58                               const size_t renderFlags,
59                               const OFBool checkAllStrings,
60                               const OFBool convertToUTF8)
61 {
62     OFCondition result = EC_Normal;
63 
64     if ((ifname == NULL) || (strlen(ifname) == 0))
65     {
66         OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": invalid filename: <empty string>");
67         return EC_IllegalParameter;
68     }
69 
70     DcmFileFormat *dfile = new DcmFileFormat();
71     if (dfile != NULL)
72     {
73         if (readMode == ERM_dataset)
74             result = dfile->getDataset()->loadFile(ifname, xfer);
75         else
76             result = dfile->loadFile(ifname, xfer);
77         if (result.bad())
78         {
79             OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": error (" << result.text()
80                 << ") reading file: " << ifname);
81         }
82     } else
83         result = EC_MemoryExhausted;
84 
85 #ifdef DCMTK_ENABLE_CHARSET_CONVERSION
86     /* convert all DICOM strings to UTF-8 (if requested) */
87     if (result.good() && convertToUTF8)
88     {
89         DcmDataset *dset = dfile->getDataset();
90         OFLOG_INFO(dsr2htmlLogger, "converting all element values that are affected by SpecificCharacterSet (0008,0005) to UTF-8");
91         // check whether SpecificCharacterSet is absent but needed
92         if ((defaultCharset != NULL) && !dset->tagExistsWithValue(DCM_SpecificCharacterSet) &&
93             dset->containsExtendedCharacters(OFFalse /*checkAllStrings*/))
94         {
95             // use the manually specified source character set
96             result = dset->convertCharacterSet(defaultCharset, OFString("ISO_IR 192"));
97         } else {
98             // expect that SpecificCharacterSet contains the correct value
99             result = dset->convertToUTF8();
100         }
101         if (result.bad())
102         {
103             OFLOG_FATAL(dsr2htmlLogger, result.text() << ": converting file to UTF-8: " << ifname);
104         }
105     }
106 #else
107     // avoid compiler warning on unused variable
108     (void)convertToUTF8;
109 #endif
110     if (result.good())
111     {
112         result = EC_CorruptedData;
113         DcmDataset *dset = dfile->getDataset();
114         DSRDocument *dsrdoc = new DSRDocument();
115         if (dsrdoc != NULL)
116         {
117             result = dsrdoc->read(*dset, readFlags);
118             if (result.good())
119             {
120                 // check extended character set
121                 OFString charset;
122                 if ((dsrdoc->getSpecificCharacterSet(charset).bad() || charset.empty()) &&
123                     dset->containsExtendedCharacters(checkAllStrings))
124                 {
125                     // we have an unspecified extended character set
126                     if (defaultCharset == NULL)
127                     {
128                         // the dataset contains non-ASCII characters that really should not be there
129                         OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": SpecificCharacterSet (0008,0005) "
130                             << "element absent but extended characters used in file: " << ifname);
131                         OFLOG_DEBUG(dsr2htmlLogger, "use option --charset-assume to manually specify an appropriate character set");
132                         result = EC_IllegalCall;
133                     } else {
134                         // use the default character set specified by the user
135                         result = dsrdoc->setSpecificCharacterSet(defaultCharset);
136                         if (dsrdoc->getSpecificCharacterSetType() == DSRTypes::CS_unknown)
137                         {
138                             OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": Character set '"
139                                 << defaultCharset << "' specified with option --charset-assume not supported");
140                             result = EC_IllegalCall;
141                         }
142                         else if (result.bad())
143                         {
144                             OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": Cannot use character set '"
145                                 << defaultCharset << "' specified with option --charset-assume: " << result.text());
146                         }
147                     }
148                 }
149                 if (result.good())
150                     result = dsrdoc->renderHTML(out, renderFlags, cssName);
151             } else {
152                 OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": error (" << result.text()
153                     << ") parsing file: " << ifname);
154             }
155         }
156         delete dsrdoc;
157     }
158     delete dfile;
159 
160     return result;
161 }
162 
163 
164 #define SHORTCOL 3
165 #define LONGCOL 22
166 
167 
main(int argc,char * argv[])168 int main(int argc, char *argv[])
169 {
170     size_t opt_readFlags = 0;
171     size_t opt_renderFlags = DSRTypes::HF_renderDcmtkFootnote;
172     const char *opt_cssName = NULL;
173     const char *opt_defaultCharset = NULL;
174     E_FileReadMode opt_readMode = ERM_autoDetect;
175     E_TransferSyntax opt_ixfer = EXS_Unknown;
176     OFBool opt_checkAllStrings = OFFalse;
177     OFBool opt_convertToUTF8 = OFFalse;
178 
179     OFConsoleApplication app(OFFIS_CONSOLE_APPLICATION, "Render DICOM SR file and data set to HTML/XHTML", rcsid);
180     OFCommandLine cmd;
181     cmd.setOptionColumns(LONGCOL, SHORTCOL);
182     cmd.setParamColumn(LONGCOL + SHORTCOL + 4);
183 
184     cmd.addParam("dsrfile-in",   "DICOM SR input filename to be rendered", OFCmdParam::PM_Mandatory);
185     cmd.addParam("htmlfile-out", "HTML/XHTML output filename (default: stdout)", OFCmdParam::PM_Optional);
186 
187     cmd.addGroup("general options:", LONGCOL, SHORTCOL + 2);
188       cmd.addOption("--help",                   "-h",     "print this help text and exit", OFCommandLine::AF_Exclusive);
189       cmd.addOption("--version",                          "print version information and exit", OFCommandLine::AF_Exclusive);
190       OFLog::addOptions(cmd);
191 
192     cmd.addGroup("input options:");
193       cmd.addSubGroup("input file format:");
194         cmd.addOption("--read-file",            "+f",     "read file format or data set (default)");
195         cmd.addOption("--read-file-only",       "+fo",    "read file format only");
196         cmd.addOption("--read-dataset",         "-f",     "read data set without file meta information");
197       cmd.addSubGroup("input transfer syntax:");
198         cmd.addOption("--read-xfer-auto",       "-t=",    "use TS recognition (default)");
199         cmd.addOption("--read-xfer-detect",     "-td",    "ignore TS specified in the file meta header");
200         cmd.addOption("--read-xfer-little",     "-te",    "read with explicit VR little endian TS");
201         cmd.addOption("--read-xfer-big",        "-tb",    "read with explicit VR big endian TS");
202         cmd.addOption("--read-xfer-implicit",   "-ti",    "read with implicit VR little endian TS");
203 
204     cmd.addGroup("processing options:");
205       cmd.addSubGroup("additional information:");
206         cmd.addOption("--processing-details",   "-Ip",    "show currently processed content item");
207       cmd.addSubGroup("error handling:");
208         cmd.addOption("--unknown-relationship", "-Er",    "accept unknown/missing relationship type");
209         cmd.addOption("--invalid-item-value",   "-Ev",    "accept invalid content item value\n(e.g. violation of VR or VM definition)");
210         cmd.addOption("--ignore-constraints",   "-Ec",    "ignore relationship content constraints");
211         cmd.addOption("--ignore-item-errors",   "-Ee",    "do not abort on content item errors, just warn\n(e.g. missing value type specific attributes)");
212         cmd.addOption("--skip-invalid-items",   "-Ei",    "skip invalid content items (incl. sub-tree)");
213         cmd.addOption("--disable-vr-checker",   "-Dv",    "disable check for VR-conformant string values");
214       cmd.addSubGroup("character set:");
215         cmd.addOption("--charset-require",      "+Cr",    "require declaration of ext. charset (default)");
216         cmd.addOption("--charset-assume",       "+Ca", 1, "[c]harset: string",
217                                                           "assume charset c if no extended charset declared");
218         cmd.addOption("--charset-check-all",              "check all data elements with string values\n(default: only PN, LO, LT, SH, ST, UC and UT)");
219 #ifdef DCMTK_ENABLE_CHARSET_CONVERSION
220         cmd.addOption("--convert-to-utf8",      "+U8",    "convert all element values that are affected\nby Specific Character Set (0008,0005) to UTF-8");
221 #endif
222     cmd.addGroup("output options:");
223       cmd.addSubGroup("HTML/XHTML compatibility:");
224         cmd.addOption("--html-3.2",             "+H3",    "use only HTML version 3.2 compatible features");
225         cmd.addOption("--html-4.0",             "+H4",    "allow all HTML version 4.01 features (default)");
226         cmd.addOption("--xhtml-1.1",            "+X1",    "comply with XHTML version 1.1 specification");
227         cmd.addOption("--add-document-type",    "+Hd",    "add reference to SGML document type definition");
228       cmd.addSubGroup("cascading style sheet (CSS), not with HTML 3.2:");
229         cmd.addOption("--css-reference",        "+Sr", 1, "URL: string",
230                                                           "add reference to specified CSS to document");
231         cmd.addOption("--css-file",             "+Sf", 1, "[f]ilename: string",
232                                                           "embed content of specified CSS into document");
233       cmd.addSubGroup("general rendering:");
234         cmd.addOption("--expand-inline",        "+Ri",    "expand short content items inline (default)");
235         cmd.addOption("--never-expand-inline",  "-Ri",    "never expand content items inline");
236         cmd.addOption("--always-expand-inline", "+Ra",    "always expand content items inline");
237         cmd.addOption("--render-full-data",     "+Rd",    "render full data of content items");
238         cmd.addOption("--section-title-inline", "+Rt",    "render section titles inline, not separately");
239       cmd.addSubGroup("document rendering:");
240         cmd.addOption("--document-type-title",  "+Dt",    "use document type as document title (default)");
241         cmd.addOption("--patient-info-title",   "+Dp",    "use patient information as document title");
242         cmd.addOption("--no-document-header",   "-Dh",    "do not render general document information");
243       cmd.addSubGroup("code rendering:");
244         cmd.addOption("--render-inline-codes",  "+Ci",    "render codes in continuous text blocks");
245         cmd.addOption("--concept-name-codes",   "+Cn",    "render code of concept names");
246         cmd.addOption("--numeric-unit-codes",   "+Cu",    "render code of numeric measurement units");
247         cmd.addOption("--code-value-unit",      "+Cv",    "use code value as measurement unit (default)");
248         cmd.addOption("--code-meaning-unit",    "+Cm",    "use code meaning as measurement unit");
249         cmd.addOption("--render-all-codes",     "+Cc",    "render all codes (implies +Ci, +Cn and +Cu)");
250         cmd.addOption("--code-details-tooltip", "+Ct",    "render code details as a tooltip (implies +Cc)");
251 
252     /* evaluate command line */
253     prepareCmdLineArgs(argc, argv, OFFIS_CONSOLE_APPLICATION);
254     if (app.parseCommandLine(cmd, argc, argv))
255     {
256         /* check exclusive options first */
257         if (cmd.hasExclusiveOption())
258         {
259             if (cmd.findOption("--version"))
260             {
261                 app.printHeader(OFTrue /*print host identifier*/);
262                 COUT << OFendl << "External libraries used:";
263 #if !defined(WITH_ZLIB) && !defined(DCMTK_ENABLE_CHARSET_CONVERSION)
264                 COUT << " none" << OFendl;
265 #else
266                 COUT << OFendl;
267 #endif
268 #ifdef WITH_ZLIB
269                 COUT << "- ZLIB, Version " << zlibVersion() << OFendl;
270 #endif
271 #ifdef DCMTK_ENABLE_CHARSET_CONVERSION
272                 COUT << "- " << OFCharacterEncoding::getLibraryVersionString() << OFendl;
273 #endif
274                 return 0;
275             }
276         }
277 
278         /* general options */
279 
280         OFLog::configureFromCommandLine(cmd, app);
281 
282         /* input options */
283 
284         cmd.beginOptionBlock();
285         if (cmd.findOption("--read-file")) opt_readMode = ERM_autoDetect;
286         if (cmd.findOption("--read-file-only")) opt_readMode = ERM_fileOnly;
287         if (cmd.findOption("--read-dataset")) opt_readMode = ERM_dataset;
288         cmd.endOptionBlock();
289 
290         cmd.beginOptionBlock();
291         if (cmd.findOption("--read-xfer-auto"))
292             opt_ixfer = EXS_Unknown;
293         if (cmd.findOption("--read-xfer-detect"))
294             dcmAutoDetectDatasetXfer.set(OFTrue);
295         if (cmd.findOption("--read-xfer-little"))
296         {
297             app.checkDependence("--read-xfer-little", "--read-dataset", opt_readMode == ERM_dataset);
298             opt_ixfer = EXS_LittleEndianExplicit;
299         }
300         if (cmd.findOption("--read-xfer-big"))
301         {
302             app.checkDependence("--read-xfer-big", "--read-dataset", opt_readMode == ERM_dataset);
303             opt_ixfer = EXS_BigEndianExplicit;
304         }
305         if (cmd.findOption("--read-xfer-implicit"))
306         {
307             app.checkDependence("--read-xfer-implicit", "--read-dataset", opt_readMode == ERM_dataset);
308             opt_ixfer = EXS_LittleEndianImplicit;
309         }
310         cmd.endOptionBlock();
311 
312         /* processing options */
313 
314         if (cmd.findOption("--processing-details"))
315         {
316             app.checkDependence("--processing-details", "verbose mode", dsr2htmlLogger.isEnabledFor(OFLogger::INFO_LOG_LEVEL));
317             opt_readFlags |= DSRTypes::RF_showCurrentlyProcessedItem;
318         }
319         if (cmd.findOption("--unknown-relationship"))
320             opt_readFlags |= DSRTypes::RF_acceptUnknownRelationshipType;
321         if (cmd.findOption("--invalid-item-value"))
322             opt_readFlags |= DSRTypes::RF_acceptInvalidContentItemValue;
323         if (cmd.findOption("--ignore-constraints"))
324             opt_readFlags |= DSRTypes::RF_ignoreRelationshipConstraints;
325         if (cmd.findOption("--ignore-item-errors"))
326             opt_readFlags |= DSRTypes::RF_ignoreContentItemErrors;
327         if (cmd.findOption("--skip-invalid-items"))
328             opt_readFlags |= DSRTypes::RF_skipInvalidContentItems;
329         if (cmd.findOption("--disable-vr-checker"))
330             dcmEnableVRCheckerForStringValues.set(OFFalse);
331 
332         /* character set options */
333         cmd.beginOptionBlock();
334         if (cmd.findOption("--charset-require"))
335            opt_defaultCharset = NULL;
336         if (cmd.findOption("--charset-assume"))
337           app.checkValue(cmd.getValue(opt_defaultCharset));
338         cmd.endOptionBlock();
339         if (cmd.findOption("--charset-check-all"))
340             opt_checkAllStrings = OFTrue;
341 #ifdef DCMTK_ENABLE_CHARSET_CONVERSION
342         if (cmd.findOption("--convert-to-utf8"))
343             opt_convertToUTF8 = OFTrue;
344 #endif
345 
346         /* output options */
347 
348         /* HTML compatibility */
349         cmd.beginOptionBlock();
350         if (cmd.findOption("--html-3.2"))
351             opt_renderFlags = (opt_renderFlags & ~DSRTypes::HF_XHTML11Compatibility) | DSRTypes::HF_HTML32Compatibility;
352         if (cmd.findOption("--html-4.0"))
353             opt_renderFlags = (opt_renderFlags & ~(DSRTypes::HF_XHTML11Compatibility | DSRTypes::HF_HTML32Compatibility));
354         if (cmd.findOption("--xhtml-1.1"))
355             opt_renderFlags = (opt_renderFlags & ~DSRTypes::HF_HTML32Compatibility) | DSRTypes::HF_XHTML11Compatibility | DSRTypes::HF_addDocumentTypeReference;
356         cmd.endOptionBlock();
357 
358         if (cmd.findOption("--add-document-type"))
359             opt_renderFlags |= DSRTypes::HF_addDocumentTypeReference;
360 
361         /* cascading style sheet */
362         cmd.beginOptionBlock();
363         if (cmd.findOption("--css-reference"))
364         {
365             app.checkConflict("--css-reference", "--html-3.2", (opt_renderFlags & DSRTypes::HF_HTML32Compatibility) > 0);
366             opt_renderFlags &= ~DSRTypes::HF_copyStyleSheetContent;
367             app.checkValue(cmd.getValue(opt_cssName));
368         }
369         if (cmd.findOption("--css-file"))
370         {
371             app.checkConflict("--css-file", "--html-3.2", (opt_renderFlags & DSRTypes::HF_HTML32Compatibility) > 0);
372             opt_renderFlags |= DSRTypes::HF_copyStyleSheetContent;
373             app.checkValue(cmd.getValue(opt_cssName));
374         }
375         cmd.endOptionBlock();
376 
377         /* general rendering */
378         cmd.beginOptionBlock();
379         if (cmd.findOption("--expand-inline"))
380         {
381             /* default */
382         }
383         if (cmd.findOption("--never-expand-inline"))
384             opt_renderFlags |= DSRTypes::HF_neverExpandChildrenInline;
385         if (cmd.findOption("--always-expand-inline"))
386             opt_renderFlags |= DSRTypes::HF_alwaysExpandChildrenInline;
387         cmd.endOptionBlock();
388 
389         if (cmd.findOption("--render-full-data"))
390             opt_renderFlags |= DSRTypes::HF_renderFullData;
391 
392         if (cmd.findOption("--section-title-inline"))
393             opt_renderFlags |= DSRTypes::HF_renderSectionTitlesInline;
394 
395         /* document rendering */
396         cmd.beginOptionBlock();
397         if (cmd.findOption("--document-type-title"))
398         {
399             /* default */
400         }
401         if (cmd.findOption("--patient-info-title"))
402             opt_renderFlags |= DSRTypes::HF_renderPatientTitle;
403         cmd.endOptionBlock();
404 
405         if (cmd.findOption("--no-document-header"))
406             opt_renderFlags |= DSRTypes::HF_renderNoDocumentHeader;
407 
408         /* code rendering */
409         if (cmd.findOption("--render-inline-codes"))
410             opt_renderFlags |= DSRTypes::HF_renderInlineCodes;
411         if (cmd.findOption("--concept-name-codes"))
412             opt_renderFlags |= DSRTypes::HF_renderConceptNameCodes;
413         if (cmd.findOption("--numeric-unit-codes"))
414             opt_renderFlags |= DSRTypes::HF_renderNumericUnitCodes;
415         if (cmd.findOption("--code-value-unit"))
416             opt_renderFlags &= ~DSRTypes::HF_useCodeMeaningAsUnit;
417         if (cmd.findOption("--code-meaning-unit"))
418             opt_renderFlags |= DSRTypes::HF_useCodeMeaningAsUnit;
419         if (cmd.findOption("--render-all-codes"))
420             opt_renderFlags |= DSRTypes::HF_renderAllCodes;
421         if (cmd.findOption("--code-details-tooltip"))
422         {
423             app.checkConflict("--code-details-tooltip", "--html-3.2", (opt_renderFlags & DSRTypes::HF_HTML32Compatibility) > 0);
424             opt_renderFlags |= DSRTypes::HF_useCodeDetailsTooltip;
425         }
426     }
427 
428     /* print resource identifier */
429     OFLOG_DEBUG(dsr2htmlLogger, rcsid << OFendl);
430 
431     /* make sure data dictionary is loaded */
432     if (!dcmDataDict.isDictionaryLoaded())
433     {
434         OFLOG_WARN(dsr2htmlLogger, "no data dictionary loaded, check environment variable: "
435             << DCM_DICT_ENVIRONMENT_VARIABLE);
436     }
437 
438     // map "old" charset names to DICOM defined terms
439     if (opt_defaultCharset != NULL)
440     {
441         OFString charset(opt_defaultCharset);
442         if (charset == "latin-1")
443             opt_defaultCharset = "ISO_IR 100";
444         else if (charset == "latin-2")
445             opt_defaultCharset = "ISO_IR 101";
446         else if (charset == "latin-3")
447             opt_defaultCharset = "ISO_IR 109";
448         else if (charset == "latin-4")
449             opt_defaultCharset = "ISO_IR 110";
450         else if (charset == "latin-5")
451             opt_defaultCharset = "ISO_IR 148";
452         else if (charset == "cyrillic")
453             opt_defaultCharset = "ISO_IR 144";
454         else if (charset == "arabic")
455             opt_defaultCharset = "ISO_IR 127";
456         else if (charset == "greek")
457             opt_defaultCharset = "ISO_IR 126";
458         else if (charset == "hebrew")
459             opt_defaultCharset = "ISO_IR 138";
460     }
461 
462     int result = 0;
463     const char *ifname = NULL;
464     /* first parameter is treated as the input filename */
465     cmd.getParam(1, ifname);
466     if (cmd.getParamCount() == 2)
467     {
468         /* second parameter specifies the output filename */
469         const char *ofname = NULL;
470         cmd.getParam(2, ofname);
471         STD_NAMESPACE ofstream stream(ofname);
472         if (stream.good())
473         {
474             if (renderFile(stream, ifname, opt_cssName, opt_defaultCharset, opt_readMode, opt_ixfer, opt_readFlags,
475                 opt_renderFlags, opt_checkAllStrings, opt_convertToUTF8).bad())
476             {
477                 result = 2;
478             }
479         } else
480             result = 1;
481     } else {
482         /* use standard output */
483         if (renderFile(COUT, ifname, opt_cssName, opt_defaultCharset, opt_readMode, opt_ixfer, opt_readFlags,
484             opt_renderFlags, opt_checkAllStrings, opt_convertToUTF8).bad())
485         {
486             result = 3;
487         }
488     }
489 
490     return result;
491 }
492