large-image-source-dicom 1.27.1.dev42__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.
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/PKG-INFO +1 -1
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py +183 -28
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/girder_source.py +3 -4
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/PKG-INFO +1 -1
- large-image-source-dicom-1.27.1.dev57/large_image_source_dicom.egg-info/requires.txt +5 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/test_dicom/web_client_specs/dicomWebSpec.js +35 -1
- large-image-source-dicom-1.27.1.dev42/large_image_source_dicom.egg-info/requires.txt +0 -5
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/LICENSE +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/README.rst +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/__init__.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/assetstore/__init__.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/assetstore/rest.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/dicom_metadata.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/dicom_tags.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/dicomweb_utils.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/girder_plugin.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/constants.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/main.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/models/AssetstoreModel.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/package.json +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/routes.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/assetstoreImport.pug +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreCreate.pug +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreEditFields.pug +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreImportButton.pug +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreMixins.pug +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/AssetstoresView.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/AuthOptions.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/DICOMwebImportView.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/EditAssetstoreWidget.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom/web_client/views/NewAssetstoreWidget.js +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/SOURCES.txt +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/dependency_links.txt +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/entry_points.txt +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/large_image_source_dicom.egg-info/top_level.txt +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/pyproject.toml +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/setup.cfg +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/setup.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/test_dicom/__init__.py +0 -0
- {large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/test_dicom/test_web_client.py +0 -0
|
@@ -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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 =
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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':
|
|
70
|
-
'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'),
|
|
@@ -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
|
-
|
|
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
|
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{large-image-source-dicom-1.27.1.dev42 → large-image-source-dicom-1.27.1.dev57}/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|