We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
libvips chokes on dzi conversion with sparse inputs
Upload a tif file which is sparse, in `tiff.py` line 212:

```
image = pyvips.Image.new_from_file(
            str(tiff_file.path.absolute()), access="sequential"
        )

        pyvips.Image.dzsave(
            image, dzi_output, tile_size=settings.DZI_TILE_SIZE
        )
```

The `dzsave` will fail with empty tiles, eg:

```
["Image can't be converted to dzi: unable to call dzsave\n  TIFFFillTile: 0: Invalid tile byte count, tile 1\nTIFFFillTile: 0: Invalid tile byte count, tile 3\nTIFFFillTile: 0: Invalid tile byte count, tile 4\nTIFFFillTile: 0: Invalid tile byte count, tile 5\nTIFFFillTile: 0: Invalid tile byte count, tile 6\nTIFFFillTile: 0: Invalid tile byte count, tile 7\nTIFFFillTile: 0: Invalid tile byte count, tile 8\nTIFFFillTile: 0: Invalid tile byte count, tile 9\nTIFFFillTile: 0: Invalid tile byte count, tile 10\nTIFFFillTile: 0: Invalid tile byte count, tile 11\nTIFFFillTile: 0: Invalid tile byte count, tile 12\nTIFFFillTile: 0: Invalid tile byte count, tile 13\nTIFFFillTile: 0: Invalid tile byte count, tile 14\nTIFFFillTile: 0: Invalid tile byte count, tile 15\nTIFFFillTile: 0: Invalid tile byte count, tile 16\nTIFFFillTile: 0: Invalid tile byte count, tile 17\nTIFFFillTile: 0: Invalid tile byte count, tile 18\nTIFFFillTile: 0: Invalid tile byte count, tile 19\nTIFFFillTile: 0: Invalid tile byte count, tile 20\nTIFFFillTile: 0: Invalid tile byte count, tile 23\n"]
```

Test image shared with Miriam on slack.

