You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ADIT has no way to look at pixel data. A user who only has ADIT access (the brainstorming example: a PhD student transferring studies for a professor, without an account on the PACS viewer) cannot check what a series actually contains before transferring it. The explorer stops at series-level text metadata, selective transfer offers a study zip download (#151), and the DICOMweb API is not usable from a browser session. The ask is a quick preview to sanity-check a series, not a diagnostic viewer. Low priority.
No issue in openradx/adit, openradx/radis or openradx/adit-radis-shared asks for a viewer or preview (titles and bodies searched for viewer, preview, OHIF, cornerstone, thumbnail on 2026-09-25). TODO.md:92 lists "Preview uploaded images" under the upload portal, which is the same UI component with a different data source.
Current behaviour
The explorer URL tree ends at .../series/<series_uid>/ (adit/dicom_explorer/urls.py:49-53). The series page renders four text cards (series_detail.html:14-17); _series_infos.html shows UID, number, description, modality and image count. dicom_explorer.js contains only "use strict";.
DicomDataCollector wraps find_patients/find_studies/find_series only and passes the 3 s DICOM_EXPLORER_RESPONSE_TIMEOUT (adit/settings/base.py:455) as the DIMSE timeout (dicom_data_collector.py:12-13). Every explorer request runs in an executor under asyncio.wait_for with the same 3 s (views.py:71-86); on timeout the thread is not cancelled.
Access: dicom_explorer.query_dicom_server (models.py:13-17, dummy PermissionSupport model) plus accessible_by_user(user, "source") of the active group (views.py:56,135). DicomExplorerLockedMixin exists but is unused (mixins.py:6-8, # TODO: Use in dicom explorer).
DicomOperator.find_images (adit/core/utils/dicom_operator.py:321) and fetch_image (:433-466, WADO-RS > C-GET > C-MOVE) exist but are not used by the explorer. Both need a PatientID: fetch_image takes it as a positional argument, find_images raises without it on patient-root-only servers (:355-361). For a single instance, _fetch_images_with_c_move short-circuits on SOPInstanceUID (:578-579), so no image-level C-FIND is needed, but the receiver round trip (:673) and the 30 s C_MOVE_DOWNLOAD_TIMEOUT inactivity window (base.py:458, :743) still apply.
The DICOMweb API cannot serve a browser preview: token-only auth (base.py:229-231, Authorization: Token), all_groups=True access semantics (dicom_web/views.py:54) unlike the explorer, can_retrieve not enforced (:196), no /frames, /rendered, /thumbnail or WADO-URI route (dicom_web/urls.py:28), multipart-only retrieve responses (views.py:204), and /metadata fetches full pixel data before stripping it (views.py:262-268).
The Download a single study directly in the web browser #151 download (selective_transfer/views.py:61-63, PR Allow direct download of a study from selective transfer query results #248) is the closest precedent: login_required async view, selective_transfer.can_download_study (selective_transfer/models.py:8-11), route carrying patients/<patient_id> (selective_transfer/urls.py:33), accessible_by_user(user, "source") in the downloader (dicom_downloader.py:153). The upload app is the precedent for same-origin session-authenticated browser calls (upload/views.py:96-114, upload.js:364-371).
Vendored JS: dcmjs 0.30.3, dicom-web-anonymizer 0.3.0, dicomweb-client 0.5.2, committed under adit/upload/static/vendor and refreshed by copy_statics (cli.py:56-74), all loaded as classic <script src> globals (upload_layout.html:5-7). No cornerstone/OHIF/dwv anywhere. Production statics use whitenoise CompressedManifestStaticFilesStorage (settings/production.py:16).
Proposal
Staged, so stage 1 can be accepted alone.
Instance listing, no pixels. Add collect_images to DicomDataCollector and an instance list on the series page (lazy via HTMX or a .../instances/ route): InstanceNumber, SOPClassUID, ImageType, Rows/Columns, NumberOfFrames, sorted by InstanceNumber, limited via limit_results (a CT series can have 1000+ instances; DICOM_EXPLORER_RESULT_LIMIT is 101, base.py:451). This already answers "is this the series I think it is" and has no privacy implications beyond today's explorer.
Single-instance endpoint + minimal preview. A login_required async view returning one instance as application/dicom (write_dataset to BytesIO), route carrying patient_id like the download route, same server access check as the explorer (active group), own timeout setting (tens of seconds, not the 3 s C-FIND timeout), Cache-Control: no-store, no pseudonymization (consistent with the explorer's identifying metadata). Client: Preview button on the series page, middle instance first, Prev/Next over the stage-1 list, default window/level, "No preview available" for non-image SOP classes and on errors. Run the fetch via sync_to_async(thread_sensitive=False) like DicomDownloader (dicom_downloader.py:113), not the explorer's uncancelled executor pattern.
Permission. A per-instance application/dicom endpoint is, by construction, a scriptable full-series download; gating it with query_dicom_server alone would undo the query/download separation Download a single study directly in the web browser #151 introduced, and unlike the zip download it is unpseudonymized and never passes EXCLUDE_MODALITIES (dicom_downloader.py:156-158). Require selective_transfer.can_download_study, or add dicom_explorer.can_preview_images? Either way existing groups get nothing automatically; every deployment must grant it.
Which servers. Offer preview on every source server, or only where WADO-RS or C-GET is supported? On C-MOVE-only servers each slice is a receiver round trip of seconds and can block up to 30 s; fast scrolling spawns concurrent C-MOVEs against the PACS. A per-user concurrency cap or a short-lived per-series cache (temporary PHI on the web node) may be needed from the start.
JS delivery. cornerstone3D (ESM, wasm codecs, web workers loaded by relative URL, which manifest hashing renames) fits the current vendoring badly; legacy cornerstone-core + cornerstone-wado-image-loader + dicom-parser ship UMD bundles that fit the <script src> setup. Not verified against current npm builds. A viewer library also should not live under adit/upload/static/vendor.
Scope. Single slice with prev/next and window/level only? Multi-frame, enhanced CT/MR, compressed transfer syntaxes and SR/PR/KO/SEG need explicit handling; MODALITIES_EXCLUDED_FROM_NIFTI_CONVERSION (base.py:522) could be generalised for the "no preview" set.
Locked/suspended. Should the new view honour DicomExplorerSettings.locked and fix the unused-mixin TODO in passing?
Usage accounting. The DICOMweb API records APIUsage (dicom_web/models.py:18-32); the explorer and download record nothing. Should preview fetches be counted?
Implementation notes
Options, cheapest first:
(a) Instance metadata listing only (stage 1). Pure C-FIND, no new permission, no JS.
(b) Per-server viewer URL template on DicomServer plus a link-out button, mirroring the RADIS pacs_link. Cheap, but does not serve users without a PACS viewer account, which is the stated use case, and overlaps with the brainstorming bullets on the RADIS PACS viewer URL and opening RADIS reports from ADIT; better handled as one cross-referenced "viewer link" issue than folded in here.
(c) Server-rendered PNG thumbnail via pydicom pixel_array + pillow. No JS, but pillow and numpy are only transitive (uv.lock:2063, :2332), no pylibjpeg/gdcm is locked, so JPEG-family syntaxes are not guaranteed to decode, and VOI LUT / MONOCHROME1 handling is needed. pydicom is 3.0.2 (uv.lock:2799-2800).
(d) Explorer-scoped application/dicom endpoint + cornerstone panel (stage 2, recommended). Reuses fetch_image and the Download a single study directly in the web browser #151 view pattern, works for PACS without DICOMweb, keeps the DICOMweb API and its permission TODOs untouched, and the same client component can later serve TODO.md:92. Same-origin requests carry the session cookie; only GET is used so CSRF does not apply.
(e) Session auth on the existing WADO views + client-side multipart parsing with the vendored dicomweb-client. Widens the API auth surface and inherits all_groups=True and the unenforced can_retrieve. Not recommended.
(f) OHIF over ADIT's DICOMweb. Needs cheap series /metadata, /frames or WADO-URI, thumbnails, CORS or same-origin hosting, an auth story (OHIF's ?token= query parameter per its docs; header format vs ADIT's Token scheme not verified) and iframe embedding against XFrameOptionsMiddleware (base.py:106). On a C-MOVE PACS every frame is a C-MOVE. Out of scope.
Tests: explorer tests use AsyncClient with mocker.patch("adit.dicom_explorer.views.DicomDataCollector") (tests/test_views.py:81); a view calling DicomOperator directly needs another patch target. Add: permission denied for a user with query_dicom_server only, a Cache-Control assertion, an SR instance returning "no preview", and one acceptance test against ORTHANC1 like dicom_web/tests/acceptance/test_wadors.py. Docs: docs/user-docs/user-guide.md:185-191 (section 7), features.md.
All of the above is from reading the checkout at f30d498; the app was not run.
Motivation
ADIT has no way to look at pixel data. A user who only has ADIT access (the brainstorming example: a PhD student transferring studies for a professor, without an account on the PACS viewer) cannot check what a series actually contains before transferring it. The explorer stops at series-level text metadata, selective transfer offers a study zip download (#151), and the DICOMweb API is not usable from a browser session. The ask is a quick preview to sanity-check a series, not a diagnostic viewer. Low priority.
No issue in openradx/adit, openradx/radis or openradx/adit-radis-shared asks for a viewer or preview (titles and bodies searched for viewer, preview, OHIF, cornerstone, thumbnail on 2026-09-25).
TODO.md:92lists "Preview uploaded images" under the upload portal, which is the same UI component with a different data source.Current behaviour
.../series/<series_uid>/(adit/dicom_explorer/urls.py:49-53). The series page renders four text cards (series_detail.html:14-17);_series_infos.htmlshows UID, number, description, modality and image count.dicom_explorer.jscontains only"use strict";.DicomDataCollectorwrapsfind_patients/find_studies/find_seriesonly and passes the 3 sDICOM_EXPLORER_RESPONSE_TIMEOUT(adit/settings/base.py:455) as the DIMSE timeout (dicom_data_collector.py:12-13). Every explorer request runs in an executor underasyncio.wait_forwith the same 3 s (views.py:71-86); on timeout the thread is not cancelled.dicom_explorer.query_dicom_server(models.py:13-17, dummyPermissionSupportmodel) plusaccessible_by_user(user, "source")of the active group (views.py:56,135).DicomExplorerLockedMixinexists but is unused (mixins.py:6-8,# TODO: Use in dicom explorer).DicomOperator.find_images(adit/core/utils/dicom_operator.py:321) andfetch_image(:433-466, WADO-RS > C-GET > C-MOVE) exist but are not used by the explorer. Both need aPatientID:fetch_imagetakes it as a positional argument,find_imagesraises without it on patient-root-only servers (:355-361). For a single instance,_fetch_images_with_c_moveshort-circuits onSOPInstanceUID(:578-579), so no image-level C-FIND is needed, but the receiver round trip (:673) and the 30 sC_MOVE_DOWNLOAD_TIMEOUTinactivity window (base.py:458,:743) still apply.base.py:229-231,Authorization: Token),all_groups=Trueaccess semantics (dicom_web/views.py:54) unlike the explorer,can_retrievenot enforced (:196), no/frames,/rendered,/thumbnailor WADO-URI route (dicom_web/urls.py:28), multipart-only retrieve responses (views.py:204), and/metadatafetches full pixel data before stripping it (views.py:262-268).selective_transfer/views.py:61-63, PR Allow direct download of a study from selective transfer query results #248) is the closest precedent:login_requiredasync view,selective_transfer.can_download_study(selective_transfer/models.py:8-11), route carryingpatients/<patient_id>(selective_transfer/urls.py:33),accessible_by_user(user, "source")in the downloader (dicom_downloader.py:153). The upload app is the precedent for same-origin session-authenticated browser calls (upload/views.py:96-114,upload.js:364-371).adit/upload/static/vendorand refreshed bycopy_statics(cli.py:56-74), all loaded as classic<script src>globals (upload_layout.html:5-7). No cornerstone/OHIF/dwv anywhere. Production statics use whitenoiseCompressedManifestStaticFilesStorage(settings/production.py:16).Proposal
Staged, so stage 1 can be accepted alone.
collect_imagestoDicomDataCollectorand an instance list on the series page (lazy via HTMX or a.../instances/route): InstanceNumber, SOPClassUID, ImageType, Rows/Columns, NumberOfFrames, sorted by InstanceNumber, limited vialimit_results(a CT series can have 1000+ instances;DICOM_EXPLORER_RESULT_LIMITis 101,base.py:451). This already answers "is this the series I think it is" and has no privacy implications beyond today's explorer.login_requiredasync view returning one instance asapplication/dicom(write_datasettoBytesIO), route carryingpatient_idlike the download route, same server access check as the explorer (active group), own timeout setting (tens of seconds, not the 3 s C-FIND timeout),Cache-Control: no-store, no pseudonymization (consistent with the explorer's identifying metadata). Client: Preview button on the series page, middle instance first, Prev/Next over the stage-1 list, default window/level, "No preview available" for non-image SOP classes and on errors. Run the fetch viasync_to_async(thread_sensitive=False)likeDicomDownloader(dicom_downloader.py:113), not the explorer's uncancelled executor pattern.Open questions
application/dicomendpoint is, by construction, a scriptable full-series download; gating it withquery_dicom_serveralone would undo the query/download separation Download a single study directly in the web browser #151 introduced, and unlike the zip download it is unpseudonymized and never passesEXCLUDE_MODALITIES(dicom_downloader.py:156-158). Requireselective_transfer.can_download_study, or adddicom_explorer.can_preview_images? Either way existing groups get nothing automatically; every deployment must grant it.<script src>setup. Not verified against current npm builds. A viewer library also should not live underadit/upload/static/vendor.MODALITIES_EXCLUDED_FROM_NIFTI_CONVERSION(base.py:522) could be generalised for the "no preview" set.DicomExplorerSettings.lockedand fix the unused-mixin TODO in passing?APIUsage(dicom_web/models.py:18-32); the explorer and download record nothing. Should preview fetches be counted?Implementation notes
Options, cheapest first:
DicomServerplus a link-out button, mirroring the RADISpacs_link. Cheap, but does not serve users without a PACS viewer account, which is the stated use case, and overlaps with the brainstorming bullets on the RADIS PACS viewer URL and opening RADIS reports from ADIT; better handled as one cross-referenced "viewer link" issue than folded in here.pixel_array+ pillow. No JS, but pillow and numpy are only transitive (uv.lock:2063,:2332), no pylibjpeg/gdcm is locked, so JPEG-family syntaxes are not guaranteed to decode, and VOI LUT / MONOCHROME1 handling is needed. pydicom is 3.0.2 (uv.lock:2799-2800).application/dicomendpoint + cornerstone panel (stage 2, recommended). Reusesfetch_imageand the Download a single study directly in the web browser #151 view pattern, works for PACS without DICOMweb, keeps the DICOMweb API and its permission TODOs untouched, and the same client component can later serveTODO.md:92. Same-origin requests carry the session cookie; only GET is used so CSRF does not apply.all_groups=Trueand the unenforcedcan_retrieve. Not recommended./metadata,/framesor WADO-URI, thumbnails, CORS or same-origin hosting, an auth story (OHIF's?token=query parameter per its docs; header format vs ADIT'sTokenscheme not verified) and iframe embedding againstXFrameOptionsMiddleware(base.py:106). On a C-MOVE PACS every frame is a C-MOVE. Out of scope.Tests: explorer tests use
AsyncClientwithmocker.patch("adit.dicom_explorer.views.DicomDataCollector")(tests/test_views.py:81); a view callingDicomOperatordirectly needs another patch target. Add: permission denied for a user withquery_dicom_serveronly, aCache-Controlassertion, an SR instance returning "no preview", and one acceptance test against ORTHANC1 likedicom_web/tests/acceptance/test_wadors.py. Docs:docs/user-docs/user-guide.md:185-191(section 7),features.md.All of the above is from reading the checkout at f30d498; the app was not run.
Related
dicom_web/views.py:94,196can_query/can_retrieveTODOs andTODO.md:20; the recommended option does not depend on them.TODO.md:92"Preview uploaded images".pacs_link(radis/reports/templates/reports/_report_buttons_panel.html:20-27) for option (b).