large-image-source-dicom 1.27.1.dev44__tar.gz → 1.27.1.dev57__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/PKG-INFO +1 -1
  2. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py +183 -28
  3. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/girder_source.py +3 -4
  4. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/PKG-INFO +1 -1
  5. large-image-source-dicom-1.27.1.dev57/large_image_source_dicom.egg-info/requires.txt +5 -0
  6. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/test_dicom/web_client_specs/dicomWebSpec.js +35 -1
  7. large-image-source-dicom-1.27.1.dev44/large_image_source_dicom.egg-info/requires.txt +0 -5
  8. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/LICENSE +0 -0
  9. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/README.rst +0 -0
  10. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/__init__.py +0 -0
  11. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/assetstore/__init__.py +0 -0
  12. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/assetstore/rest.py +0 -0
  13. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/dicom_metadata.py +0 -0
  14. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/dicom_tags.py +0 -0
  15. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/dicomweb_utils.py +0 -0
  16. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/girder_plugin.py +0 -0
  17. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/constants.js +0 -0
  18. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/main.js +0 -0
  19. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/models/AssetstoreModel.js +0 -0
  20. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/package.json +0 -0
  21. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/routes.js +0 -0
  22. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/assetstoreImport.pug +0 -0
  23. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreCreate.pug +0 -0
  24. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreEditFields.pug +0 -0
  25. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreImportButton.pug +0 -0
  26. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreMixins.pug +0 -0
  27. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/AssetstoresView.js +0 -0
  28. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/AuthOptions.js +0 -0
  29. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/DICOMwebImportView.js +0 -0
  30. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/EditAssetstoreWidget.js +0 -0
  31. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/NewAssetstoreWidget.js +0 -0
  32. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/SOURCES.txt +0 -0
  33. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/dependency_links.txt +0 -0
  34. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/entry_points.txt +0 -0
  35. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/top_level.txt +0 -0
  36. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/pyproject.toml +0 -0
  37. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/setup.cfg +0 -0
  38. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/setup.py +0 -0
  39. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/test_dicom/__init__.py +0 -0
  40. {large-image-source-dicom-1.27.1.dev44 → large-image-source-dicom-1.27.1.dev57}/test_dicom/test_web_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-dicom
3
- Version: 1.27.1.dev44
3
+ Version: 1.27.1.dev57
4
4
  Summary: A DICOM tilesource for large_image.
5
5
  Home-page: https://github.com/girder/large_image
6
6
  Author: Kitware, Inc.
@@ -3,6 +3,7 @@ from large_image_source_dicom.dicom_tags import dicom_key_to_tag
3
3
  from large_image_source_dicom.dicomweb_utils import get_dicomweb_metadata
4
4
  from requests.exceptions import HTTPError
5
5
 
6
+ from girder.api.rest import setContentDisposition, setResponseHeader
6
7
  from girder.exceptions import ValidationException
7
8
  from girder.models.file import File
8
9
  from girder.models.folder import Folder
@@ -11,6 +12,8 @@ from girder.utility.abstract_assetstore_adapter import AbstractAssetstoreAdapter
11
12
 
12
13
  DICOMWEB_META_KEY = 'dicomweb_meta'
13
14
 
15
+ BUF_SIZE = 65536
16
+
14
17
 
15
18
  class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
