Add Radiant Viewer as embedded viewer to .zip Archive download ?

This isn’t directly related to Orthanc, but could be a nice addition for a Python Plug-in.

Radiant has a tool that allows one run the Radiant Viewer from a USB with an
unzipped Archive folder added to the package that they supply here:

https://www.radiantviewer.com/dicom-viewer-manual/recording-dicom-cd_dvd.html

It requires a license, but it does run with a watermark without one.

It is apparently possible to override the Archive Downloading in a Python Plug-in Script
with something like that shown below. There is some stuff for logging included, but that could be ignored (bold).

I’m not very experienced with manipulating .zip archives in Python, but it seems like
there should be a way to:

  1. Create a ‘blank’ .zip in memory.

  2. Add the unzipped Radiant package to the new .zip Archive.

  3. Add the Study .zip to the new .zip Archive.

  4. Return the new .zip archive that has the Radiant Viewer and the Study .zip.

Unzipping the new download would have the Radiant Viewer files and the Study .zip at the root of the unzipped Radiant Folder. The Study has to actually be unzipped (I think) in order for the Viewer to work, and it has to be run from a USB/CD.

The idea would be to create an archive that could very easily allow one to use the
Radiant Viewer as an embedded viewer when downloading study Archives.

There is a reference on SO that has some suggestions about how to do that ?

https://stackoverflow.com/questions/2463770/python-in-memory-zip-library

Intercept native method to enable logging, .zip archive

def OnDownloadStudyArchive(output, uri, **request):

host = “Not Defined”
userprofilejwt = “Not Defined”
if “headers” in request and “host” in request[‘headers’]:
host = request[‘headers’][‘host’]
if “headers” in request and “userprofilejwt” in request[‘headers’]:
userprofilejwt = request[‘headers’][‘userprofilejwt’]
logging.info(“STUDY|DOWNLOAD_ARCHIVE|ID=” + request[‘groups’][0] + " HOST=" + host + " PROFILE= " + userprofilejwt)
archive = orthanc.RestApiGet(uri)
output.AnswerBuffer(archive, ‘application/zip’)

orthanc.RegisterRestCallback(‘/studies/(.*)/archive’, OnDownloadStudyArchive)

Just wondering if anyone has experience with that sort of thing and if that seems feasible.

Stephen D. Scotti

Progress on this. This might actually work:

You’ll need these:

import io
from io import BytesIO
import zipfile
from zipfile
import ZipFile, ZipInfo

Intercept native method to enable logging, .zip archive

def OnDownloadStudyArchive(output, uri, **request):

orthanc.LogWarning(‘Creating Archive Bundled With Radiant Viewer’)
new_zip = BytesIO()

archive = orthanc.RestApiGet(uri)

‘/python/radiant_cd.zip’ is the location of the .zip archive for the Radiant CD/USB viewer, accessible from the script.

https://www.radiantviewer.com/dicom-viewer-manual/recording-dicom-cd_dvd.html

with ZipFile(‘/python/radiant_cd.zip’, ‘r’) as radiant_zip:
with ZipFile(new_zip, ‘w’) as new_archive:
for item in radiant_zip.filelist:
new_archive.writestr(item, radiant_zip.read(item.filename))
new_archive.writestr(‘archive.zip’, archive)
output.AnswerBuffer(new_zip.getvalue(), ‘application/zip’)

orthanc.RegisterRestCallback(‘/studies/(.*)/archive’, OnDownloadStudyArchive)

That generates an Archive named “Duck^Donald^Quack.zip” on my system, and when unzipped has the Radiant Viewer Files and a file called archive.zip.
In order to allow the viewer to work, you need to also extract the archive.zip file so it is unstuffed, probably after moving them to a USB, or before writing to a CD.

To open the Viewer (Windows OS only), launch it with ‘run’, or it might also autolaunch.

If you have a license file, that could be added to the reference radiant_cd.zip package on the server.

sscotti@Stephens-iMac Duck^Donald^Quack % ls -l

drwxr-xr-x@ 8 sscotti staff 256 Oct 1 10:00 COMMON
drwxr-xr-x@ 59 sscotti staff 1888 Aug 18 03:16 RA32
drwxr-xr-x@ 58 sscotti staff 1856 Aug 18 03:16 RA64
-rw-------@ 1 sscotti staff 9620397 Oct 19 05:20 archive.zip
-rw-r–r–@ 1 sscotti staff 200 Oct 19 2017 autorun.inf
-rw-r–r–@ 1 sscotti staff 2127 Nov 30 2017 readme.txt
-rw-r–r–@ 1 sscotti staff 37 Oct 19 2017 run.bat

Stephen D. Scotti

Thanks for the example, I slightly updated it to this form:

def OnDownloadStudyZIPWithViewer(output, uri, **request):
    """Bundle DICOM viewer in ZIP archive
    """
    oid = uri.split("/")[2]
    orthanc.LogInfo(f"{OnDownloadStudyZIPWithViewer.__doc__.strip()} {oid}")
    archive_data = BytesIO(orthanc.RestApiGet(uri.replace(zip_action, "media")))
    with ZipFile(archive_data, "a") as archive_zip:
        for root, _, files in walk(viewer_path):
            prefix = Path(root)
            for viewer_file in files:
                name = str(prefix / viewer_file)
                archive_zip.write(name, arcname=name.replace(viewer_path, ""),
                                  compress_type=ZIP_DEFLATED)
    output.SetHttpHeader("Content-Disposition", f'attachment; filename="{oid}-viewer.zip"')
    output.AnswerBuffer(archive_data.getvalue(), "application/zip")

The rationale is that since I know I’ll be providing files from the archive I do not want to got throught the uncompress phase every time, so just place everything on the fs and point at it in the config file with

    "MyAppName": {
        "viewer-path": "/usr/local/share/orthanc/resources/ViewerDirectory"
    },

then in your module global scope:

zip_action = "zip"
config = json.loads(orthanc.GetConfiguration())
viewer_path = config.get("MyAppName", {}).get("viewer-path")

if viewer_path:
    orthanc.LogInfo("Registering embedded DICOM viewer API extension")
    orthanc.RegisterRestCallback(f"/studies/(.*)/{zip_action}", OnDownloadStudyZIPWithViewer)
1 Like