Initial commit

This commit is contained in:
René Mathieu
2026-01-17 13:49:51 +01:00
commit 0fef8d96c5
1897 changed files with 396119 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
# Copyright 2020 pydicom authors. See LICENSE file for details.
"""Pydicom command line interface program for codify"""
import argparse
import pydicom.util.codify
default_exclude_size = 100
def add_subparser(subparsers: argparse._SubParsersAction) -> None:
codify_parser = subparsers.add_parser(
"codify",
description=(
"Read a DICOM file and produce the pydicom (Python) "
"code which can create that file"
),
epilog=(
"Binary data (e.g. pixels) larger than --exclude-size "
f"(default {default_exclude_size} bytes) is not included. "
"A dummy line with a syntax error is produced. "
"Private data elements are not included by default."
),
)
# Codify existed before as a stand-alone before, re-use it here
pydicom.util.codify.set_parser_arguments(codify_parser, default_exclude_size)
codify_parser.set_defaults(func=pydicom.util.codify.do_codify)

View File

@@ -0,0 +1,232 @@
# Copyright 2020 pydicom authors. See LICENSE file for details.
"""Pydicom command line interface program
Each subcommand is a module within pydicom.cli, which
defines an add_subparser(subparsers) function to set argparse
attributes, and calls set_defaults(func=callback_function)
"""
import argparse
from importlib.metadata import entry_points
import re
import sys
from typing import cast, Any
from collections.abc import Callable
from pydicom import dcmread
from pydicom.data.data_manager import get_charset_files, get_testdata_file
from pydicom.dataset import Dataset
subparsers: argparse._SubParsersAction | None = None
# Restrict the allowed syntax tightly, since use Python `eval`
# on the expression. Do not allow callables, or assignment, for example.
re_kywd_or_item = (
r"\w+" # Keyword (\w allows underscore, needed for file_meta)
r"(\[(-)?\d+\])?" # Optional [index] or [-index]
)
re_file_spec_object = re.compile(re_kywd_or_item + r"(\." + re_kywd_or_item + r")*$")
filespec_help = (
"File specification, in format [pydicom::]filename[::element]. "
"If `pydicom::` prefix is present, then use the pydicom "
"test file with that name. If `element` is given, "
"use only that data element within the file. "
"Examples: "
"path/to/your_file.dcm, "
"your_file.dcm::StudyDate, "
"pydicom::rtplan.dcm::BeamSequence[0], "
"yourplan.dcm::BeamSequence[0].BeamNumber"
)
def eval_element(ds: Dataset, element: str) -> Any:
try:
return eval("ds." + element, {"ds": ds})
except AttributeError:
raise argparse.ArgumentTypeError(
f"Data element '{element}' is not in the dataset"
)
except IndexError as e:
raise argparse.ArgumentTypeError(f"'{element}' has an index error: {e}")
def filespec_parts(filespec: str) -> tuple[str, str, str]:
"""Parse the filespec format into prefix, filename, element
Format is [prefix::filename::element]
Note that ':' can also exist in valid filename, e.g. r'c:\temp\test.dcm'
"""
*prefix_file, last = filespec.split("::")
if not prefix_file: # then only the filename component
return "", last, ""
prefix = "pydicom" if prefix_file[0] == "pydicom" else ""
if prefix:
prefix_file.pop(0)
# If list empty after pop above, then have pydicom::filename
if not prefix_file:
return prefix, last, ""
return prefix, "".join(prefix_file), last
def filespec_parser(filespec: str) -> list[tuple[Dataset, Any]]:
"""Utility to return a dataset and an optional data element value within it
Note: this is used as an argparse 'type' for adding parsing arguments.
Parameters
----------
filespec: str
A filename with optional `pydicom::` prefix and optional data element,
in format:
[pydicom::]<filename>[::<element>]
If an element is specified, it must be a path to a data element,
sequence item (dataset), or a sequence.
Examples:
your_file.dcm
your_file.dcm::StudyDate
pydicom::rtplan.dcm::BeamSequence[0]
pydicom::rtplan.dcm::BeamSequence[0].BeamLimitingDeviceSequence
Returns
-------
List[Tuple[Dataset, Any]]
Matching pairs of (dataset, data element value)
This usually is a single pair, but a list is returned for future
ability to work across multiple files.
Note
----
This function is meant to be used in a call to an `argparse` library's
`add_argument` call for subparsers, with name="filespec" and
`type=filespec_parser`. When used that way, the resulting args.filespec
will contain the return values of this function
(e.g. use `ds, element_val = args.filespec` after parsing arguments)
See the `pydicom.cli.show` module for an example.
Raises
------
argparse.ArgumentTypeError
If the filename does not exist in local path or in pydicom test files,
or if the optional element is not a valid expression,
or if the optional element is a valid expression but does not exist
within the dataset
"""
prefix, filename, element = filespec_parts(filespec)
# Get the pydicom test filename even without prefix, in case user forgot it
try:
pydicom_filename = cast(str, get_testdata_file(filename))
except ValueError: # will get this if absolute path passed
pydicom_filename = ""
# Check if filename is in charset files
if not pydicom_filename:
try:
char_filenames = get_charset_files(filename)
if char_filenames:
pydicom_filename = char_filenames[0]
except NotImplementedError: # will get this if absolute path passed
pass
if prefix == "pydicom":
filename = pydicom_filename
# Check element syntax first to avoid unnecessary load of file
if element and not re_file_spec_object.match(element):
raise argparse.ArgumentTypeError(
f"Component '{element}' is not valid syntax for a "
"data element, sequence, or sequence item"
)
# Read DICOM file
try:
ds = dcmread(filename, force=True)
except FileNotFoundError:
extra = (
(f", \nbut 'pydicom::{filename}' test data file is available")
if pydicom_filename
else ""
)
raise argparse.ArgumentTypeError(f"File '{filename}' not found{extra}")
except Exception as e:
raise argparse.ArgumentTypeError(f"Error reading '{filename}': {e}")
if not element:
return [(ds, None)]
data_elem_val = eval_element(ds, element)
return [(ds, data_elem_val)]
def help_command(args: argparse.Namespace) -> None:
if subparsers is None:
print("No subcommands are available")
return
subcommands: list[str] = list(subparsers.choices.keys())
if args.subcommand and args.subcommand in subcommands:
subparsers.choices[args.subcommand].print_help()
else:
print("Use pydicom help [subcommand] to show help for a subcommand")
subcommands.remove("help")
print(f"Available subcommands: {', '.join(subcommands)}")
SubCommandType = dict[str, Callable[[argparse._SubParsersAction], None]]
def get_subcommand_entry_points() -> SubCommandType:
subcommands = {}
for entry_point in entry_points(group="pydicom_subcommands"):
subcommands[entry_point.name] = entry_point.load()
return subcommands
def main(args: list[str] | None = None) -> None:
"""Entry point for 'pydicom' command line interface
Parameters
----------
args : List[str], optional
Command-line arguments to parse. If ``None``, then :attr:`sys.argv`
is used.
"""
global subparsers
py_version = sys.version.split()[0]
parser = argparse.ArgumentParser(
prog="pydicom",
description=f"pydicom command line utilities (Python {py_version})",
)
subparsers = parser.add_subparsers(help="subcommand help")
help_parser = subparsers.add_parser("help", help="display help for subcommands")
help_parser.add_argument(
"subcommand", nargs="?", help="Subcommand to show help for"
)
help_parser.set_defaults(func=help_command)
# Get subcommands to register themselves as a subparser
subcommands = get_subcommand_entry_points()
for subcommand in subcommands.values():
subcommand(subparsers)
ns = parser.parse_args(args)
if not vars(ns):
parser.print_help()
else:
ns.func(ns)