16
19
  """
@@ -104,20 +107,163 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
104
107
 
105
108
  def downloadFile(self, file, offset=0, headers=True, endByte=None,
106
109
  contentDisposition=None, extraParameters=None, **kwargs):
107
- # FIXME: do we want to support downloading files? We probably
108
- # wouldn't download them the regular way, but we could instead
109
- # use a dicomweb-client like so:
110
- # instance = client.retrieve_instance(
111
- # study_instance_uid=...,
112
- # series_instance_uid=...,
113
- # sop_instance_uid=...,
114
- # )
115
- # pydicom.filewriter.write_file('output_name.dcm', instance)
116
- msg = 'Download support not yet implemented for DICOMweb files.'
117
- raise NotImplementedError(
118
- msg,
110
+
111
+ if offset != 0 or endByte is not None:
112
+ # FIXME: implement range requests
113
+ msg = 'Range requests are not yet implemented'
114
+ raise NotImplementedError(msg)
115
+
116
+ from dicomweb_client.web import _Transaction
117
+
118
+ dicom_uids = file['dicom_uids']
119
+ study_uid = dicom_uids['study_uid']
120
+ series_uid = dicom_uids['series_uid']
121
+ instance_uid = dicom_uids['instance_uid']
122
+
123
+ client = _create_dicomweb_client(self.assetstore_meta)
124
+
125
+ if headers:
126
+ setResponseHeader('Content-Type', file['mimeType'])
127
+ setContentDisposition(file['name'], contentDisposition or 'attachment')
128
+
129
+ # The filesystem assetstore calls the following function, which sets
130
+ # the above and also sets the range and content-length headers:
131
+ # `self.setContentHeaders(file, offset, endByte, contentDisposition)`
132
+ # However, we can't call that since we don't have a great way of
133
+ # determining the DICOM file size without downloading the whole thing.
134
+ # FIXME: call that function if we find a way to determine file size.
135
+
136
+ # Create the URL
137
+ url = client._get_instances_url(
138
+ _Transaction.RETRIEVE,
139
+ study_uid,
140
+ series_uid,
141
+ instance_uid,
119
142
  )
120
143
 
144
+ # Build the headers
145
+ transfer_syntax = '*'
146
+ accept_parts = [
147
+ 'multipart/related',
148
+ 'type="application/dicom"',
149
+ f'transfer-syntax={transfer_syntax}',
150
+ ]
151
+ headers = {
152
+ 'Accept': '; '.join(accept_parts),
153
+ }
154
+
155
+ def stream():
156
+ # Perform the request
157
+ response = client._http_get(url, headers=headers, stream=True)
158
+ for chunk in self._stream_retrieve_instance_response(response):
159
+ yield chunk
160
+
161
+ return stream
162
+
163
+ def _extract_media_type_and_boundary(self, response):
164
+ content_type = response.headers['content-type']
165
+ media_type, *ct_info = [ct.strip() for ct in content_type.split(';')]
166
+ boundary = None
167
+ for item in ct_info:
168
+ attr, _, value = item.partition('=')
169
+ if attr.lower() == 'boundary':
170
+ boundary = value.strip('"').encode()
171
+ break
172
+
173
+ return media_type, boundary
174
+
175
+ def _stream_retrieve_instance_response(self, response):
176
+ # The first part of this function was largely copied from dicomweb-client's
177
+ # _decode_multipart_message() function. But we can't use that function here
178
+ # because it relies on reading the whole DICOM file into memory. We want to
179
+ # avoid that and stream in chunks.
180
+
181
+ # Split the content-type to find the media type and boundary.
182
+ media_type, boundary = self._extract_media_type_and_boundary(response)
183
+ if media_type.lower() != 'multipart/related':
184
+ msg = f'Unexpected media type: "{media_type}". Expected "multipart/related".'
185
+ raise ValueError(msg)
186
+
187
+ # Ensure we have the multipart/related boundary.
188
+ # The beginning boundary and end boundary look slightly different (in my
189
+ # examples, beginning looks like '--{boundary}\r\n', and ending looks like
190
+ # '\r\n--{boundary}--'). But we skip over the beginning boundary anyways
191
+ # since it is before the message body. An end boundary might look like this:
192
+ # \r\n--50d7ccd118978542c422543a7156abfce929e7615bc024e533c85801cd77--
193
+ if boundary is None:
194
+ content_type = response.headers['content-type']
195
+ msg = f'Failed to locate boundary in content-type: {content_type}'
196
+ raise ValueError(msg)
197
+
198
+ # Both dicomweb-client and requests-toolbelt check for
199
+ # the ending boundary exactly like so:
200
+ ending = b'\r\n--' + boundary
201
+
202
+ # Sometimes, there are a few extra bytes after the ending, such
203
+ # as '--' and '\r\n'. Imaging Data Commons has '--\r\n' at the end.
204
+ # But we don't care about what comes after the ending. As soon as we
205
+ # encounter the ending, we are done.
206
+ ending_size = len(ending)
207
+
208
+ # Make sure the buffer is at least large enough to contain the
209
+ # ending_size - 1, so that the ending cannot be split between more than 2 chunks.
210
+ buffer_size = max(BUF_SIZE, ending_size - 1)
211
+
212
+ with response:
213
+ # Create our iterator
214
+ iterator = response.iter_content(buffer_size)
215
+
216
+ # First, stream until we encounter the first `\r\n\r\n`,
217
+ # which denotes the end of the header section.
218
+ header_found = False
219
+ end_header_delimiter = b'\r\n\r\n'
220
+ for chunk in iterator:
221
+ if end_header_delimiter in chunk:
222
+ idx = chunk.index(end_header_delimiter)
223
+ # Save the first section of data. We will yield it later.
224
+ prev_chunk = chunk[idx + len(end_header_delimiter):]
225
+ header_found = True
226
+ break
227
+
228
+ if not header_found:
229
+ msg = 'Failed to find header in response content'
230
+ raise ValueError(msg)
231
+
232
+ # Now the header has been finished. Stream the data until
233
+ # we encounter the ending boundary or finish the data.
234
+ # The "prev_chunk" will start out set to the section right after the header.
235
+ for chunk in iterator:
236
+ # Ensure the chunk is large enough to contain the ending_size - 1, so
237
+ # we can be sure the ending won't be split across more than 2 chunks.
238
+ while len(chunk) < ending_size - 1:
239
+ try:
240
+ chunk += next(iterator)
241
+ except StopIteration:
242
+ break
243
+
244
+ # Check if the ending is split between the previous and current chunks.
245
+ if ending in prev_chunk + chunk[:ending_size - 1]:
246
+ # We found the ending! Remove the ending boundary and return.
247
+ data = prev_chunk + chunk[:ending_size - 1]
248
+ yield data.split(ending, maxsplit=1)[0]
249
+ return
250
+
251
+ if prev_chunk:
252
+ yield prev_chunk
253
+
254
+ prev_chunk = chunk
255
+
256
+ # We did not find the ending while looping.
257
+ # Check if it is in the final chunk.
258
+ if ending in prev_chunk:
259
+ # Found the ending in the final chunk.
260
+ yield prev_chunk.split(ending, maxsplit=1)[0]
261
+ return
262
+
263
+ # We should have encountered the ending earlier and returned
264
+ msg = 'Failed to find ending boundary in response content'
265
+ raise ValueError(msg)
266
+
121
267
  def importData(self, parent, parentType, params, progress, user, **kwargs):
122
268
  """
