Skip to content

feat: download dicomweb data to local file system #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,19 @@ dicomwebjs dump https://d33do7qe4w26qo.cloudfront.net/dicomweb/studies/1.3.6.1.4
dicomwebjs dump testdata/studies/1.2.276.1.74.1.2.132733202464108492637644434464108492/series/2.16.840.1.113883.3.8467.132733202477512857637644434477512857/metadata.gz
```

## File Locations
## download `url`
dicomwebjs can be used to download a directory of file locations. By default it will fetch everything at and below the specified URL, plus referenced bulkdata. Bulkdata will be
placed in the `studies/<studyUID>/bulkdata` directory, with metadata references to the bulkdata using the `../../bulkdata/<path>` relative URI locations.



### Example Commands
```
dicomwebjs download https://d33do7qe4w26qo.cloudfront.net/dicomweb/studies/1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627 -d ~/dicomweb
```


### File Locations
Files dicomweb can be paths to JSON files. However, tree structure data must follow the Static DICOMweb format, specifically starting at `studies/` relative to the base directory, and containing some/all of the ones below.
Note that un-compressed files are acceptable as well, but will not be found on a search.

Expand Down
71 changes: 50 additions & 21 deletions bin/dicomwebjs.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,64 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { dicomweb, instanceDicom, dumpDicom } from '../src/index.js';
import { Command } from "commander"
import { dicomweb, instanceDicom, dumpDicom, utils } from "../src/index.js"

const program = new Command();
const program = new Command()

program.option(
"-s, --study <studyInstanceUID>",
"Download a specific study instance UID"
)

program
.name('dicomwebjs')
.description('dicomwebjs based tools for manipulation of DICOMweb')
.version('0.0.1')
.option('--seriesUID <seriesUID>', 'For a specific seriesUID');
.name("dicomwebjs")
.description("dicomwebjs based tools for manipulation of DICOMweb")
.version("0.0.1")
.option("--seriesUID <seriesUID>", "For a specific seriesUID")

program.command('dump')
.description('Dump a dicomweb file')
.argument('<dicomwebUrl>', 'dicomweb URL or file location')
program
.command("dump")
.description("Dump a dicomweb file")
.argument("<dicomwebUrl>", "dicomweb URL or file location")
.option("--debug", "Set debug level logging")
.action(async (fileName, options) => {
const qido = await dicomweb.readDicomWeb(fileName, options);
const qido = await dicomweb.readDicomWeb(fileName, options)
for (const dict of qido) {
dumpDicom({ dict });
dumpDicom({ dict })
}
});
})

program.command('instance')
.description('Write the instance data')
.argument('<part10>', 'part 10 file')
.option('-p, --pretty', 'Pretty print')
program
.command("download")
.description("Download dicomweb file(s)")
.argument("<dicomwebUrl>", "dicomweb URL to the base DICOMweb service")
.option(
"-d, --directory <dicomwebdir>",
"Download to local DICOMweb directory"
)
.option("--debug", "Set debug level logging")
.action(async (fileName, options) => {
const qido = await dicomweb.readDicomWeb(fileName, options);
for (const dict of qido) {
instanceDicom({ dict }, options);
utils.logger.setOptions(options)
let downloadUrls = [fileName]
if (options.study) {
downloadUrls = await dicomweb.queryDownloads(fileName, options)
}
for (const downloadUrl of downloadUrls) {
const data = await dicomweb.readDicomWeb(downloadUrl, options)
dicomweb.store(downloadUrl, data, options)
}
})

program
.command("instance")
.description("Write the instance data")
.argument("<part10>", "part 10 file")
.option("-p, --pretty", "Pretty print")
.option("--debug", "Set debug level logging")
.action(async (fileName, options) => {
const qido = await dicomweb.readDicomWeb(fileName, options)
for (const dict of qido) {
instanceDicom({ dict }, options)
}
})

program.parse();
program.parse()
1,030 changes: 1,030 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

24 changes: 20 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,29 @@
},
"dependencies": {
"commander": "^12.1.0",
"config-point": ">=0.5.1",
"dcmjs": "^0.29.5",
"dcmjs-dimse": "0.1.27",
"dicomweb-client": "0.8.4"
"dicomweb-client": "0.8.4",
"loglevel": "^1.9.2"
},
"devDependencies": {
"eslint": "^9.6.0",
"husky": "^1.3.1",
"jest": "^29.7.0",
"must": "^0.13.4"
}
"lint-staged": "^13.1.2",
"must": "^0.13.4",
"prettier": "^3.3.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,json,css}": [
"prettier --write",
"git add"
]
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
11 changes: 0 additions & 11 deletions src/dcmjs-commands.code-workspace

This file was deleted.

60 changes: 53 additions & 7 deletions src/dicomweb.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import zlib from 'zlib';
import fs from 'fs';
import zlib from "zlib";
import fs from "fs";
import dcmjs from "dcmjs";

import { httprequest } from './webRetrieve.js';
import { httprequest } from "./webRetrieve.js";
import { logger } from "./utils/index.js";

const { DicomMessage, DicomMetaDictionary } = dcmjs.data;

const log = logger.commandsLog;

/**
* The dicomweb support classes for querying and reading various DICOMweb data sources
*/

export function readDicomWeb(url, options = {}) {
if (url.startsWith('http')) {
if (url.startsWith("http")) {
return readDicomWebHttp(url, options);
}
return readDicomWebFile(url, options);
Expand All @@ -14,11 +24,47 @@ export function readDicomWebHttp(url, options) {
return httprequest(url, options);
}


export function readDicomWebFile(fileName, _options) {
const isGzip = fileName.endsWith('.gz');
const isGzip = fileName.endsWith(".gz");
const arrayBuffer = fs.readFileSync(fileName).buffer;
const uncompressed = isGzip ? zlib.gunzipSync(arrayBuffer) : arrayBuffer;
const str = uncompressed.toString();
return JSON.parse(str);
}
}

export function getStudyQuery(wadoUrl, _options, forStudy) {
const { StudyInstanceUID } = forStudy;
console.log("Querying for study", StudyInstanceUID);
return `${wadoUrl}?StudyInstanceUID=${StudyInstanceUID}`;
}

export function getSeriesQuery(wadoUrl, options, forStudy) {
const { StudyInstanceUID } = forStudy;
return `${wadoUrl}/studies/${StudyInstanceUID}/series`;
}

export async function queryDownloads(wadoUrl, options) {
const StudyInstanceUID = options.study;
const SeriesInstanceUID = options.series || [];
const query = { StudyInstanceUID, SeriesInstanceUID };
const studyQuery = getStudyQuery(wadoUrl, options);
const study = await readDicomWeb(studyQuery)?.[0];
const downloaded = [];
if (!study) {
log.warn("No study found for", options.study);
return downloaded;
}
downloaded.push({
relativePath: `studies/${StudyInstanceUID}`,
data: [study],
});

const series = await readDicomWeb(getSeriesQuery(wadoUrl, options, query));
log.debug("Found series", series);
return [];
}

export function store(path, data, options) {
log.info("Storing data", path);
log.debug(JSON.stringify(data, null, 2));
}
37 changes: 20 additions & 17 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from "fs";
import dcmjs from "dcmjs";

export * as dicomweb from './dicomweb.js';
export * as utils from "./utils/index.js";
export * as dicomweb from "./dicomweb.js";

const { DicomMessage, DicomMetaDictionary } = dcmjs.data;

Expand All @@ -20,23 +21,23 @@ export function dumpDicom(dicomDict, options = {}) {
dumpData(dicomDict.dict, options);
}

export function dumpData(data, options, indent = '') {
if (typeof data !== 'object') {
export function dumpData(data, options, indent = "") {
if (typeof data !== "object") {
return;
}
const keys = Object.keys(data).sort();
for (const key of keys) {
const value = data[key]
const value = data[key];
if (!value) {
continue;
}
const { vr } = value;
const punctuatedTag = DicomMetaDictionary.punctuateTag(key);
const entry = DicomMetaDictionary.dictionary[punctuatedTag];
const name = entry?.name || '';
if (vr === 'SQ') {
const name = entry?.name || "";
if (vr === "SQ") {
console.log(indent, key, name);
dumpSq(name || key, value, options, indent + ' ');
dumpSq(name || key, value, options, indent + " ");
continue;
}
console.log(indent, key, name, valueToString(value, options));
Expand All @@ -46,7 +47,7 @@ export function dumpData(data, options, indent = '') {
export function valueToString(value, options) {
const { Value: values, vr, InlineBinary, BulkDataURI } = value;
if (InlineBinary) {
return `Inline Binary ${InlineBinary.substring(0, Math.min(InlineBinary.length, 32))}${InlineBinary.length > 31 ? '...' : ''} (${InlineBinary.length * 3 / 4})`
return `Inline Binary ${InlineBinary.substring(0, Math.min(InlineBinary.length, 32))}${InlineBinary.length > 31 ? "..." : ""} (${(InlineBinary.length * 3) / 4})`;
}
if (BulkDataURI) {
return `URL ${BulkDataURI}`;
Expand All @@ -56,20 +57,20 @@ export function valueToString(value, options) {
return vr;
}
console.log("***** Value = ", value);
return '';
return "";
}
if (values.length === 0) return '';
if (values.length === 0) return "";
const [v0] = values;
if (v0 instanceof ArrayBuffer) {
return `ArrayBuffer of length ${values.length}`;
}
if (typeof v0 === 'object') {
return values.map(it => JSON.stringify(it)).join(', ');
if (typeof v0 === "object") {
return values.map((it) => JSON.stringify(it)).join(", ");
}
if (!Array.isArray(values)) {
return JSON.stringify(values);
}
return values.map(it => String(it)).join(', ');
return values.map((it) => String(it)).join(", ");
}

export function dumpSq(tag, value, options, indent) {
Expand All @@ -80,13 +81,15 @@ export function dumpSq(tag, value, options, indent) {
}
for (let i = 0; i < sq.length; i++) {
console.log(indent, "Item #", i + 1);
dumpData(sq[i], options, indent + ' ');
dumpData(sq[i], options, indent + " ");
}
console.log(indent, 'End of', tag, "with", sq.length, 'items');
console.log(indent, "End of", tag, "with", sq.length, "items");
}

export function instanceDicom(dicomDict, options = {}) {
const { pretty } = options;
const result = pretty ? JSON.stringify(dicomDict.dict, null, 2) : JSON.stringify(dicomDict.dict);
console.log('', result);
const result = pretty
? JSON.stringify(dicomDict.dict, null, 2)
: JSON.stringify(dicomDict.dict);
console.log("", result);
}
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as logger from "./logger.js";
45 changes: 45 additions & 0 deletions src/utils/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import loglevel from "loglevel";

/**
* Gets a logger and adds a getLogger function to id to get child loggers.
* This looks like the loggers in the unreleased loglevel 2.0 and is intended
* for forwards compatibility.
*/
export function getRootLogger(name) {
const logger = loglevel.getLogger(name[0]);
logger.getLogger = (...names) => {
return getRootLogger(`${name}.${names.join(".")}`);
};
return logger;
}

/** Gets a nested logger.
* This will eventually inherit the level from the parent level, but right now
* it doesn't
*/
export function getLogger(...name) {
return getRootLogger(name.join("."));
}

/** Pre-setup cateogires for easy logging, by package name */
export const dcmjsLog = getLogger("dcmjs");
export const commandsLog = dcmjsLog.getLogger("commands");

/** Dicom issue log is for reporting inconsistencies and issues with DICOM logging */
export const dicomIssueLog = getLogger("dicom", "issue");

export function setOptions(options) {
if (options.loglevel) {
loglevel.setLevel(options.loglevel);
} else if (options.debug) {
console.log("Setting loglevel to debug");
loglevel.setLevel("debug");
console.log(
"commands level is",
loglevel.getLogger("dcmjs", "commands").getLevel(),
);
} else {
loglevel.setLevel("info");
}
loglevel.rebuild();
}
Loading