--- 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/tiff.py`
Content:
```
1 from pathlib import Path
2 from tempfile import TemporaryFile
3 from typing import NamedTuple
4 from uuid import uuid4
5 
6 import pyvips
7 import tifffile
8 from django.conf import settings
9 from django.core.exceptions import ValidationError
10 from django.core.files import File
11 
12 from grandchallenge.cases.image_builders import ImageBuilderResult
13 from grandchallenge.cases.models import FolderUpload, Image, ImageFile
14 
15 
16 class GrandChallengeTiffFileTags(NamedTuple):
17     image_width: int
18     image_height: int
19     resolution_levels: int
20     color_space: str
21 
22 
23 class GrandChallengeTiffFile(NamedTuple):
24     path: Path
25     tags: GrandChallengeTiffFileTags
26 
27 
28 def load_tiff_file(*, path: Path) -> GrandChallengeTiffFile:
29     """
30     Loads and validates a file using tifffile
31     :param path: The path to the potential tiff file
32     :return: A tiff file that can be used in the rest of grand challenge
33     """
34     try:
35         file = tifffile.TiffFile(str(path.absolute()))
36     except ValueError:
37         raise ValidationError("Image isn't a TIFF file")
38 
39     tags = _validate_tifffile(pages=file.pages)
40 
41     return GrandChallengeTiffFile(path=path, tags=tags)
42 
43 
44 def _validate_tifffile(  # noqa: C901
45     *, pages: tifffile.tifffile.TiffPages
46 ) -> GrandChallengeTiffFileTags:
47     """
48     Validates a tiff file loaded with tifffile for use in grand challenge
49     :param pages: The pages and tags from tiffile
50     :return: The extracted tags that are needed by the rest of the framework
51     """
52     required_tile_tags = ("TileOffsets", "TileByteCounts")
53 
54     forbidden_description_tags = ("dicom", "xml")
55 
56     tags = pages[0].tags
57 
58     # Checks if the image description exists,
59     # if so, ensure there's no DICOM or XML data
60     try:
61         image_description = str(tags["ImageDescription"].value).lower()
62         for forbidden in forbidden_description_tags:
63             if forbidden in image_description:
64                 raise ValidationError(
65                     "Image contains unauthorized information"
66                 )
67     except KeyError:
68         pass
69 
70     # Fails if the image doesn't have all required tile tags
71     if not all(tag in tags for tag in required_tile_tags):
72         raise ValidationError("Image has incomplete tile information")
73 
74     # Fails if the image only has a single resolution page
75     resolution_levels = len(pages)
76     if resolution_levels == 1:
77         raise ValidationError("Image only has a single resolution level")
78 
79     # Fails if the image doesn't have the chunky format
80     if str(tags["PlanarConfiguration"].value) != "PLANARCONFIG.CONTIG":
81         raise ValidationError(
82             "Image planar configuration isn't configured as 'Chunky' format"
83         )
84 
85     # Fails if the color space isn't supported
86     try:
87         color_space = get_color_space(
88             str(tags["PhotometricInterpretation"].value)
89         )
90     except KeyError:
91         raise ValidationError("Image lacks color space information")
92 
93     # Fails if the amount of bytes per sample doesn't correspond to the
94     # colour space
95     tif_color_channels = tags["SamplesPerPixel"].value
96     if Image.COLOR_SPACE_COMPONENTS[color_space] != tif_color_channels:
97         raise ValidationError("Image contains invalid amount of channels.")
98 
99     try:
100         image_width = tags["ImageWidth"].value
101         image_height = tags["ImageLength"].value
102     except KeyError:
103         raise ValidationError("Missing tags in tiff file")
104 
105     return GrandChallengeTiffFileTags(
106         image_width=image_width,
107         image_height=image_height,
108         color_space=color_space,
109         resolution_levels=resolution_levels,
110     )
111 
112 
113 def get_color_space(color_space_string) -> Image.COLOR_SPACES:
114     color_space_string = color_space_string.split(".")[1].upper()
115 
116     if color_space_string == "MINISBLACK":
117         color_space = Image.COLOR_SPACE_GRAY
118     else:
119         try:
120             color_space = dict(Image.COLOR_SPACES)[color_space_string]
121         except KeyError:
122             raise ValidationError("Invalid color space")
123 
124     return color_space
125 
126 
127 def image_builder_tiff(path: Path) -> ImageBuilderResult:
128     new_images = []
129     new_image_files = []
130     consumed_files = set()
131     invalid_file_errors = {}
132     new_folder_upload = []
133 
134     for file_path in path.iterdir():
135         pk = uuid4()
136 
137         try:
138             tiff_file = load_tiff_file(path=file_path)
139             dzi_output = create_dzi_images(tiff_file=tiff_file, pk=pk)
140         except ValidationError as e:
141             invalid_file_errors[file_path.name] = str(e)
142             continue
143 
144         image = create_tiff_image_entry(tiff_file=tiff_file, pk=pk)
145 
146         temp_file = TemporaryFile()
147         with open(tiff_file.path.absolute(), "rb") as open_file:
148             buffer = True
149             while buffer:
150                 buffer = open_file.read(1024)
151                 temp_file.write(buffer)
152 
153         new_image_files.append(
154             ImageFile(
155                 image=image,
156                 image_type=ImageFile.IMAGE_TYPE_TIFF,
157                 file=File(temp_file, name=f"{image.pk}.tif"),
158             )
159         )
160 
161         temp_dzi_file = TemporaryFile()
162         with open(dzi_output + ".dzi", "rb") as open_file:
163             buffer = True
164             while buffer:
165                 buffer = open_file.read(1024)
166                 temp_dzi_file.write(buffer)
167 
168         new_image_files.append(
169             ImageFile(
170                 image=image,
171                 image_type=ImageFile.IMAGE_TYPE_DZI,
172                 file=File(temp_dzi_file, name=f"{image.pk}.dzi"),
173             )
174         )
175 
176         dzi_folder_upload = FolderUpload(
177             folder=dzi_output + "_files", image=image
178         )
179         new_images.append(image)
180         consumed_files.add(tiff_file.path.name)
181         new_folder_upload.append(dzi_folder_upload)
182 
183     return ImageBuilderResult(
184         consumed_files=consumed_files,
185         file_errors_map=invalid_file_errors,
186         new_images=new_images,
187         new_image_files=new_image_files,
188         new_folder_upload=new_folder_upload,
189     )
190 
191 
192 def create_tiff_image_entry(*, tiff_file: GrandChallengeTiffFile, pk) -> Image:
193     # Builds a new Image model item
194     return Image(
195         pk=pk,
196         name=tiff_file.path.name,
197         width=tiff_file.tags.image_width,
198         height=tiff_file.tags.image_height,
199         depth=1,
200         resolution_levels=tiff_file.tags.resolution_levels,
201         color_space=tiff_file.tags.color_space,
202         eye_choice=Image.EYE_UNKNOWN,
203         stereoscopic_choice=Image.STEREOSCOPIC_UNKNOWN,
204         field_of_view=Image.FOV_UNKNOWN,
205     )
206 
207 
208 def create_dzi_images(*, tiff_file: GrandChallengeTiffFile, pk) -> str:
209     # Creates a dzi file(out.dzi) and corresponding tiles in folder {pk}_files
210     dzi_output = str(tiff_file.path.parent / str(pk))
211     try:
212         image = pyvips.Image.new_from_file(
213             str(tiff_file.path.absolute()), access="sequential"
214         )
215 
216         pyvips.Image.dzsave(
217             image, dzi_output, tile_size=settings.DZI_TILE_SIZE
218         )
219     except Exception as e:
220         raise ValidationError("Image can't be converted to dzi: " + str(e))
221 
222     return dzi_output
223
```
--- 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/tiff.py b/app/grandchallenge/cases/image_builders/tiff.py
--- a/app/grandchallenge/cases/image_builders/tiff.py
+++ b/app/grandchallenge/cases/image_builders/tiff.py
@@ -133,14 +133,18 @@
 
     for file_path in path.iterdir():
         pk = uuid4()
-
+        dzi_output = None
         try:
             tiff_file = load_tiff_file(path=file_path)
-            dzi_output = create_dzi_images(tiff_file=tiff_file, pk=pk)
         except ValidationError as e:
             invalid_file_errors[file_path.name] = str(e)
             continue
 
+        try:
+            dzi_output = create_dzi_images(tiff_file=tiff_file, pk=pk)
+        except ValidationError as e:
+            invalid_file_errors[file_path.name] = str(e)
+
         image = create_tiff_image_entry(tiff_file=tiff_file, pk=pk)
 
         temp_file = TemporaryFile()
@@ -158,27 +162,29 @@
             )
         )
 
-        temp_dzi_file = TemporaryFile()
-        with open(dzi_output + ".dzi", "rb") as open_file:
-            buffer = True
-            while buffer:
-                buffer = open_file.read(1024)
-                temp_dzi_file.write(buffer)
+        if dzi_output:
+            temp_dzi_file = TemporaryFile()
+            with open(dzi_output + ".dzi", "rb") as open_file:
+                buffer = True
+                while buffer:
+                    buffer = open_file.read(1024)
+                    temp_dzi_file.write(buffer)
+
+            new_image_files.append(
+                ImageFile(
+                    image=image,
+                    image_type=ImageFile.IMAGE_TYPE_DZI,
+                    file=File(temp_dzi_file, name=f"{image.pk}.dzi"),
+                )
+            )
 
-        new_image_files.append(
-            ImageFile(
-                image=image,
-                image_type=ImageFile.IMAGE_TYPE_DZI,
-                file=File(temp_dzi_file, name=f"{image.pk}.dzi"),
+            dzi_folder_upload = FolderUpload(
+                folder=dzi_output + "_files", image=image
             )
-        )
+            new_folder_upload.append(dzi_folder_upload)
 
-        dzi_folder_upload = FolderUpload(
-            folder=dzi_output + "_files", image=image
-        )
         new_images.append(image)
         consumed_files.add(tiff_file.path.name)
-        new_folder_upload.append(dzi_folder_upload)
 
     return ImageBuilderResult(
         consumed_files=consumed_files,

{"golden_diff": "diff --git a/app/grandchallenge/cases/image_builders/tiff.py b/app/grandchallenge/cases/image_builders/tiff.py\n--- a/app/grandchallenge/cases/image_builders/tiff.py\n+++ b/app/grandchallenge/cases/image_builders/tiff.py\n@@ -133,14 +133,18 @@\n \n     for file_path in path.iterdir():\n         pk = uuid4()\n-\n+        dzi_output = None\n         try:\n             tiff_file = load_tiff_file(path=file_path)\n-            dzi_output = create_dzi_images(tiff_file=tiff_file, pk=pk)\n         except ValidationError as e:\n             invalid_file_errors[file_path.name] = str(e)\n             continue\n \n+        try:\n+            dzi_output = create_dzi_images(tiff_file=tiff_file, pk=pk)\n+        except ValidationError as e:\n+            invalid_file_errors[file_path.name] = str(e)\n+\n         image = create_tiff_image_entry(tiff_file=tiff_file, pk=pk)\n \n         temp_file = TemporaryFile()\n@@ -158,27 +162,29 @@\n             )\n         )\n \n-        temp_dzi_file = TemporaryFile()\n-        with open(dzi_output + \".dzi\", \"rb\") as open_file:\n-            buffer = True\n-            while buffer:\n-                buffer = open_file.read(1024)\n-                temp_dzi_file.write(buffer)\n+        if dzi_output:\n+            temp_dzi_file = TemporaryFile()\n+            with open(dzi_output + \".dzi\", \"rb\") as open_file:\n+                buffer = True\n+                while buffer:\n+                    buffer = open_file.read(1024)\n+                    temp_dzi_file.write(buffer)\n+\n+            new_image_files.append(\n+                ImageFile(\n+                    image=image,\n+                    image_type=ImageFile.IMAGE_TYPE_DZI,\n+                    file=File(temp_dzi_file, name=f\"{image.pk}.dzi\"),\n+                )\n+            )\n \n-        new_image_files.append(\n-            ImageFile(\n-                image=image,\n-                image_type=ImageFile.IMAGE_TYPE_DZI,\n-                file=File(temp_dzi_file, name=f\"{image.pk}.dzi\"),\n+            dzi_folder_upload = FolderUpload(\n+                folder=dzi_output + \"_files\", image=image\n             )\n-        )\n+            new_folder_upload.append(dzi_folder_upload)\n \n-        dzi_folder_upload = FolderUpload(\n-            folder=dzi_output + \"_files\", image=image\n-        )\n         new_images.append(image)\n         consumed_files.add(tiff_file.path.name)\n-        new_folder_upload.append(dzi_folder_upload)\n \n     return ImageBuilderResult(\n         consumed_files=consumed_files,\n", "issue": "libvips chokes on dzi conversion with sparse inputs\nUpload a tif file which is sparse, in `tiff.py` line 212:\r\n\r\n```\r\nimage = pyvips.Image.new_from_file(\r\n            str(tiff_file.path.absolute()), access=\"sequential\"\r\n        )\r\n\r\n        pyvips.Image.dzsave(\r\n            image, dzi_output, tile_size=settings.DZI_TILE_SIZE\r\n        )\r\n```\r\n\r\nThe `dzsave` will fail with empty tiles, eg:\r\n\r\n```\r\n[\"Image can't be converted to dzi: unable to call dzsave\\n  TIFFFillTile: 0: Invalid tile byte count, tile 1\\nTIFFFillTile: 0: Invalid tile byte count, tile 3\\nTIFFFillTile: 0: Invalid tile byte count, tile 4\\nTIFFFillTile: 0: Invalid tile byte count, tile 5\\nTIFFFillTile: 0: Invalid tile byte count, tile 6\\nTIFFFillTile: 0: Invalid tile byte count, tile 7\\nTIFFFillTile: 0: Invalid tile byte count, tile 8\\nTIFFFillTile: 0: Invalid tile byte count, tile 9\\nTIFFFillTile: 0: Invalid tile byte count, tile 10\\nTIFFFillTile: 0: Invalid tile byte count, tile 11\\nTIFFFillTile: 0: Invalid tile byte count, tile 12\\nTIFFFillTile: 0: Invalid tile byte count, tile 13\\nTIFFFillTile: 0: Invalid tile byte count, tile 14\\nTIFFFillTile: 0: Invalid tile byte count, tile 15\\nTIFFFillTile: 0: Invalid tile byte count, tile 16\\nTIFFFillTile: 0: Invalid tile byte count, tile 17\\nTIFFFillTile: 0: Invalid tile byte count, tile 18\\nTIFFFillTile: 0: Invalid tile byte count, tile 19\\nTIFFFillTile: 0: Invalid tile byte count, tile 20\\nTIFFFillTile: 0: Invalid tile byte count, tile 23\\n\"]\r\n```\r\n\r\nTest image shared with Miriam on slack.\n", "before_files": [{"content": "from pathlib import Path\nfrom tempfile import TemporaryFile\nfrom typing import NamedTuple\nfrom uuid import uuid4\n\nimport pyvips\nimport tifffile\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.core.files import File\n\nfrom grandchallenge.cases.image_builders import ImageBuilderResult\nfrom grandchallenge.cases.models import FolderUpload, Image, ImageFile\n\n\nclass GrandChallengeTiffFileTags(NamedTuple):\n    image_width: int\n    image_height: int\n    resolution_levels: int\n    color_space: str\n\n\nclass GrandChallengeTiffFile(NamedTuple):\n    path: Path\n    tags: GrandChallengeTiffFileTags\n\n\ndef load_tiff_file(*, path: Path) -> GrandChallengeTiffFile:\n    \"\"\"\n    Loads and validates a file using tifffile\n    :param path: The path to the potential tiff file\n    :return: A tiff file that can be used in the rest of grand challenge\n    \"\"\"\n    try:\n        file = tifffile.TiffFile(str(path.absolute()))\n    except ValueError:\n        raise ValidationError(\"Image isn't a TIFF file\")\n\n    tags = _validate_tifffile(pages=file.pages)\n\n    return GrandChallengeTiffFile(path=path, tags=tags)\n\n\ndef _validate_tifffile(  # noqa: C901\n    *, pages: tifffile.tifffile.TiffPages\n) -> GrandChallengeTiffFileTags:\n    \"\"\"\n    Validates a tiff file loaded with tifffile for use in grand challenge\n    :param pages: The pages and tags from tiffile\n    :return: The extracted tags that are needed by the rest of the framework\n    \"\"\"\n    required_tile_tags = (\"TileOffsets\", \"TileByteCounts\")\n\n    forbidden_description_tags = (\"dicom\", \"xml\")\n\n    tags = pages[0].tags\n\n    # Checks if the image description exists,\n    # if so, ensure there's no DICOM or XML data\n    try:\n        image_description = str(tags[\"ImageDescription\"].value).lower()\n        for forbidden in forbidden_description_tags:\n            if forbidden in image_description:\n                raise ValidationError(\n                    \"Image contains unauthorized information\"\n                )\n    except KeyError:\n        pass\n\n    # Fails if the image doesn't have all required tile tags\n    if not all(tag in tags for tag in required_tile_tags):\n        raise ValidationError(\"Image has incomplete tile information\")\n\n    # Fails if the image only has a single resolution page\n    resolution_levels = len(pages)\n    if resolution_levels == 1:\n        raise ValidationError(\"Image only has a single resolution level\")\n\n    # Fails if the image doesn't have the chunky format\n    if str(tags[\"PlanarConfiguration\"].value) != \"PLANARCONFIG.CONTIG\":\n        raise ValidationError(\n            \"Image planar configuration isn't configured as 'Chunky' format\"\n        )\n\n    # Fails if the color space isn't supported\n    try:\n        color_space = get_color_space(\n            str(tags[\"PhotometricInterpretation\"].value)\n        )\n    except KeyError:\n        raise ValidationError(\"Image lacks color space information\")\n\n    # Fails if the amount of bytes per sample doesn't correspond to the\n    # colour space\n    tif_color_channels = tags[\"SamplesPerPixel\"].value\n    if Image.COLOR_SPACE_COMPONENTS[color_space] != tif_color_channels:\n        raise ValidationError(\"Image contains invalid amount of channels.\")\n\n    try:\n        image_width = tags[\"ImageWidth\"].value\n        image_height = tags[\"ImageLength\"].value\n    except KeyError:\n        raise ValidationError(\"Missing tags in tiff file\")\n\n    return GrandChallengeTiffFileTags(\n        image_width=image_width,\n        image_height=image_height,\n        color_space=color_space,\n        resolution_levels=resolution_levels,\n    )\n\n\ndef get_color_space(color_space_string) -> Image.COLOR_SPACES:\n    color_space_string = color_space_string.split(\".\")[1].upper()\n\n    if color_space_string == \"MINISBLACK\":\n        color_space = Image.COLOR_SPACE_GRAY\n    else:\n        try:\n            color_space = dict(Image.COLOR_SPACES)[color_space_string]\n        except KeyError:\n            raise ValidationError(\"Invalid color space\")\n\n    return color_space\n\n\ndef image_builder_tiff(path: Path) -> ImageBuilderResult:\n    new_images = []\n    new_image_files = []\n    consumed_files = set()\n    invalid_file_errors = {}\n    new_folder_upload = []\n\n    for file_path in path.iterdir():\n        pk = uuid4()\n\n        try:\n            tiff_file = load_tiff_file(path=file_path)\n            dzi_output = create_dzi_images(tiff_file=tiff_file, pk=pk)\n        except ValidationError as e:\n            invalid_file_errors[file_path.name] = str(e)\n            continue\n\n        image = create_tiff_image_entry(tiff_file=tiff_file, pk=pk)\n\n        temp_file = TemporaryFile()\n        with open(tiff_file.path.absolute(), \"rb\") as open_file:\n            buffer = True\n            while buffer:\n                buffer = open_file.read(1024)\n                temp_file.write(buffer)\n\n        new_image_files.append(\n            ImageFile(\n                image=image,\n                image_type=ImageFile.IMAGE_TYPE_TIFF,\n                file=File(temp_file, name=f\"{image.pk}.tif\"),\n            )\n        )\n\n        temp_dzi_file = TemporaryFile()\n        with open(dzi_output + \".dzi\", \"rb\") as open_file:\n            buffer = True\n            while buffer:\n                buffer = open_file.read(1024)\n                temp_dzi_file.write(buffer)\n\n        new_image_files.append(\n            ImageFile(\n                image=image,\n                image_type=ImageFile.IMAGE_TYPE_DZI,\n                file=File(temp_dzi_file, name=f\"{image.pk}.dzi\"),\n            )\n        )\n\n        dzi_folder_upload = FolderUpload(\n            folder=dzi_output + \"_files\", image=image\n        )\n        new_images.append(image)\n        consumed_files.add(tiff_file.path.name)\n        new_folder_upload.append(dzi_folder_upload)\n\n    return ImageBuilderResult(\n        consumed_files=consumed_files,\n        file_errors_map=invalid_file_errors,\n        new_images=new_images,\n        new_image_files=new_image_files,\n        new_folder_upload=new_folder_upload,\n    )\n\n\ndef create_tiff_image_entry(*, tiff_file: GrandChallengeTiffFile, pk) -> Image:\n    # Builds a new Image model item\n    return Image(\n        pk=pk,\n        name=tiff_file.path.name,\n        width=tiff_file.tags.image_width,\n        height=tiff_file.tags.image_height,\n        depth=1,\n        resolution_levels=tiff_file.tags.resolution_levels,\n        color_space=tiff_file.tags.color_space,\n        eye_choice=Image.EYE_UNKNOWN,\n        stereoscopic_choice=Image.STEREOSCOPIC_UNKNOWN,\n        field_of_view=Image.FOV_UNKNOWN,\n    )\n\n\ndef create_dzi_images(*, tiff_file: GrandChallengeTiffFile, pk) -> str:\n    # Creates a dzi file(out.dzi) and corresponding tiles in folder {pk}_files\n    dzi_output = str(tiff_file.path.parent / str(pk))\n    try:\n        image = pyvips.Image.new_from_file(\n            str(tiff_file.path.absolute()), access=\"sequential\"\n        )\n\n        pyvips.Image.dzsave(\n            image, dzi_output, tile_size=settings.DZI_TILE_SIZE\n        )\n    except Exception as e:\n        raise ValidationError(\"Image can't be converted to dzi: \" + str(e))\n\n    return dzi_output\n", "path": "app/grandchallenge/cases/image_builders/tiff.py"}], "after_files": [{"content": "from pathlib import Path\nfrom tempfile import TemporaryFile\nfrom typing import NamedTuple\nfrom uuid import uuid4\n\nimport pyvips\nimport tifffile\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.core.files import File\n\nfrom grandchallenge.cases.image_builders import ImageBuilderResult\nfrom grandchallenge.cases.models import FolderUpload, Image, ImageFile\n\n\nclass GrandChallengeTiffFileTags(NamedTuple):\n    image_width: int\n    image_height: int\n    resolution_levels: int\n    color_space: str\n\n\nclass GrandChallengeTiffFile(NamedTuple):\n    path: Path\n    tags: GrandChallengeTiffFileTags\n\n\ndef load_tiff_file(*, path: Path) -> GrandChallengeTiffFile:\n    \"\"\"\n    Loads and validates a file using tifffile\n    :param path: The path to the potential tiff file\n    :return: A tiff file that can be used in the rest of grand challenge\n    \"\"\"\n    try:\n        file = tifffile.TiffFile(str(path.absolute()))\n    except ValueError:\n        raise ValidationError(\"Image isn't a TIFF file\")\n\n    tags = _validate_tifffile(pages=file.pages)\n\n    return GrandChallengeTiffFile(path=path, tags=tags)\n\n\ndef _validate_tifffile(  # noqa: C901\n    *, pages: tifffile.tifffile.TiffPages\n) -> GrandChallengeTiffFileTags:\n    \"\"\"\n    Validates a tiff file loaded with tifffile for use in grand challenge\n    :param pages: The pages and tags from tiffile\n    :return: The extracted tags that are needed by the rest of the framework\n    \"\"\"\n    required_tile_tags = (\"TileOffsets\", \"TileByteCounts\")\n\n    forbidden_description_tags = (\"dicom\", \"xml\")\n\n    tags = pages[0].tags\n\n    # Checks if the image description exists,\n    # if so, ensure there's no DICOM or XML data\n    try:\n        image_description = str(tags[\"ImageDescription\"].value).lower()\n        for forbidden in forbidden_description_tags:\n            if forbidden in image_description:\n                raise ValidationError(\n                    \"Image contains unauthorized information\"\n                )\n    except KeyError:\n        pass\n\n    # Fails if the image doesn't have all required tile tags\n    if not all(tag in tags for tag in required_tile_tags):\n        raise ValidationError(\"Image has incomplete tile information\")\n\n    # Fails if the image only has a single resolution page\n    resolution_levels = len(pages)\n    if resolution_levels == 1:\n        raise ValidationError(\"Image only has a single resolution level\")\n\n    # Fails if the image doesn't have the chunky format\n    if str(tags[\"PlanarConfiguration\"].value) != \"PLANARCONFIG.CONTIG\":\n        raise ValidationError(\n            \"Image planar configuration isn't configured as 'Chunky' format\"\n        )\n\n    # Fails if the color space isn't supported\n    try:\n        color_space = get_color_space(\n            str(tags[\"PhotometricInterpretation\"].value)\n        )\n    except KeyError:\n        raise ValidationError(\"Image lacks color space information\")\n\n    # Fails if the amount of bytes per sample doesn't correspond to the\n    # colour space\n    tif_color_channels = tags[\"SamplesPerPixel\"].value\n    if Image.COLOR_SPACE_COMPONENTS[color_space] != tif_color_channels:\n        raise ValidationError(\"Image contains invalid amount of channels.\")\n\n    try:\n        image_width = tags[\"ImageWidth\"].value\n        image_height = tags[\"ImageLength\"].value\n    except KeyError:\n        raise ValidationError(\"Missing tags in tiff file\")\n\n    return GrandChallengeTiffFileTags(\n        image_width=image_width,\n        image_height=image_height,\n        color_space=color_space,\n        resolution_levels=resolution_levels,\n    )\n\n\ndef get_color_space(color_space_string) -> Image.COLOR_SPACES:\n    color_space_string = color_space_string.split(\".\")[1].upper()\n\n    if color_space_string == \"MINISBLACK\":\n        color_space = Image.COLOR_SPACE_GRAY\n    else:\n        try:\n            color_space = dict(Image.COLOR_SPACES)[color_space_string]\n        except KeyError:\n            raise ValidationError(\"Invalid color space\")\n\n    return color_space\n\n\ndef image_builder_tiff(path: Path) -> ImageBuilderResult:\n    new_images = []\n    new_image_files = []\n    consumed_files = set()\n    invalid_file_errors = {}\n    new_folder_upload = []\n\n    for file_path in path.iterdir():\n        pk = uuid4()\n        dzi_output = None\n        try:\n            tiff_file = load_tiff_file(path=file_path)\n        except ValidationError as e:\n            invalid_file_errors[file_path.name] = str(e)\n            continue\n\n        try:\n            dzi_output = create_dzi_images(tiff_file=tiff_file, pk=pk)\n        except ValidationError as e:\n            invalid_file_errors[file_path.name] = str(e)\n\n        image = create_tiff_image_entry(tiff_file=tiff_file, pk=pk)\n\n        temp_file = TemporaryFile()\n        with open(tiff_file.path.absolute(), \"rb\") as open_file:\n            buffer = True\n            while buffer:\n                buffer = open_file.read(1024)\n                temp_file.write(buffer)\n\n        new_image_files.append(\n            ImageFile(\n                image=image,\n                image_type=ImageFile.IMAGE_TYPE_TIFF,\n                file=File(temp_file, name=f\"{image.pk}.tif\"),\n            )\n        )\n\n        if dzi_output:\n            temp_dzi_file = TemporaryFile()\n            with open(dzi_output + \".dzi\", \"rb\") as open_file:\n                buffer = True\n                while buffer:\n                    buffer = open_file.read(1024)\n                    temp_dzi_file.write(buffer)\n\n            new_image_files.append(\n                ImageFile(\n                    image=image,\n                    image_type=ImageFile.IMAGE_TYPE_DZI,\n                    file=File(temp_dzi_file, name=f\"{image.pk}.dzi\"),\n                )\n            )\n\n            dzi_folder_upload = FolderUpload(\n                folder=dzi_output + \"_files\", image=image\n            )\n            new_folder_upload.append(dzi_folder_upload)\n\n        new_images.append(image)\n        consumed_files.add(tiff_file.path.name)\n\n    return ImageBuilderResult(\n        consumed_files=consumed_files,\n        file_errors_map=invalid_file_errors,\n        new_images=new_images,\n        new_image_files=new_image_files,\n        new_folder_upload=new_folder_upload,\n    )\n\n\ndef create_tiff_image_entry(*, tiff_file: GrandChallengeTiffFile, pk) -> Image:\n    # Builds a new Image model item\n    return Image(\n        pk=pk,\n        name=tiff_file.path.name,\n        width=tiff_file.tags.image_width,\n        height=tiff_file.tags.image_height,\n        depth=1,\n        resolution_levels=tiff_file.tags.resolution_levels,\n        color_space=tiff_file.tags.color_space,\n        eye_choice=Image.EYE_UNKNOWN,\n        stereoscopic_choice=Image.STEREOSCOPIC_UNKNOWN,\n        field_of_view=Image.FOV_UNKNOWN,\n    )\n\n\ndef create_dzi_images(*, tiff_file: GrandChallengeTiffFile, pk) -> str:\n    # Creates a dzi file(out.dzi) and corresponding tiles in folder {pk}_files\n    dzi_output = str(tiff_file.path.parent / str(pk))\n    try:\n        image = pyvips.Image.new_from_file(\n            str(tiff_file.path.absolute()), access=\"sequential\"\n        )\n\n        pyvips.Image.dzsave(\n            image, dzi_output, tile_size=settings.DZI_TILE_SIZE\n        )\n    except Exception as e:\n        raise ValidationError(\"Image can't be converted to dzi: \" + str(e))\n\n    return dzi_output\n", "path": "app/grandchallenge/cases/image_builders/tiff.py"}]}