123
269
  Import DICOMweb WSI instances from a DICOMweb server.
@@ -155,6 +301,7 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
155
301
 
156
302
  study_uid_key = dicom_key_to_tag('StudyInstanceUID')
157
303
  series_uid_key = dicom_key_to_tag('SeriesInstanceUID')
304
+ instance_uid_key = dicom_key_to_tag('SOPInstanceUID')
158
305
 
159
306
  # We are only searching for WSI datasets. Ignore all others.
160
307
  # FIXME: is this actually working? For the SLIM server at
@@ -192,25 +339,33 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
192
339
 
193
340
  # Set the DICOMweb metadata
194
341
  item['dicomweb_meta'] = get_dicomweb_metadata(client, study_uid, series_uid)
195
- item = Item().save(item)
196
-
197
- # Create a placeholder file with the same name
198
- file = File().createFile(
199
- name=f'{series_uid}.dcm',
200
- creator=user,
201
- item=item,
202
- reuseExisting=True,
203
- assetstore=self.assetstore,
204
- mimeType=None,
205
- size=0,
206
- saveFile=False,
207
- )
208
- file['dicomweb_meta'] = {
342
+ item['dicom_uids'] = {
209
343
  'study_uid': study_uid,
210
344
  'series_uid': series_uid,
211
345
  }
212
- file['imported'] = True
213
- File().save(file)
346
+ item = Item().save(item)
347
+
348
+ instance_results = client.search_for_instances(study_uid, series_uid)
349
+ for instance in instance_results:
350
+ instance_uid = instance[instance_uid_key]['Value'][0]
351
+
352
+ file = File().createFile(
353
+ name=f'{instance_uid}.dcm',
354
+ creator=user,
355
+ item=item,
356
+ reuseExisting=True,
357
+ assetstore=self.assetstore,
358
+ mimeType='application/dicom',
359
+ size=None,
360
+ saveFile=False,
361
+ )
362
+ file['dicom_uids'] = {
363
+ 'study_uid': study_uid,
364
+ 'series_uid': series_uid,
365
+ 'instance_uid': instance_uid,
366
+ }
367
+ file['imported'] = True
368
+ File().save(file)
214
369
 
215
370
  items.append(item)
216
371
 
@@ -59,15 +59,14 @@ class DICOMGirderTileSource(DICOMFileTileSource, GirderTileSource):
59
59
 
60
60
  def _getDICOMwebLargeImagePath(self, assetstore):
61
61
  meta = assetstore[DICOMWEB_META_KEY]
62
- file = Item().childFiles(self.item, limit=1)[0]
63
- file_meta = file['dicomweb_meta']
62
+ item_uids = self.item['dicom_uids']
64
63
 
65
64
  adapter = assetstore_utilities.getAssetstoreAdapter(assetstore)
66
65
 
67
66
  return {
68
67
  'url': meta['url'],
69
- 'study_uid': file_meta['study_uid'],
70
- 'series_uid': file_meta['series_uid'],
68
+ 'study_uid': item_uids['study_uid'],
69
+ 'series_uid': item_uids['series_uid'],
71
70
  # The following are optional
72
71
  'qido_prefix': meta.get('qido_prefix'),
73
72
  'wado_prefix': meta.get('wado_prefix'),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-dicom
3
- Version: 1.27.1.dev44
3
+ Version: 1.27.1.dev57
4
4
  Summary: A DICOM tilesource for large_image.
5
5
  Home-page: https://github.com/girder/large_image
6
6
  Author: Kitware, Inc.
@@ -0,0 +1,5 @@
1
+ large-image>=1.27.1.dev57
2
+ wsidicom>=0.9.0
3
+
4
+ [girder]
5
+ girder-large-image>=1.27.1.dev57
@@ -13,9 +13,12 @@ describe('DICOMWeb assetstore', function () {
13
13
  'Admin',
14
14
  'Admin',
15
15
  'adminpassword!'));
16
+
16
17
  it('Create an assetstore and import data', function () {
17
18
  var destinationId;
18
19
  var destinationType;
20
+ var itemId;
21
+ var fileId;
19
22
 
20
23
  // After importing, we will verify that this item exists
21
24
  const verifyItemName = '1.3.6.1.4.1.5962.99.1.3205815762.381594633.1639588388306.2.0';
@@ -195,7 +198,38 @@ describe('DICOMWeb assetstore', function () {
195
198
  }
196
199
  }).responseJSON.item;
197
200
 
198
- return items.length > 0 && items[0].largeImage !== undefined;
201
+ if (items.length === 0 || items[0].largeImage === undefined) {
202
+ return false;
203
+ }
204
+
205
+ // Save the itemId, and the file id
206
+ itemId = items[0]['_id'];
207
+ fileId = items[0].largeImage.fileId;
208
+ return true
199
209
  }, 'Wait for large images to be present');
210
+
211
+ // Verify that we can download the item
212
+ waitsFor(function () {
213
+ const resp = girder.rest.restRequest({
214
+ url: 'item/' + itemId + '/download',
215
+ type: 'GET',
216
+ async: false,
217
+ });
218
+
219
+ // Should be larger than 10 million bytes
220
+ return resp.status === 200 && resp.responseText.length > 10000000;
221
+ }, 'Wait to download all DICOM files in the item');
222
+
223
+ // Verify that we can download a single file
224
+ waitsFor(function () {
225
+ const resp = girder.rest.restRequest({
226
+ url: 'file/' + fileId + '/download',
227
+ type: 'GET',
228
+ async: false,
229
+ });
230
+
231
+ // Should be larger than 500k bytes
232
+ return resp.status === 200 && resp.responseText.length > 500000;
233
+ }, 'Wait to download a single DICOM file');
200
234
  });
201
235
  });
@@ -1,5 +0,0 @@
1
- large-image>=1.27.1.dev44
2
- wsidicom>=0.9.0
3
-
4
- [girder]
5
- girder-large-image>=1.27.1.dev44