branch: externals/dicom
commit a782e5aadb4194afaa45ef6dd50cc237646a8c48
Author: Daniel Mendler <[email protected]>
Commit: Daniel Mendler <[email protected]>

    Display DICOM data as a nested tree
---
 dicom.el | 205 ++++++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 116 insertions(+), 89 deletions(-)

diff --git a/dicom.el b/dicom.el
index 1f467dc7c7..4207e432df 100644
--- a/dicom.el
+++ b/dicom.el
@@ -72,15 +72,18 @@
   "Field width."
   :type 'natnum)
 
-(defcustom dicom-hidden-fields
+(defcustom dicom-field-filter
   '( SpecificCharacterSet
-     DirectoryRecordType
-     OffsetOfReferencedLowerLevelDirectoryEntity
-     OffsetOfTheNextDirectoryRecord
      RecordInUseFlag
-     PrivateCreator)
+     PrivateCreator
+     FileSetConsistencyFlag
+     FileSetID
+     IconImageSequence
+     "\\`OffsetOf"
+     "UID\\'"
+     " ")
   "List of hidden DICOM properties."
-  :type '(repeat symbol))
+  :type '(repeat (choice string symbol)))
 
 (defcustom dicom-cache-dir (expand-file-name
                             (file-name-concat
@@ -108,9 +111,9 @@ progress:${percent-pos}%%' %s) & disown"
   '((t :inherit header-line :height 1.2 :weight bold))
   "Header line face.")
 
-(defface dicom-item
-  '((t :inherit (header-line outline-2) :extend t))
-  "Item face.")
+(defface dicom-title
+  '((t :inherit header-line :extend t))
+  "Item title face.")
 
 ;;;; Keymaps
 
@@ -232,35 +235,50 @@ progress:${percent-pos}%%' %s) & disown"
         file (file-name-concat dicom-cache-dir (md5 file)))
   (cons (concat file "." ext) (concat file ".tmp." ext)))
 
