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