View File

@@ -0,0 +1,162 @@
# Copyright 2019 pydicom authors. See LICENSE file for details.
"""Pydicom command line interface program for `pydicom show`"""
import argparse
from collections.abc import Callable
from pydicom.dataset import Dataset
from pydicom.cli.main import filespec_help, filespec_parser
def add_subparser(subparsers: argparse._SubParsersAction) -> None:
subparser = subparsers.add_parser(
"show", description="Display all or part of a DICOM file"
)
subparser.add_argument("filespec", help=filespec_help, type=filespec_parser)
subparser.add_argument(
"-x",
"--exclude-private",
help="Don't show private data elements",
action="store_true",
)
subparser.add_argument(
"-t", "--top", help="Only show top level", action="store_true"
)
subparser.add_argument(
"-q",
"--quiet",
help="Only show basic information",
action="store_true",
)
subparser.set_defaults(func=do_command)
def do_command(args: argparse.Namespace) -> None:
if len(args.filespec) != 1:
raise NotImplementedError("Show can only work on a single DICOM file input")
ds, element_val = args.filespec[0]
if not element_val:
element_val = ds
if args.exclude_private:
ds.remove_private_tags()
if args.quiet and isinstance(element_val, Dataset):
show_quiet(element_val)
elif args.top and isinstance(element_val, Dataset):
print(element_val.top())
else:
print(str(element_val))
def SOPClassname(ds: Dataset) -> str | None:
class_uid = ds.get("SOPClassUID")
if class_uid is None:
return None
return f"SOPClassUID: {class_uid.name}"
def quiet_rtplan(ds: Dataset) -> str | None:
if "BeamSequence" not in ds:
return None
plan_label = ds.get("RTPlanLabel")
plan_name = ds.get("RTPlanName")
line = f"Plan Label: {plan_label} "
if plan_name:
line += f"Plan Name: {plan_name}"
lines = [line]
if "FractionGroupSequence" in ds: # it should be, is mandatory
for fraction_group in ds.FractionGroupSequence:
fraction_group_num = fraction_group.get("FractionGroupNumber", "")
descr = fraction_group.get("FractionGroupDescription", "")
fractions = fraction_group.get("NumberOfFractionsPlanned")
fxn_info = f"{fractions} fraction(s) planned" if fractions else ""
lines.append(f"Fraction Group {fraction_group_num} {descr} {fxn_info}")
num_brachy = fraction_group.get("NumberOfBrachyApplicationSetups")
lines.append(f" Brachy Application Setups: {num_brachy}")
for refd_beam in fraction_group.ReferencedBeamSequence:
ref_num = refd_beam.get("ReferencedBeamNumber")
dose = refd_beam.get("BeamDose")
mu = refd_beam.get("BeamMeterset")
line = f" Beam {ref_num} "
if dose or mu:
line += f"Dose {dose} Meterset {mu}"
lines.append(line)
for beam in ds.BeamSequence:
beam_num = beam.get("BeamNumber")
beam_name = beam.get("BeamName")
beam_type = beam.get("BeamType")
beam_delivery = beam.get("TreatmentDeliveryType")
beam_radtype = beam.get("RadiationType")
line = (
f"Beam {beam_num} '{beam_name}' {beam_delivery} "
f"{beam_type} {beam_radtype}"
)
if beam_type == "STATIC":
cp = beam.ControlPointSequence[0]
if cp:
energy = cp.get("NominalBeamEnergy")
gantry = cp.get("GantryAngle")
bld = cp.get("BeamLimitingDeviceAngle")
couch = cp.get("PatientSupportAngle")
line += f" energy {energy} gantry {gantry}, coll {bld}, couch {couch}"
wedges = beam.get("NumberOfWedges")
comps = beam.get("NumberOfCompensators")
boli = beam.get("NumberOfBoli")
blocks = beam.get("NumberOfBlocks")
line += f" ({wedges} wedges, {comps} comps, {boli} boli, {blocks} blocks)"
lines.append(line)
return "\n".join(lines)
def quiet_image(ds: Dataset) -> str | None:
if "SOPClassUID" not in ds or "Image Storage" not in ds.SOPClassUID.name:
return None
results = [
f"{name}: {ds.get(name, 'N/A')}"
for name in [
"BitsStored",
"Modality",
"Rows",
"Columns",
"SliceLocation",
]
]
return "\n".join(results)
# Items to show in quiet mode
# Item can be a callable or a DICOM keyword
quiet_items: list[Callable[[Dataset], str | None] | str] = [
SOPClassname,
"PatientName",
"PatientID",
# Images
"StudyID",
"StudyDate",
"StudyTime",
"StudyDescription",
quiet_image,
quiet_rtplan,
]
def show_quiet(ds: Dataset) -> None:
for item in quiet_items:
if callable(item):
result = item(ds)
if result:
print(result)
else:
print(f"{item}: {ds.get(item, 'N/A')}")