We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Allow PatientID, PatientName and StudyDate through in metadata of mha/mhd,zraw
See title. This metadata is scrubbed of during import, but should stay, because it can be used by the ImageInfoPlugin

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `app/grandchallenge/cases/image_builders/metaio_utils.py`
Content:
```
1 import re
2 import zlib
3 from pathlib import Path
4 from typing import Any, Dict, List, Mapping, Pattern, Tuple, Union
5 
6 import SimpleITK
7 import SimpleITK._SimpleITK as _SimpleITK
8 from django.core.files import File
9 
10 METAIO_IMAGE_TYPES = {
11     "MET_NONE": None,
12     "MET_ASCII_CHAR": None,
13     "MET_CHAR": SimpleITK.sitkInt8,
14     "MET_UCHAR": SimpleITK.sitkUInt8,
15     "MET_SHORT": SimpleITK.sitkInt16,
16     "MET_USHORT": SimpleITK.sitkUInt16,
17     "MET_INT": SimpleITK.sitkInt32,
18     "MET_UINT": SimpleITK.sitkUInt32,
19     "MET_LONG": SimpleITK.sitkInt64,
20     "MET_ULONG": SimpleITK.sitkUInt64,
21     "MET_LONG_LONG": None,
22     "MET_ULONG_LONG": None,
23     "MET_FLOAT": SimpleITK.sitkFloat32,
24     "MET_DOUBLE": SimpleITK.sitkFloat64,
25     "MET_STRING": None,
26     "MET_CHAR_ARRAY": SimpleITK.sitkVectorInt8,
27     "MET_UCHAR_ARRAY": SimpleITK.sitkVectorUInt8,
28     "MET_SHORT_ARRAY": SimpleITK.sitkVectorInt16,
29     "MET_USHORT_ARRAY": SimpleITK.sitkVectorUInt16,
30     "MET_INT_ARRAY": SimpleITK.sitkVectorInt32,
31     "MET_UINT_ARRAY": SimpleITK.sitkVectorUInt32,
32     "MET_LONG_ARRAY": SimpleITK.sitkVectorInt64,
33     "MET_ULONG_ARRAY": SimpleITK.sitkVectorUInt64,
34     "MET_LONG_LONG_ARRAY": None,
35     "MET_ULONG_LONG_ARRAY": None,
36     "MET_FLOAT_ARRAY": SimpleITK.sitkVectorFloat32,
37     "MET_DOUBLE_ARRAY": SimpleITK.sitkVectorFloat64,
38     "MET_FLOAT_MATRIX": None,
39     "MET_OTHER": None,
40 }
41 
42 FLOAT_MATCH_REGEXP: Pattern = re.compile(
43     r"^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$"
44 )
45 FLOAT_LIST_MATCH_REGEXP: Pattern = re.compile(
46     r"^([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)"
47     r"(\s[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)*$"
48 )
49 CONTENT_TIMES_LIST_MATCH_REGEXP: Pattern = re.compile(
50     r"^((2[0-3]|[0-1]\d)[0-5]\d[0-5]\d(\.\d\d\d)?)"
51     r"(\s(2[0-3]|[0-1]\d)[0-5]\d[0-5]\d(\.\d\d\d)?)*$"
52 )
53 
54 ADDITIONAL_HEADERS: Dict[str, Pattern] = {
55     "Exposures": FLOAT_LIST_MATCH_REGEXP,
56     "ContentTimes": CONTENT_TIMES_LIST_MATCH_REGEXP,
57     "t0": FLOAT_MATCH_REGEXP,
58     "t1": FLOAT_MATCH_REGEXP,
59 }
60 
61 HEADERS_MATCHING_NUM_TIMEPOINTS: List[str] = ["Exposures", "ContentTimes"]
62 
63 EXPECTED_HEADERS: List[str] = [
64     "ObjectType",
65     "NDims",
66     "BinaryData",
67     "BinaryDataByteOrderMSB",
68     "CompressedData",
69     "CompressedDataSize",
70     "TransformMatrix",
71     "Offset",
72     "CenterOfRotation",
73     "AnatomicalOrientation",
74     "ElementSpacing",
75     "ElementNumberOfChannels",
76     "DimSize",
77     "ElementType",
78     "ElementDataFile",
79 ]
80 
81 
82 def parse_mh_header(file: File) -> Mapping[str, Union[str, None]]:
83     """
84     Attempts to parse the headers of an mhd file.
85 
86     This function must be secure to safeguard against any untrusted uploaded
87     file.
88 
89     Parameters
90     ----------
91     filename
92 
93     Returns
94     -------
95         The extracted header from the mhd file as key value pairs.
96 
97     Raises
98     ------
99     ValueError
100         Raised when the file contains problems making it impossible to
101         read.
102     """
103 
104     # attempt to limit number of read headers to prevent overflow attacks
105     read_line_limit = 10000
106 
107     result = {}
108     with file.open("rb") as f:
109         bin_line = True
110         while bin_line is not None:
111             read_line_limit -= 1
112             if read_line_limit < 0:
113                 raise ValueError("Files contains too many header lines")
114 
115             bin_line = f.readline(10000)
116             if not bin_line:
117                 bin_line = None
118                 continue
119             if len(bin_line) >= 10000:
120                 raise ValueError("Line length is too long")
121 
122             try:
123                 line = bin_line.decode("utf-8")
124             except UnicodeDecodeError:
125                 raise ValueError("Header contains invalid UTF-8")
126             else:
127                 extract_key_value_pairs(line, result)
128             if "ElementDataFile" in result:
129                 break  # last parsed header...
130     return result
131 
132 
133 def extract_key_value_pairs(line: str, result: Dict[str, str]):
134     line = line.rstrip("\n\r")
135     if line.strip():
136         if "=" in line:
137             key, value = line.split("=", 1)
138             result[key.strip()] = value.strip()
139         else:
140             result[line.strip()] = None
141 
142 
143 def extract_header_listing(
144     property: str, headers: Mapping[str, Union[str, None]], dtype: type = float
145 ) -> List[Any]:
146     return [dtype(e) for e in headers[property].strip().split(" ")]
147 
148 
149 def load_sitk_image_with_nd_support(mhd_file: Path,) -> SimpleITK.Image:
150     headers = parse_mh_header(mhd_file)
151     is_mha = headers["ElementDataFile"].strip() == "LOCAL"
152     data_file_path = resolve_mh_data_file_path(headers, is_mha, mhd_file)
153 
154     shape = extract_header_listing("DimSize", headers=headers, dtype=int)
155 
156     dtype, num_components = determine_mh_components_and_dtype(headers)
157 
158     sitk_image = create_sitk_img_from_mh_data(
159         data_file_path, dtype, headers, is_mha, num_components, shape
160     )
161 
162     sitk_image.SetDirection(
163         extract_header_listing("TransformMatrix", headers=headers)
164     )
165     sitk_image.SetSpacing(
166         extract_header_listing("ElementSpacing", headers=headers)
167     )
168     sitk_image.SetOrigin(extract_header_listing("Offset", headers=headers))
169 
170     return sitk_image
171 
172 
173 def determine_mh_components_and_dtype(
174     headers: Mapping[str, Union[str, None]]
175 ) -> Tuple[int, int]:
176     num_components = 1
177     if "ElementNumberOfChannels" in headers:
178         num_components = int(headers["ElementNumberOfChannels"])
179         if "_ARRAY" not in headers["ElementType"] and num_components > 1:
180             headers["ElementType"] = headers["ElementType"] + "_ARRAY"
181     dtype = METAIO_IMAGE_TYPES[headers["ElementType"]]
182     if dtype is None:
183         error_msg = (
184             f"MetaIO datatype: {headers['ElementType']} is not supported"
185         )
186         raise NotImplementedError(error_msg)
187     return dtype, num_components
188 
189 
190 def resolve_mh_data_file_path(
191     headers: Mapping[str, Union[str, None]], is_mha: bool, mhd_file: Path
192 ) -> Path:
193     if is_mha:
194         data_file_path = mhd_file
195     else:
196         data_file_path = (
197             mhd_file.resolve().parent / Path(headers["ElementDataFile"]).name
198         )
199     if not data_file_path.exists():
200         raise IOError("cannot find data file")
201     return data_file_path
202 
203 
204 def create_sitk_img_from_mh_data(
205     data_file_path: Path,
206     dtype: int,
207     headers: Mapping[str, Union[str, None]],
208     is_mha: bool,
209     num_components: int,
210     shape,
211 ) -> SimpleITK.Image:
212     is_compressed = headers["CompressedData"] == "True"
213     with open(str(data_file_path), "rb") as f:
214         if is_mha:
215             line = ""
216             while "ElementDataFile = LOCAL" not in str(line):
217                 line = f.readline()
218         if not is_compressed:
219             s = f.read()
220         else:
221             s = zlib.decompress(f.read())
222     sitk_image = SimpleITK.Image(shape, dtype, num_components)
223     _SimpleITK._SetImageFromArray(s, sitk_image)
224     return sitk_image
225 
226 
227 def validate_and_clean_additional_mh_headers(
228     headers: Mapping[str, Union[str, None]]
229 ) -> Mapping[str, Union[str, None]]:
230     cleaned_headers = {}
231     for key, value in headers.items():
232         if key in EXPECTED_HEADERS:
233             cleaned_headers[key] = value
234         else:
235             if key in ADDITIONAL_HEADERS:
236                 match_pattern = ADDITIONAL_HEADERS[key]
237                 if not re.match(match_pattern, value):
238                     raise ValueError(
239                         f"Invalid data type found for "
240                         f"additional header key: {key}"
241                     )
242                 cleaned_headers[key] = value
243         if key in HEADERS_MATCHING_NUM_TIMEPOINTS:
244             validate_list_data_matches_num_timepoints(
245                 headers=headers, key=key, value=value
246             )
247 
248     return cleaned_headers
249 
250 
251 def validate_list_data_matches_num_timepoints(
252     headers: Mapping[str, Union[str, None]], key: str, value: str
253 ):
254     num_timepoints = len(value.split(" "))
255     expected_timepoints = (
256         int(headers["DimSize"].split(" ")[3])
257         if int(headers["NDims"]) >= 4
258         else 1
259     )
260     if num_timepoints != expected_timepoints:
261         raise ValueError(
262             f"Found {num_timepoints} values for {key}, "
263             f"but expected {expected_timepoints} (1/timepoint)"
264         )
265 
266 
267 def add_additional_mh_headers_to_sitk_image(
268     sitk_image: SimpleITK.Image, headers: Mapping[str, Union[str, None]]
269 ):
270     cleaned_headers = validate_and_clean_additional_mh_headers(headers)
271     for header in ADDITIONAL_HEADERS:
272         if header in cleaned_headers:
273             value = cleaned_headers[header]
274             if isinstance(value, (list, tuple)):
275                 value = " ".format([str(v) for v in value])
276             else:
277                 value = str(value)
278             sitk_image.SetMetaData(header, value)
279 
280 
281 def load_sitk_image(mhd_file: Path) -> SimpleITK.Image:
282     headers = parse_mh_header(mhd_file)
283     headers = validate_and_clean_additional_mh_headers(headers=headers)
284     ndims = int(headers["NDims"])
285     if ndims < 4:
286         sitk_image = SimpleITK.ReadImage(str(mhd_file))
287         for key in sitk_image.GetMetaDataKeys():
288             if key not in ADDITIONAL_HEADERS:
289                 sitk_image.EraseMetaData(key)
290     elif ndims <= 4:
291         sitk_image = load_sitk_image_with_nd_support(mhd_file=mhd_file)
292     else:
293         error_msg = (
294             "SimpleITK images with more than 4 dimensions are not supported"
295         )
296         raise NotImplementedError(error_msg)
297     add_additional_mh_headers_to_sitk_image(
298         sitk_image=sitk_image, headers=headers
299     )
300     return sitk_image
301
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/app/grandchallenge/cases/image_builders/metaio_utils.py b/app/grandchallenge/cases/image_builders/metaio_utils.py
--- a/app/grandchallenge/cases/image_builders/metaio_utils.py
+++ b/app/grandchallenge/cases/image_builders/metaio_utils.py
@@ -51,7 +51,16 @@
     r"(\s(2[0-3]|[0-1]\d)[0-5]\d[0-5]\d(\.\d\d\d)?)*$"
 )
 
+LENGTH_LIMIT_MATCH_REGEXP: Pattern = re.compile(r"^.{0,128}$")
+
+STUDYDATE_MATCH_REGEXP: Pattern = re.compile(r"^\d{4}\d{1,2}\d{1,2}$")
+
 ADDITIONAL_HEADERS: Dict[str, Pattern] = {
+    "PatientID": LENGTH_LIMIT_MATCH_REGEXP,
+    "PatientName": LENGTH_LIMIT_MATCH_REGEXP,
+    "StudyDate": STUDYDATE_MATCH_REGEXP,
+    "StudyInstanceUID": LENGTH_LIMIT_MATCH_REGEXP,
+    "SeriesInstanceUID": LENGTH_LIMIT_MATCH_REGEXP,
     "Exposures": FLOAT_LIST_MATCH_REGEXP,
     "ContentTimes": CONTENT_TIMES_LIST_MATCH_REGEXP,
     "t0": FLOAT_MATCH_REGEXP,

{"golden_diff": "diff --git a/app/grandchallenge/cases/image_builders/metaio_utils.py b/app/grandchallenge/cases/image_builders/metaio_utils.py\n--- a/app/grandchallenge/cases/image_builders/metaio_utils.py\n+++ b/app/grandchallenge/cases/image_builders/metaio_utils.py\n@@ -51,7 +51,16 @@\n     r\"(\\s(2[0-3]|[0-1]\\d)[0-5]\\d[0-5]\\d(\\.\\d\\d\\d)?)*$\"\n )\n \n+LENGTH_LIMIT_MATCH_REGEXP: Pattern = re.compile(r\"^.{0,128}$\")\n+\n+STUDYDATE_MATCH_REGEXP: Pattern = re.compile(r\"^\\d{4}\\d{1,2}\\d{1,2}$\")\n+\n ADDITIONAL_HEADERS: Dict[str, Pattern] = {\n+    \"PatientID\": LENGTH_LIMIT_MATCH_REGEXP,\n+    \"PatientName\": LENGTH_LIMIT_MATCH_REGEXP,\n+    \"StudyDate\": STUDYDATE_MATCH_REGEXP,\n+    \"StudyInstanceUID\": LENGTH_LIMIT_MATCH_REGEXP,\n+    \"SeriesInstanceUID\": LENGTH_LIMIT_MATCH_REGEXP,\n     \"Exposures\": FLOAT_LIST_MATCH_REGEXP,\n     \"ContentTimes\": CONTENT_TIMES_LIST_MATCH_REGEXP,\n     \"t0\": FLOAT_MATCH_REGEXP,\n", "issue": "Allow PatientID, PatientName and StudyDate through in metadata of mha/mhd,zraw\nSee title. This metadata is scrubbed of during import, but should stay, because it can be used by the ImageInfoPlugin\n", "before_files": [{"content": "import re\nimport zlib\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Mapping, Pattern, Tuple, Union\n\nimport SimpleITK\nimport SimpleITK._SimpleITK as _SimpleITK\nfrom django.core.files import File\n\nMETAIO_IMAGE_TYPES = {\n    \"MET_NONE\": None,\n    \"MET_ASCII_CHAR\": None,\n    \"MET_CHAR\": SimpleITK.sitkInt8,\n    \"MET_UCHAR\": SimpleITK.sitkUInt8,\n    \"MET_SHORT\": SimpleITK.sitkInt16,\n    \"MET_USHORT\": SimpleITK.sitkUInt16,\n    \"MET_INT\": SimpleITK.sitkInt32,\n    \"MET_UINT\": SimpleITK.sitkUInt32,\n    \"MET_LONG\": SimpleITK.sitkInt64,\n    \"MET_ULONG\": SimpleITK.sitkUInt64,\n    \"MET_LONG_LONG\": None,\n    \"MET_ULONG_LONG\": None,\n    \"MET_FLOAT\": SimpleITK.sitkFloat32,\n    \"MET_DOUBLE\": SimpleITK.sitkFloat64,\n    \"MET_STRING\": None,\n    \"MET_CHAR_ARRAY\": SimpleITK.sitkVectorInt8,\n    \"MET_UCHAR_ARRAY\": SimpleITK.sitkVectorUInt8,\n    \"MET_SHORT_ARRAY\": SimpleITK.sitkVectorInt16,\n    \"MET_USHORT_ARRAY\": SimpleITK.sitkVectorUInt16,\n    \"MET_INT_ARRAY\": SimpleITK.sitkVectorInt32,\n    \"MET_UINT_ARRAY\": SimpleITK.sitkVectorUInt32,\n    \"MET_LONG_ARRAY\": SimpleITK.sitkVectorInt64,\n    \"MET_ULONG_ARRAY\": SimpleITK.sitkVectorUInt64,\n    \"MET_LONG_LONG_ARRAY\": None,\n    \"MET_ULONG_LONG_ARRAY\": None,\n    \"MET_FLOAT_ARRAY\": SimpleITK.sitkVectorFloat32,\n    \"MET_DOUBLE_ARRAY\": SimpleITK.sitkVectorFloat64,\n    \"MET_FLOAT_MATRIX\": None,\n    \"MET_OTHER\": None,\n}\n\nFLOAT_MATCH_REGEXP: Pattern = re.compile(\n    r\"^[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$\"\n)\nFLOAT_LIST_MATCH_REGEXP: Pattern = re.compile(\n    r\"^([-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)\"\n    r\"(\\s[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)*$\"\n)\nCONTENT_TIMES_LIST_MATCH_REGEXP: Pattern = re.compile(\n    r\"^((2[0-3]|[0-1]\\d)[0-5]\\d[0-5]\\d(\\.\\d\\d\\d)?)\"\n    r\"(\\s(2[0-3]|[0-1]\\d)[0-5]\\d[0-5]\\d(\\.\\d\\d\\d)?)*$\"\n)\n\nADDITIONAL_HEADERS: Dict[str, Pattern] = {\n    \"Exposures\": FLOAT_LIST_MATCH_REGEXP,\n    \"ContentTimes\": CONTENT_TIMES_LIST_MATCH_REGEXP,\n    \"t0\": FLOAT_MATCH_REGEXP,\n    \"t1\": FLOAT_MATCH_REGEXP,\n}\n\nHEADERS_MATCHING_NUM_TIMEPOINTS: List[str] = [\"Exposures\", \"ContentTimes\"]\n\nEXPECTED_HEADERS: List[str] = [\n    \"ObjectType\",\n    \"NDims\",\n    \"BinaryData\",\n    \"BinaryDataByteOrderMSB\",\n    \"CompressedData\",\n    \"CompressedDataSize\",\n    \"TransformMatrix\",\n    \"Offset\",\n    \"CenterOfRotation\",\n    \"AnatomicalOrientation\",\n    \"ElementSpacing\",\n    \"ElementNumberOfChannels\",\n    \"DimSize\",\n    \"ElementType\",\n    \"ElementDataFile\",\n]\n\n\ndef parse_mh_header(file: File) -> Mapping[str, Union[str, None]]:\n    \"\"\"\n    Attempts to parse the headers of an mhd file.\n\n    This function must be secure to safeguard against any untrusted uploaded\n    file.\n\n    Parameters\n    ----------\n    filename\n\n    Returns\n    -------\n        The extracted header from the mhd file as key value pairs.\n\n    Raises\n    ------\n    ValueError\n        Raised when the file contains problems making it impossible to\n        read.\n    \"\"\"\n\n    # attempt to limit number of read headers to prevent overflow attacks\n    read_line_limit = 10000\n\n    result = {}\n    with file.open(\"rb\") as f:\n        bin_line = True\n        while bin_line is not None:\n            read_line_limit -= 1\n            if read_line_limit < 0:\n                raise ValueError(\"Files contains too many header lines\")\n\n            bin_line = f.readline(10000)\n            if not bin_line:\n                bin_line = None\n                continue\n            if len(bin_line) >= 10000:\n                raise ValueError(\"Line length is too long\")\n\n            try:\n                line = bin_line.decode(\"utf-8\")\n            except UnicodeDecodeError:\n                raise ValueError(\"Header contains invalid UTF-8\")\n            else:\n                extract_key_value_pairs(line, result)\n            if \"ElementDataFile\" in result:\n                break  # last parsed header...\n    return result\n\n\ndef extract_key_value_pairs(line: str, result: Dict[str, str]):\n    line = line.rstrip(\"\\n\\r\")\n    if line.strip():\n        if \"=\" in line:\n            key, value = line.split(\"=\", 1)\n            result[key.strip()] = value.strip()\n        else:\n            result[line.strip()] = None\n\n\ndef extract_header_listing(\n    property: str, headers: Mapping[str, Union[str, None]], dtype: type = float\n) -> List[Any]:\n    return [dtype(e) for e in headers[property].strip().split(\" \")]\n\n\ndef load_sitk_image_with_nd_support(mhd_file: Path,) -> SimpleITK.Image:\n    headers = parse_mh_header(mhd_file)\n    is_mha = headers[\"ElementDataFile\"].strip() == \"LOCAL\"\n    data_file_path = resolve_mh_data_file_path(headers, is_mha, mhd_file)\n\n    shape = extract_header_listing(\"DimSize\", headers=headers, dtype=int)\n\n    dtype, num_components = determine_mh_components_and_dtype(headers)\n\n    sitk_image = create_sitk_img_from_mh_data(\n        data_file_path, dtype, headers, is_mha, num_components, shape\n    )\n\n    sitk_image.SetDirection(\n        extract_header_listing(\"TransformMatrix\", headers=headers)\n    )\n    sitk_image.SetSpacing(\n        extract_header_listing(\"ElementSpacing\", headers=headers)\n    )\n    sitk_image.SetOrigin(extract_header_listing(\"Offset\", headers=headers))\n\n    return sitk_image\n\n\ndef determine_mh_components_and_dtype(\n    headers: Mapping[str, Union[str, None]]\n) -> Tuple[int, int]:\n    num_components = 1\n    if \"ElementNumberOfChannels\" in headers:\n        num_components = int(headers[\"ElementNumberOfChannels\"])\n        if \"_ARRAY\" not in headers[\"ElementType\"] and num_components > 1:\n            headers[\"ElementType\"] = headers[\"ElementType\"] + \"_ARRAY\"\n    dtype = METAIO_IMAGE_TYPES[headers[\"ElementType\"]]\n    if dtype is None:\n        error_msg = (\n            f\"MetaIO datatype: {headers['ElementType']} is not supported\"\n        )\n        raise NotImplementedError(error_msg)\n    return dtype, num_components\n\n\ndef resolve_mh_data_file_path(\n    headers: Mapping[str, Union[str, None]], is_mha: bool, mhd_file: Path\n) -> Path:\n    if is_mha:\n        data_file_path = mhd_file\n    else:\n        data_file_path = (\n            mhd_file.resolve().parent / Path(headers[\"ElementDataFile\"]).name\n        )\n    if not data_file_path.exists():\n        raise IOError(\"cannot find data file\")\n    return data_file_path\n\n\ndef create_sitk_img_from_mh_data(\n    data_file_path: Path,\n    dtype: int,\n    headers: Mapping[str, Union[str, None]],\n    is_mha: bool,\n    num_components: int,\n    shape,\n) -> SimpleITK.Image:\n    is_compressed = headers[\"CompressedData\"] == \"True\"\n    with open(str(data_file_path), \"rb\") as f:\n        if is_mha:\n            line = \"\"\n            while \"ElementDataFile = LOCAL\" not in str(line):\n                line = f.readline()\n        if not is_compressed:\n            s = f.read()\n        else:\n            s = zlib.decompress(f.read())\n    sitk_image = SimpleITK.Image(shape, dtype, num_components)\n    _SimpleITK._SetImageFromArray(s, sitk_image)\n    return sitk_image\n\n\ndef validate_and_clean_additional_mh_headers(\n    headers: Mapping[str, Union[str, None]]\n) -> Mapping[str, Union[str, None]]:\n    cleaned_headers = {}\n    for key, value in headers.items():\n        if key in EXPECTED_HEADERS:\n            cleaned_headers[key] = value\n        else:\n            if key in ADDITIONAL_HEADERS:\n                match_pattern = ADDITIONAL_HEADERS[key]\n                if not re.match(match_pattern, value):\n                    raise ValueError(\n                        f\"Invalid data type found for \"\n                        f\"additional header key: {key}\"\n                    )\n                cleaned_headers[key] = value\n        if key in HEADERS_MATCHING_NUM_TIMEPOINTS:\n            validate_list_data_matches_num_timepoints(\n                headers=headers, key=key, value=value\n            )\n\n    return cleaned_headers\n\n\ndef validate_list_data_matches_num_timepoints(\n    headers: Mapping[str, Union[str, None]], key: str, value: str\n):\n    num_timepoints = len(value.split(\" \"))\n    expected_timepoints = (\n        int(headers[\"DimSize\"].split(\" \")[3])\n        if int(headers[\"NDims\"]) >= 4\n        else 1\n    )\n    if num_timepoints != expected_timepoints:\n        raise ValueError(\n            f\"Found {num_timepoints} values for {key}, \"\n            f\"but expected {expected_timepoints} (1/timepoint)\"\n        )\n\n\ndef add_additional_mh_headers_to_sitk_image(\n    sitk_image: SimpleITK.Image, headers: Mapping[str, Union[str, None]]\n):\n    cleaned_headers = validate_and_clean_additional_mh_headers(headers)\n    for header in ADDITIONAL_HEADERS:\n        if header in cleaned_headers:\n            value = cleaned_headers[header]\n            if isinstance(value, (list, tuple)):\n                value = \" \".format([str(v) for v in value])\n            else:\n                value = str(value)\n            sitk_image.SetMetaData(header, value)\n\n\ndef load_sitk_image(mhd_file: Path) -> SimpleITK.Image:\n    headers = parse_mh_header(mhd_file)\n    headers = validate_and_clean_additional_mh_headers(headers=headers)\n    ndims = int(headers[\"NDims\"])\n    if ndims < 4:\n        sitk_image = SimpleITK.ReadImage(str(mhd_file))\n        for key in sitk_image.GetMetaDataKeys():\n            if key not in ADDITIONAL_HEADERS:\n                sitk_image.EraseMetaData(key)\n    elif ndims <= 4:\n        sitk_image = load_sitk_image_with_nd_support(mhd_file=mhd_file)\n    else:\n        error_msg = (\n            \"SimpleITK images with more than 4 dimensions are not supported\"\n        )\n        raise NotImplementedError(error_msg)\n    add_additional_mh_headers_to_sitk_image(\n        sitk_image=sitk_image, headers=headers\n    )\n    return sitk_image\n", "path": "app/grandchallenge/cases/image_builders/metaio_utils.py"}], "after_files": [{"content": "import re\nimport zlib\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Mapping, Pattern, Tuple, Union\n\nimport SimpleITK\nimport SimpleITK._SimpleITK as _SimpleITK\nfrom django.core.files import File\n\nMETAIO_IMAGE_TYPES = {\n    \"MET_NONE\": None,\n    \"MET_ASCII_CHAR\": None,\n    \"MET_CHAR\": SimpleITK.sitkInt8,\n    \"MET_UCHAR\": SimpleITK.sitkUInt8,\n    \"MET_SHORT\": SimpleITK.sitkInt16,\n    \"MET_USHORT\": SimpleITK.sitkUInt16,\n    \"MET_INT\": SimpleITK.sitkInt32,\n    \"MET_UINT\": SimpleITK.sitkUInt32,\n    \"MET_LONG\": SimpleITK.sitkInt64,\n    \"MET_ULONG\": SimpleITK.sitkUInt64,\n    \"MET_LONG_LONG\": None,\n    \"MET_ULONG_LONG\": None,\n    \"MET_FLOAT\": SimpleITK.sitkFloat32,\n    \"MET_DOUBLE\": SimpleITK.sitkFloat64,\n    \"MET_STRING\": None,\n    \"MET_CHAR_ARRAY\": SimpleITK.sitkVectorInt8,\n    \"MET_UCHAR_ARRAY\": SimpleITK.sitkVectorUInt8,\n    \"MET_SHORT_ARRAY\": SimpleITK.sitkVectorInt16,\n    \"MET_USHORT_ARRAY\": SimpleITK.sitkVectorUInt16,\n    \"MET_INT_ARRAY\": SimpleITK.sitkVectorInt32,\n    \"MET_UINT_ARRAY\": SimpleITK.sitkVectorUInt32,\n    \"MET_LONG_ARRAY\": SimpleITK.sitkVectorInt64,\n    \"MET_ULONG_ARRAY\": SimpleITK.sitkVectorUInt64,\n    \"MET_LONG_LONG_ARRAY\": None,\n    \"MET_ULONG_LONG_ARRAY\": None,\n    \"MET_FLOAT_ARRAY\": SimpleITK.sitkVectorFloat32,\n    \"MET_DOUBLE_ARRAY\": SimpleITK.sitkVectorFloat64,\n    \"MET_FLOAT_MATRIX\": None,\n    \"MET_OTHER\": None,\n}\n\nFLOAT_MATCH_REGEXP: Pattern = re.compile(\n    r\"^[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$\"\n)\nFLOAT_LIST_MATCH_REGEXP: Pattern = re.compile(\n    r\"^([-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)\"\n    r\"(\\s[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)*$\"\n)\nCONTENT_TIMES_LIST_MATCH_REGEXP: Pattern = re.compile(\n    r\"^((2[0-3]|[0-1]\\d)[0-5]\\d[0-5]\\d(\\.\\d\\d\\d)?)\"\n    r\"(\\s(2[0-3]|[0-1]\\d)[0-5]\\d[0-5]\\d(\\.\\d\\d\\d)?)*$\"\n)\n\nLENGTH_LIMIT_MATCH_REGEXP: Pattern = re.compile(r\"^.{0,128}$\")\n\nSTUDYDATE_MATCH_REGEXP: Pattern = re.compile(r\"^\\d{4}\\d{1,2}\\d{1,2}$\")\n\nADDITIONAL_HEADERS: Dict[str, Pattern] = {\n    \"PatientID\": LENGTH_LIMIT_MATCH_REGEXP,\n    \"PatientName\": LENGTH_LIMIT_MATCH_REGEXP,\n    \"StudyDate\": STUDYDATE_MATCH_REGEXP,\n    \"StudyInstanceUID\": LENGTH_LIMIT_MATCH_REGEXP,\n    \"SeriesInstanceUID\": LENGTH_LIMIT_MATCH_REGEXP,\n    \"Exposures\": FLOAT_LIST_MATCH_REGEXP,\n    \"ContentTimes\": CONTENT_TIMES_LIST_MATCH_REGEXP,\n    \"t0\": FLOAT_MATCH_REGEXP,\n    \"t1\": FLOAT_MATCH_REGEXP,\n}\n\nHEADERS_MATCHING_NUM_TIMEPOINTS: List[str] = [\"Exposures\", \"ContentTimes\"]\n\nEXPECTED_HEADERS: List[str] = [\n    \"ObjectType\",\n    \"NDims\",\n    \"BinaryData\",\n    \"BinaryDataByteOrderMSB\",\n    \"CompressedData\",\n    \"CompressedDataSize\",\n    \"TransformMatrix\",\n    \"Offset\",\n    \"CenterOfRotation\",\n    \"AnatomicalOrientation\",\n    \"ElementSpacing\",\n    \"ElementNumberOfChannels\",\n    \"DimSize\",\n    \"ElementType\",\n    \"ElementDataFile\",\n]\n\n\ndef parse_mh_header(file: File) -> Mapping[str, Union[str, None]]:\n    \"\"\"\n    Attempts to parse the headers of an mhd file.\n\n    This function must be secure to safeguard against any untrusted uploaded\n    file.\n\n    Parameters\n    ----------\n    filename\n\n    Returns\n    -------\n        The extracted header from the mhd file as key value pairs.\n\n    Raises\n    ------\n    ValueError\n        Raised when the file contains problems making it impossible to\n        read.\n    \"\"\"\n\n    # attempt to limit number of read headers to prevent overflow attacks\n    read_line_limit = 10000\n\n    result = {}\n    with file.open(\"rb\") as f:\n        bin_line = True\n        while bin_line is not None:\n            read_line_limit -= 1\n            if read_line_limit < 0:\n                raise ValueError(\"Files contains too many header lines\")\n\n            bin_line = f.readline(10000)\n            if not bin_line:\n                bin_line = None\n                continue\n            if len(bin_line) >= 10000:\n                raise ValueError(\"Line length is too long\")\n\n            try:\n                line = bin_line.decode(\"utf-8\")\n            except UnicodeDecodeError:\n                raise ValueError(\"Header contains invalid UTF-8\")\n            else:\n                extract_key_value_pairs(line, result)\n            if \"ElementDataFile\" in result:\n                break  # last parsed header...\n    return result\n\n\ndef extract_key_value_pairs(line: str, result: Dict[str, str]):\n    line = line.rstrip(\"\\n\\r\")\n    if line.strip():\n        if \"=\" in line:\n            key, value = line.split(\"=\", 1)\n            result[key.strip()] = value.strip()\n        else:\n            result[line.strip()] = None\n\n\ndef extract_header_listing(\n    property: str, headers: Mapping[str, Union[str, None]], dtype: type = float\n) -> List[Any]:\n    return [dtype(e) for e in headers[property].strip().split(\" \")]\n\n\ndef load_sitk_image_with_nd_support(mhd_file: Path,) -> SimpleITK.Image:\n    headers = parse_mh_header(mhd_file)\n    is_mha = headers[\"ElementDataFile\"].strip() == \"LOCAL\"\n    data_file_path = resolve_mh_data_file_path(headers, is_mha, mhd_file)\n\n    shape = extract_header_listing(\"DimSize\", headers=headers, dtype=int)\n\n    dtype, num_components = determine_mh_components_and_dtype(headers)\n\n    sitk_image = create_sitk_img_from_mh_data(\n        data_file_path, dtype, headers, is_mha, num_components, shape\n    )\n\n    sitk_image.SetDirection(\n        extract_header_listing(\"TransformMatrix\", headers=headers)\n    )\n    sitk_image.SetSpacing(\n        extract_header_listing(\"ElementSpacing\", headers=headers)\n    )\n    sitk_image.SetOrigin(extract_header_listing(\"Offset\", headers=headers))\n\n    return sitk_image\n\n\ndef determine_mh_components_and_dtype(\n    headers: Mapping[str, Union[str, None]]\n) -> Tuple[int, int]:\n    num_components = 1\n    if \"ElementNumberOfChannels\" in headers:\n        num_components = int(headers[\"ElementNumberOfChannels\"])\n        if \"_ARRAY\" not in headers[\"ElementType\"] and num_components > 1:\n            headers[\"ElementType\"] = headers[\"ElementType\"] + \"_ARRAY\"\n    dtype = METAIO_IMAGE_TYPES[headers[\"ElementType\"]]\n    if dtype is None:\n        error_msg = (\n            f\"MetaIO datatype: {headers['ElementType']} is not supported\"\n        )\n        raise NotImplementedError(error_msg)\n    return dtype, num_components\n\n\ndef resolve_mh_data_file_path(\n    headers: Mapping[str, Union[str, None]], is_mha: bool, mhd_file: Path\n) -> Path:\n    if is_mha:\n        data_file_path = mhd_file\n    else:\n        data_file_path = (\n            mhd_file.resolve().parent / Path(headers[\"ElementDataFile\"]).name\n        )\n    if not data_file_path.exists():\n        raise IOError(\"cannot find data file\")\n    return data_file_path\n\n\ndef create_sitk_img_from_mh_data(\n    data_file_path: Path,\n    dtype: int,\n    headers: Mapping[str, Union[str, None]],\n    is_mha: bool,\n    num_components: int,\n    shape,\n) -> SimpleITK.Image:\n    is_compressed = headers[\"CompressedData\"] == \"True\"\n    with open(str(data_file_path), \"rb\") as f:\n        if is_mha:\n            line = \"\"\n            while \"ElementDataFile = LOCAL\" not in str(line):\n                line = f.readline()\n        if not is_compressed:\n            s = f.read()\n        else:\n            s = zlib.decompress(f.read())\n    sitk_image = SimpleITK.Image(shape, dtype, num_components)\n    _SimpleITK._SetImageFromArray(s, sitk_image)\n    return sitk_image\n\n\ndef validate_and_clean_additional_mh_headers(\n    headers: Mapping[str, Union[str, None]]\n) -> Mapping[str, Union[str, None]]:\n    cleaned_headers = {}\n    for key, value in headers.items():\n        if key in EXPECTED_HEADERS:\n            cleaned_headers[key] = value\n        else:\n            if key in ADDITIONAL_HEADERS:\n                match_pattern = ADDITIONAL_HEADERS[key]\n                if not re.match(match_pattern, value):\n                    raise ValueError(\n                        f\"Invalid data type found for \"\n                        f\"additional header key: {key}\"\n                    )\n                cleaned_headers[key] = value\n        if key in HEADERS_MATCHING_NUM_TIMEPOINTS:\n            validate_list_data_matches_num_timepoints(\n                headers=headers, key=key, value=value\n            )\n\n    return cleaned_headers\n\n\ndef validate_list_data_matches_num_timepoints(\n    headers: Mapping[str, Union[str, None]], key: str, value: str\n):\n    num_timepoints = len(value.split(\" \"))\n    expected_timepoints = (\n        int(headers[\"DimSize\"].split(\" \")[3])\n        if int(headers[\"NDims\"]) >= 4\n        else 1\n    )\n    if num_timepoints != expected_timepoints:\n        raise ValueError(\n            f\"Found {num_timepoints} values for {key}, \"\n            f\"but expected {expected_timepoints} (1/timepoint)\"\n        )\n\n\ndef add_additional_mh_headers_to_sitk_image(\n    sitk_image: SimpleITK.Image, headers: Mapping[str, Union[str, None]]\n):\n    cleaned_headers = validate_and_clean_additional_mh_headers(headers)\n    for header in ADDITIONAL_HEADERS:\n        if header in cleaned_headers:\n            value = cleaned_headers[header]\n            if isinstance(value, (list, tuple)):\n                value = \" \".format([str(v) for v in value])\n            else:\n                value = str(value)\n            sitk_image.SetMetaData(header, value)\n\n\ndef load_sitk_image(mhd_file: Path) -> SimpleITK.Image:\n    headers = parse_mh_header(mhd_file)\n    headers = validate_and_clean_additional_mh_headers(headers=headers)\n    ndims = int(headers[\"NDims\"])\n    if ndims < 4:\n        sitk_image = SimpleITK.ReadImage(str(mhd_file))\n        for key in sitk_image.GetMetaDataKeys():\n            if key not in ADDITIONAL_HEADERS:\n                sitk_image.EraseMetaData(key)\n    elif ndims <= 4:\n        sitk_image = load_sitk_image_with_nd_support(mhd_file=mhd_file)\n    else:\n        error_msg = (\n            \"SimpleITK images with more than 4 dimensions are not supported\"\n        )\n        raise NotImplementedError(error_msg)\n    add_additional_mh_headers_to_sitk_image(\n        sitk_image=sitk_image, headers=headers\n    )\n    return sitk_image\n", "path": "app/grandchallenge/cases/image_builders/metaio_utils.py"}]}