nanodcm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. #!/usr/bin/env php
  2. <?php
  3. /**
  4. * nanodcm file
  5. *
  6. * @package Nanodicom
  7. * @category Base
  8. * @author Nano Documet <nanodocumet@gmail.com>
  9. * @version 1.3.1
  10. * @copyright (c) 2010-2011
  11. * @license http://www.opensource.org/licenses/mit-license.php MIT-license
  12. */
  13. /**
  14. * The Command Line interface for Nanodicom.
  15. *
  16. * This cli script helps you run Nanodicom from the command line without the need to create php files.
  17. * WARNING: Tested in Linux only.
  18. *
  19. * Heavily inspired by Goyote's Kohana Installer
  20. * [https://github.com/goyote/kohana-installer]
  21. *
  22. * @package Nanodicom
  23. * @category Cli
  24. * @author Nano Documet <nanodocumet@gmail.com>
  25. * @version 1.3.1
  26. * @copyright (c) 2010-2011
  27. * @license http://www.opensource.org/licenses/mit-license.php MIT-license
  28. * @license http://kohanaframework.org/license
  29. */
  30. class Nanodicom_CLI
  31. {
  32. /**
  33. * Raw CLI arguments and options.
  34. *
  35. * @var array
  36. */
  37. protected $argv = array();
  38. /**
  39. * Parsed CLI options.
  40. *
  41. * @var array
  42. */
  43. protected $passed_options = array();
  44. /**
  45. * Found options to be used.
  46. *
  47. * @var array
  48. */
  49. protected $options = array();
  50. /**
  51. * Class constructor.
  52. *
  53. * @param array $argv
  54. */
  55. public function __construct(array $argv)
  56. {
  57. $this->argv = $argv;
  58. $this->parse_options();
  59. $this->root_dir = getcwd();
  60. }
  61. /**
  62. * Parses and stores all the valid options from the raw $argv array.
  63. */
  64. public function parse_options()
  65. {
  66. foreach ($this->argv as $option)
  67. {
  68. if (substr($option, 0, 2) !== '--')
  69. {
  70. continue;
  71. }
  72. $option = ltrim($option, '--');
  73. if (strpos($option, '=') !== FALSE)
  74. {
  75. list($option, $value) = explode('=', $option);
  76. if (strtolower($value) === 'false')
  77. {
  78. $value = FALSE;
  79. }
  80. }
  81. else
  82. {
  83. $value = TRUE;
  84. }
  85. $this->passed_options[strtolower($option)] = $value;
  86. }
  87. }
  88. /**
  89. * Retrieves the value of a passed in CLI option.
  90. *
  91. * e.g. If --name=value was passed in it returns "value".
  92. *
  93. * @param string $name
  94. * @param string $default
  95. * @return string
  96. */
  97. public function get_option($name, $default = NULL)
  98. {
  99. if ($this->has_option($name))
  100. {
  101. return $this->passed_options[strtolower($name)];
  102. }
  103. return $default;
  104. }
  105. /**
  106. * Checks to see if a option was passed in.
  107. *
  108. * Both --name and --name=value are valid formats, and will return true.
  109. *
  110. * @param string $name
  111. * @return boolean
  112. */
  113. public function has_option($name)
  114. {
  115. return array_key_exists(strtolower($name), $this->passed_options);
  116. }
  117. /**
  118. * Gets the common options shared by all tools
  119. */
  120. public function common_options()
  121. {
  122. // Get the path, default to current dir
  123. $path = $this->get_option('path', $this->root_dir);
  124. // Only remove the trailing slash
  125. $path = rtrim($path, '/').'/';
  126. if (strlen($path) AND substr($path, 0, 1) != '/')
  127. {
  128. // Append "root_dir" only when is not an absolute path
  129. $path = $this->root_dir.'/'.$path;
  130. }
  131. // The mask to match
  132. $mask = $this->get_option('mask', '*');
  133. // Do it recursively
  134. $recursive = (boolean) $this->get_option('recursive', TRUE);
  135. // Show errors?
  136. $errors = $this->get_option('errors', TRUE);
  137. // Show warnings?
  138. $warnings = $this->get_option('warnings', TRUE);
  139. // Show warnings?
  140. $debug = $this->get_option('debug', TRUE);
  141. return array($path, $mask, $recursive, $errors, $warnings, $debug);
  142. }
  143. /**
  144. * Outputs the summary of the matched dicom files
  145. */
  146. public function execute_summary()
  147. {
  148. // Grab the common options
  149. list($path, $mask, $recursive, $errors, $warnings, $debug) = $this->common_options();
  150. // Get the list of files (with full path)
  151. $files = $this->sdir($path, $mask, $recursive, TRUE);
  152. $return = '';
  153. foreach ($files as $file)
  154. {
  155. $return .= $this->colorize_output("Summary of file: ~\"".$file."\"~\n\n");
  156. // Create a dumper
  157. $dicom = Nanodicom::factory($file);
  158. // Get the summary
  159. $return .= $dicom->summary();
  160. if ($errors)
  161. {
  162. // Record any error if present
  163. $return .= $this->output_messages('errors', $dicom);
  164. }
  165. if ($warnings)
  166. {
  167. // Record any warning if present
  168. $return .= $this->output_messages('warnings', $dicom);
  169. }
  170. // Is a valid DICOM?
  171. $is_dicom = $dicom->is_dicom();
  172. if ($debug)
  173. {
  174. // Show the parsed time
  175. $return .= $this->colorize_output('File parsed in '.$dicom->profiler_diff('parse')." ms\n");
  176. }
  177. // Release memory
  178. unset($dicom);
  179. if ( ! $is_dicom )
  180. {
  181. continue;
  182. }
  183. $return .= $this->colorize_output('File ~'.$file."~ was successfully parsed\n\n");
  184. }
  185. exit($return);
  186. }
  187. /**
  188. * Dumps the matched dicom files
  189. */
  190. public function execute_dump()
  191. {
  192. // Grab the common options
  193. list($path, $mask, $recursive, $errors, $warnings, $debug) = $this->common_options();
  194. // The mode of output
  195. $output = $this->get_option('out', 'echo');
  196. // Get the list of files (with full path)
  197. $files = $this->sdir($path, $mask, $recursive, TRUE);
  198. $return = '';
  199. foreach ($files as $file)
  200. {
  201. $return .= $this->colorize_output("Dumping file: ~\"".$file."\"~\n\n");
  202. // Create a dumper
  203. $dicom = Nanodicom::factory($file, 'dumper');
  204. // Perform the dump
  205. $return .= $dicom->dump($output);
  206. if ($errors)
  207. {
  208. // Record any error if present
  209. $return .= $this->output_messages('errors', $dicom);
  210. }
  211. if ($warnings)
  212. {
  213. // Record any warning if present
  214. $return .= $this->output_messages('warnings', $dicom);
  215. }
  216. // Is a valid DICOM?
  217. $is_dicom = $dicom->is_dicom();
  218. if ($debug)
  219. {
  220. // Show the parsed time
  221. $return .= $this->colorize_output('File parsed in '.$dicom->profiler_diff('parse')." ms\n");
  222. // Show the dumping time
  223. $return .= $this->colorize_output('File dumped in '.$dicom->profiler_diff('dump')." ms\n");
  224. }
  225. // Release memory
  226. unset($dicom);
  227. if ( ! $is_dicom )
  228. {
  229. continue;
  230. }
  231. $return .= $this->colorize_output('File ~'.$file."~ was successfully dumped\n\n");
  232. }
  233. exit($return);
  234. }
  235. /**
  236. * Anonymizes the matched dicom files
  237. */
  238. public function execute_anonymize()
  239. {
  240. // Grab the common options
  241. list($path, $mask, $recursive, $errors, $warnings, $debug) = $this->common_options();
  242. // Overwrite?
  243. $overwride = (boolean) $this->get_option('overwrite', FALSE);
  244. // Any tags?
  245. $tags = $this->get_option('tags', NULL);
  246. $tags = ($tags !== NULL) ? json_decode($tags) : $tags;
  247. $tags = $this->convert_hex_strings_in_array($tags);
  248. // Any mapping?
  249. $map = $this->get_option('map', NULL);
  250. $map = ($map !== NULL) ? json_decode($map) : $map;
  251. $map = $this->convert_hex_strings_in_array($map);
  252. // Get the list of files (with full path)
  253. $files = $this->sdir($path, $mask, $recursive, TRUE);
  254. $return = '';
  255. foreach ($files as $file)
  256. {
  257. $return .= $this->colorize_output("Anonymizing file: ~\"".$file."\"~\n\n");
  258. // Create an anonymizer
  259. $dicom = Nanodicom::factory($file, 'anonymizer');
  260. // Perform the dump
  261. $blob = $dicom->anonymize($tags, $map);
  262. if ($errors)
  263. {
  264. // Record any error if present
  265. $return .= $this->output_messages('errors', $dicom);
  266. }
  267. if ($warnings)
  268. {
  269. // Record any warning if present
  270. $return .= $this->output_messages('warnings', $dicom);
  271. }
  272. // Is a valid DICOM?
  273. $is_dicom = $dicom->is_dicom();
  274. if ($debug)
  275. {
  276. // Show the parsed time
  277. $return .= $this->colorize_output('File parsed in '.$dicom->profiler_diff('parse')." ms\n");
  278. // Show the anonymizing time
  279. $return .= $this->colorize_output('File dumped in '.$dicom->profiler_diff('anonymize')." ms\n");
  280. }
  281. // Release memory
  282. $dicom = NULL;
  283. unset($dicom);
  284. if ( ! $is_dicom )
  285. {
  286. continue;
  287. }
  288. if ( ! $overwride)
  289. {
  290. // Get new backup file name
  291. $new_file = $this->get_backup_file($file);
  292. // Move original file to backup file
  293. rename($file, $new_file);
  294. }
  295. // Save anonymized file into original name
  296. file_put_contents($file, $blob);
  297. $blob = NULL;
  298. unset($blob);
  299. $return .= $this->colorize_output('File ~'.$file."~ was successfully anonymized\n\n");
  300. }
  301. exit($return);
  302. }
  303. /**
  304. * Function to grab the messages (if exist) from last operation in DICOM object
  305. *
  306. * @param string $type The type of message to output (errors or warnings)
  307. * @param object $dicom Nanodicom object
  308. */
  309. public function output_messages($type = 'errors', $dicom)
  310. {
  311. $return = '';
  312. if ($dicom->status != Nanodicom::SUCCESS)
  313. {
  314. foreach ($dicom->{$type} as $message)
  315. {
  316. $label = substr(strtoupper($type), 0, -1);
  317. $return .= $this->colorize_output('!'.$label.': '.$message.'!')."\n";
  318. }
  319. }
  320. return $return;
  321. }
  322. /**
  323. * Function to get a unique name for the backup file
  324. *
  325. * @param string $file the file to which we will create the backup
  326. */
  327. public function get_backup_file($file)
  328. {
  329. $count = 1;
  330. while (file_exists($file.'.bak'.$count))
  331. {
  332. $count++;
  333. }
  334. return $file.'.bak'.$count;
  335. }
  336. /**
  337. * Function to get the files of a given directory. Based on original
  338. * function written by [phpnet at novaclic dot com]
  339. * (http://www.php.net/manual/en/function.scandir.php#95588)
  340. *
  341. *
  342. * @param string $path the path to scan
  343. * @param string $mask the mask used to match the files
  344. * @param boolean $recursive whether to iterate subfolders or not
  345. * @param boolean $no_cache whether to cache the results
  346. */
  347. public function sdir($path = '.', $mask = '*', $recursive = FALSE, $no_cache = FALSE)
  348. {
  349. static $dir = array(); // cache result in memory
  350. if ( ! is_dir($path))
  351. {
  352. exit($this->colorize_output("Path ~\"{$path}\"~ does not exist!\n\n"));
  353. }
  354. if ( ! isset($dir[$path]) OR $no_cache)
  355. {
  356. $dir[$path] = scandir($path);
  357. }
  358. $sdir = array();
  359. foreach ($dir[$path] as $i => $entry)
  360. {
  361. if ($entry == '.' OR $entry == '..')
  362. continue;
  363. if (is_dir($path.$entry) AND $recursive)
  364. {
  365. $files = $this->sdir($path.$entry.'/', $mask, $recursive, $no_cache);
  366. $sdir = array_merge($sdir, $files);
  367. }
  368. if (is_file($path.$entry) AND fnmatch($mask, $entry))
  369. {
  370. $sdir[] = $path.$entry;
  371. }
  372. }
  373. return $sdir;
  374. }
  375. /**
  376. * Converts any group and element values from strings to integers
  377. *
  378. * @param array $data
  379. * @return array
  380. */
  381. protected function convert_hex_strings_in_array($data)
  382. {
  383. if ($data !== NULL AND count($data))
  384. {
  385. // Keep
  386. $updated_data = array();
  387. foreach ($data as $triplets)
  388. {
  389. // Get information
  390. $group = isset($triplets[0]) ? $triplets[0] : NULL;
  391. $group = (is_string($group)) ? hexdec($group) : $group;
  392. $element = isset($triplets[1]) ? $triplets[1] : NULL;
  393. $element = (is_string($element)) ? hexdec($element) : $element;
  394. $info = isset($triplets[2]) ? $triplets[2] : NULL;
  395. $updated_data[] = array($group, $element, $info);
  396. }
  397. $data = $updated_data;
  398. }
  399. return $data;
  400. }
  401. /**
  402. * Shows the list of available commands.
  403. */
  404. public function execute_help()
  405. {
  406. if ($this->has_option('verbose'))
  407. {
  408. $this->show_verbose_help_screen();
  409. }
  410. else
  411. {
  412. $this->show_help_screen();
  413. }
  414. }
  415. /**
  416. * Creates a directory or series of directories.
  417. *
  418. * Any directory that currently doesn't exist will be created. Upon completion it will fix the
  419. * permissions so that it's writable by everyone (appropriate for development,) the mode can
  420. * be overridden.
  421. *
  422. * @param string $dir
  423. * @param string $mode
  424. */
  425. public function mkdir($dir, $mode = NULL)
  426. {
  427. system(sprintf('mkdir -p %s', escapeshellarg($dir)));
  428. $this->fix_permissions($dir, $mode);
  429. }
  430. /**
  431. * Changes the permissions of a directory.
  432. *
  433. * Note: this function is recursive, so all of the folders and files underneath the target
  434. * directory will also receive the same permissions. The default behaviour is to 777 the whole
  435. * thing, makes it easier to develop locally.
  436. *
  437. * @param string $dir
  438. * @param string $mode
  439. */
  440. public function fix_permissions($dir, $mode = NULL)
  441. {
  442. if ( ! $mode)
  443. {
  444. // Default the mode to rxw by everyone
  445. $mode = '0777';
  446. }
  447. system(sprintf('chmod -R %s %s', escapeshellarg($mode), escapeshellarg($dir)));
  448. }
  449. /**
  450. * Builds the desired path directory structure, fixing the permissions on all new directories.
  451. *
  452. * @param string $dir
  453. * @param string $mode
  454. * @return string
  455. */
  456. public function build_path_dir($dir = NULL, $mode = NULL)
  457. {
  458. $dir = trim($dir, '/');
  459. if ( ! $dir)
  460. {
  461. return $this->root_dir;
  462. }
  463. $dirs = explode('/', $dir);
  464. do
  465. {
  466. $folder = array_shift($dirs);
  467. $folder = substr($dir, 0, strpos($dir, $folder) + strlen($folder));
  468. if ( ! is_dir($this->root_dir.'/'.$folder))
  469. {
  470. $this->mkdir($this->root_dir.'/'.$dir, $mode);
  471. $this->fix_permissions($this->root_dir.'/'.$folder, $mode);
  472. break;
  473. }
  474. }
  475. while (count($dirs));
  476. return $this->root_dir.'/'.$dir;
  477. }
  478. /**
  479. * Displays the full help screen; contains the list of available commands.
  480. */
  481. public function show_verbose_help_screen()
  482. {
  483. exit($this->colorize_output(<<<EOF
  484. *********************************************
  485. * Nanodicom Command Line helper *
  486. *********************************************
  487. ** Specify a command to run **
  488. !All options are optional, if not set, default values will be used!
  489. ~$ nanodcm [command] --[option1]=[value] --[option2]=[value] ...~
  490. common options
  491. ~$ --path=my/dir~ <- Path to search. Absolute or relative.
  492. ~Default:~ Current directory
  493. ~$ --mask=*.dcm~ <- Mask to match the filenames
  494. ~Default:~ Any file "*"
  495. ~$ --recursive=false~ <- To avoid searching in subfolders.
  496. ~Default:~ Do it recursively
  497. ~$ --errors=true~ <- Output errors found
  498. ~Default:~ true.
  499. ~$ --warnings=true~ <- Output warnings found
  500. ~Default:~ true.
  501. ~$ --debug=true~ <- Shows debug messages. Mostly profiling values, currently
  502. only 'parsing' time, 'dumping' time and 'anonymizing' time.
  503. ~Default:~ true.
  504. > summary: Outputs the summary of the DICOM files found
  505. ~$ nanodcm summary --[option1]=[value] --[option2]=[value] ...~
  506. ~no extra options~
  507. > dumper: Dumps the DICOM files found
  508. ~$ nanodcm dump --[option1]=[value] --[option2]=[value] ...~
  509. options
  510. ~$ --out=html~ <- Output mode. Available by distribution: html or echo
  511. ~Default:~ "echo" mode.
  512. > anonymizer: Anonymizes the matched files
  513. ~$ nanodcm anonymize --[option1]=[value] --[option2]=[value] ...~
  514. options
  515. ~$ --overwrite=true~ <- Anonymizes and overwrites all files found.
  516. ~Default:~ false. Creates a backup.
  517. ~$ --map={json_string}~ <- Adds a mapping capability in json format.
  518. ~Default:~ empty. Uses default from anonymizer tool.
  519. ~$ --tags={json_string}~ <- Adds a mapping capability in json format.
  520. ~Default:~ empty. Uses default from anonymizer tool.
  521. EOF
  522. ));
  523. }
  524. /**
  525. * Displays the minimal help screen; contains the list of available commands.
  526. */
  527. public function show_help_screen()
  528. {
  529. exit($this->colorize_output(<<<EOF
  530. *********************************************
  531. * Nanodicom Command Line helper *
  532. *********************************************
  533. [!] For quick snippets, try "nanodcm --verbose"
  534. ** Specify a command to run **
  535. ~summary~ Outputs the summaries of the DICOM files matched
  536. ~dump~ Dumps the files matched with the given pattern
  537. ~anonymize~ Anonymizes the files matched
  538. ~pixel~ Creates images out of the files matched [SOON!]
  539. EOF
  540. ));
  541. }
  542. /**
  543. * Colorizes the output so it's more legible.
  544. *
  545. * @param string $output
  546. * @return string
  547. */
  548. public function colorize_output($output)
  549. {
  550. // Color green for highlights
  551. preg_match_all('/~(.*?)~/', $output, $matches, PREG_SET_ORDER);
  552. foreach ($matches as $match)
  553. {
  554. $output = str_replace($match[0], "\033[0;32m".$match[1]."\033[0m", $output);
  555. }
  556. // Color red for Errors!
  557. preg_match_all('/!(.*?)!/', $output, $matches, PREG_SET_ORDER);
  558. foreach ($matches as $match)
  559. {
  560. $output = str_replace($match[0], "\033[0;31m".$match[1]."\033[0m", $output);
  561. }
  562. return $output;
  563. }
  564. }
  565. class Nanodcm extends Nanodicom_Cli
  566. {
  567. }
  568. set_time_limit(0);
  569. // First index is the name of this script
  570. array_shift($argv);
  571. $helper = new Nanodcm($argv);
  572. if (empty($argv) OR $helper->has_option('verbose'))
  573. {
  574. $helper->execute_help();
  575. }
  576. else if (method_exists($helper, $method = 'execute_'.strtolower(str_replace('-', '_', $argv[0]))))
  577. {
  578. require_once 'nanodicom.php';
  579. call_user_func(array($helper, $method));
  580. }
  581. else if ( ! empty($argv[0]))
  582. {
  583. exit("\"{$argv[0]}\" is not a valid command!\n\n");
  584. }