+(defun dicom--convert-children (dom &optional tag)
+  "Convert children of DOM with TAG."
+  (delq nil (mapcar (lambda (x)
+                      (and (or (not tag) (eq tag (dom-tag x)))
+                           (dicom--convert x)))
+                    (dom-children dom))))
+
+(defun dicom--sort-alist (alist)
+  "Sort ALIST by keys."
+  (sort alist (lambda (x y) (string< (car x) (car y)))))
+
+(defun dicom--convert (dom)
+  "Convert DOM to nested lists."
+  (pcase (dom-tag dom)
+    ((or 'item 'data-set)
+     (nconc (dicom--sort-alist (dicom--convert-children dom 'element))
+            (dicom--sort-alist (dicom--convert-children dom 'sequence))))
+    ('element
+     (when-let ((name (dom-attr dom 'name))
+                ((not (or (equal (dom-attr dom 'loaded) "no")
+                          (equal (dom-attr dom 'binary) "hidden")
+                          (let (case-fold-search)
+                            (string-match-p dicom-field-filter name))))))
+       (cons (intern name) (replace-regexp-in-string
+                            "[ \t\n^]+" " " (dom-text dom)))))
+    ('sequence
+     (when-let ((name (dom-attr dom 'name))
+                ((not (let (case-fold-search)
+                        (string-match-p dicom-field-filter name))))
+                (children (dicom--convert-children dom)))
+       (cons (intern name) children)))))
+
 (defun dicom--read (file)
   "Read DICOM FILE and return list of items."
-  (let ((dom (with-temp-buffer
-               (unless (eq 0 (call-process "dcm2xml" nil t nil
-                                           "--quiet" "--charset-assume"
-                                           "latin-1" "--convert-to-utf8" file))
-                 (error "DICOM: Reading DICOM metadata with dcm2xml failed"))
-               (libxml-parse-xml-region)))
-        (items nil))
-    (dolist (item (append (and (not (dicom--dir-p file))
-                               (dom-by-tag dom 'data-set))
-                          (dom-by-tag dom 'item)))
-      (let (alist (hidden t))
-        (dolist (elem (dom-children item))
-          (let ((name (dom-attr elem 'name)))
-            (unless (or (not (eq (dom-tag elem) 'element))
-                        (equal (dom-attr elem 'loaded) "no")
-                        (equal (dom-attr elem 'binary) "hidden")
-                        (string-search "UID" name)
-                        (string-search " " name))
-              (setq name (intern name))
-              (unless (memq name dicom-hidden-fields)
-                (setq hidden nil))
-              (push (cons name (replace-regexp-in-string
-                                "[ \t\n^]" " " (dom-text elem)))
-                    alist))))
-        (unless hidden
-          (push (sort alist (lambda (x y) (string< (car x) (car y)))) items))))
-    (nreverse items)))
+  (with-temp-buffer
+    (unless (eq 0 (call-process "dcm2xml" nil t nil
+                                "--quiet" "--charset-assume"
+                                "latin-1" "--convert-to-utf8" file))
+      (error "DICOM: Reading DICOM metadata with dcm2xml failed"))
+    (let ((dicom-field-filter (string-join
+                                (mapcar (lambda (x) (format "%s" x))
+                                        dicom-field-filter)
+                                "\\|")))
+      (dicom--convert (dom-child-by-tag (libxml-parse-xml-region) 
'data-set)))))
 
 (defun dicom--image-buffer ()
   "Return image buffer or throw an error."
@@ -326,53 +344,62 @@ progress:${percent-pos}%%' %s) & disown"
            'face 'custom-button 'mouse-face 'custom-button-mouse)
           " "))
 
-(defun dicom--insert (item)
-  "Insert ITEM in buffer."
-  (let ((type (alist-get 'DirectoryRecordType item)))
-    (insert "\n" (format
-                  (propertize " %s %s\n" 'face 'dicom-item)
-                  (or type "Item")
-                  (or (and type (or (alist-get 'StudyID item)
-                                    (alist-get 'SeriesDescription item)
-                                    (alist-get 'PatientName item)))
-                      ""))))
-  (pcase-dolist (`(,k . ,v) item)
-    (unless (memq k dicom-hidden-fields)
-      (let* ((k (symbol-name k))
-             (s k))
-        (when (> (length s) dicom-field-width)
-          (setq s (truncate-string-to-width k dicom-field-width 0 nil "…"))
-          (put-text-property 0 (length s) 'help-echo k s))
-        (setq s (string-pad s dicom-field-width))
-        (insert (format "    %s  %s\n" s v))))))
-
-(defun dicom--insert-all ()
-  "Insert all items into buffer."
-  (dolist (item dicom--data)
-    (let ((pos (point)))
-      (dicom--insert item)
-      (when (equal (alist-get 'DirectoryRecordType item) "IMAGE")
-        (pcase-let* ((src (expand-file-name
-                           (string-replace "\\" "/" (alist-get 
'ReferencedFileID item))))
-                     (`(,dst . ,tmp) (dicom--cache-name src))
-                     (tooltip (buffer-substring-no-properties (1+ pos) 
(point))))
-          (delete-region pos (point))
-          (insert (propertize
-                   " " 'display `(image ,@dicom--thumb-placeholder)
-                   'pointer 'hand
-                   'keymap dicom-image-map
-                   'dicom--file src
-                   'help-echo tooltip))
-          (if (file-exists-p dst)
-              (dicom--put-image pos dst)
-            (dicom--enqueue
-             (lambda (success)
-               (if success
-                   (progn
-                     (rename-file tmp dst)
-                     (dicom--put-image pos dst))
-                 (delete-file tmp)))
-             "dcmj2pnm" "--write-png" "--scale-y-size" "200" src tmp)))))))
+(defun dicom--title (level title)
+  (insert
+   "\n"
+   (propertize (format " %s %s\n" (make-string level ?*) title)
+               'face (list 'dicom-title (intern (format "outline-%s" 
level))))))
+
+(defun dicom--insert (level item)
+  "Insert ITEM at LEVEL in buffer."
+  (let ((type (alist-get 'DirectoryRecordType item))
+        (pos (point)))
+    (dicom--title
+     level
+     (if (= level 1)
+         (if (dicom--dir-p) "DICOM DIR" "DICOM IMAGE")
+       (format "%s %s"
+               (or type "Item")
+               (or (and type (or (alist-get 'StudyID item)
+                                 (alist-get 'SeriesDescription item)
+                                 (alist-get 'PatientName item)))
+                   ""))))
+    (pcase-dolist (`(,k . ,v) item)
+      (cond
+       ((listp v)
+        (let ((level (1+ level)))
+          (dicom--title level k)
+          (dolist (x v) (dicom--insert (1+ level) x))))
+       ((not (eq k 'DirectoryRecordType))
+        (let* ((k (symbol-name k))
+               (s k))
+          (when (> (length s) dicom-field-width)
+            (setq s (truncate-string-to-width k dicom-field-width 0 nil "…"))
+            (put-text-property 0 (length s) 'help-echo k s))
+          (setq s (string-pad s dicom-field-width))
+          (insert (format "    %s  %s\n" s v))))))
+    (when (equal type "IMAGE")
+      (pcase-let* ((src (expand-file-name
+                         (string-replace "\\" "/" (alist-get 'ReferencedFileID 
item))))
+                   (`(,dst . ,tmp) (dicom--cache-name src))
+                   (tooltip (buffer-substring-no-properties (1+ pos) (point))))
+        (delete-region pos (point))
+        (insert (propertize
+                 " " 'display `(image ,@dicom--thumb-placeholder)
+                 'pointer 'hand
+                 'keymap dicom-image-map
+                 'dicom--file src
+                 'help-echo tooltip))
+        (if (file-exists-p dst)
+            (dicom--put-image pos dst)
+          (dicom--enqueue
+           (lambda (success)
+             (if success
+                 (progn
+                   (rename-file tmp dst)
+                   (dicom--put-image pos dst))
+               (delete-file tmp)))
+           "dcmj2pnm" "--write-png" "--scale-y-size" "200" src tmp))))))
 
 (defun dicom--insert-large ()
   "Insert large image."
@@ -382,7 +409,7 @@ progress:${percent-pos}%%' %s) & disown"
     (dicom--button "Larger" #'dicom-larger)
     (dicom--button "Smaller" #'dicom-smaller)
     (dicom--button "Rotate" #'dicom-rotate)
-    (when-let ((frames (alist-get 'NumberOfFrames (car dicom--data))))
+    (when-let ((frames (alist-get 'NumberOfFrames dicom--data)))
       (dicom--button (format "Play (%s frames)" frames) #'dicom-play))
     (insert "\n\n")
     (let ((pos (point)))
@@ -426,7 +453,7 @@ progress:${percent-pos}%%' %s) & disown"
               revert-buffer-function (lambda (&rest _) (dicom--setup file))
               fringe-indicator-alist '((continuation . nil)
                                        (truncation . nil))
-              outline-regexp " [A-Z]"
+              outline-regexp " \\*+"
               outline-minor-mode-cycle t
               outline-minor-mode-use-buttons 'in-margins
               header-line-format
@@ -440,7 +467,7 @@ progress:${percent-pos}%%' %s) & disown"
     (erase-buffer)
     (unless (dicom--dir-p)
       (dicom--insert-large))
-    (dicom--insert-all)
+    (dicom--insert 1 dicom--data)
     (goto-char (point-min))))
 
 (defun dicom--setup (file)
@@ -494,10 +521,10 @@ progress:${percent-pos}%%' %s) & disown"
       (dicom--proc
        (message "Conversion in progress…"))
       (t
-       (unless (alist-get 'NumberOfFrames (car dicom--data))
+       (unless (alist-get 'NumberOfFrames dicom--data)
          (user-error "DICOM: No multi frame image"))
-       (let ((rate (or (alist-get 'RecommendedDisplayFrameRate (car 
dicom--data))
-                       (alist-get 'CineRate (car dicom--data))
+       (let ((rate (or (alist-get 'RecommendedDisplayFrameRate dicom--data)
+                       (alist-get 'CineRate dicom--data)
                        25))
              dicom-timeout)
          (message "Converting %s…" (abbreviate-file-name dicom--file))

Reply via email to