Skip to content
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
155 changes: 126 additions & 29 deletions packages/create-dicomweb/bin/createdicomweb.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
#!/usr/bin/env bun
import { createRequire } from 'module';
import { Command } from 'commander';
import { instanceMain, seriesMain, studyMain, createMain, stowMain, thumbnailMain, indexMain } from '../lib/index.mjs';
import {
instanceMain,
seriesMain,
studyMain,
createMain,
stowMain,
thumbnailMain,
indexMain,
} from '../lib/index.mjs';
import {
handleHomeRelative,
createVerboseLog,
Expand All @@ -11,9 +19,10 @@ import {

const require = createRequire(import.meta.url);
const pkg = require('../package.json');
const runtime = typeof process !== 'undefined' && process.versions?.bun
? `Bun ${process.versions.bun}`
: `Node ${process.versions?.node ?? 'unknown'}`;
const runtime =
typeof process !== 'undefined' && process.versions?.bun
? `Bun ${process.versions.bun}`
: `Node ${process.versions?.node ?? 'unknown'}`;

const program = new Command();

Expand All @@ -29,15 +38,23 @@ const updateVerboseLog = () => {
program
.name('createdicomweb')
.description('dcmjs based tools for creation of metadata files')
.version(`${pkg.version}\n${runtime}`, '-V, --version', 'output create-dicomweb and runtime (Bun/Node) version')
.version(
`${pkg.version}\n${runtime}`,
'-V, --version',
'output create-dicomweb and runtime (Bun/Node) version'
)
.option('-v, --verbose', 'Enable verbose logging')
.option('-q, --quiet', 'Disable noQuiet logging');

program
.command('instance')
.description('Store instance data')
.argument('<part10>', 'part 10 file(s)')
.option('--dicomdir <path>', 'Base directory path where binary .mht files will be written in DICOMweb structure','~/dicomweb')
.option(
'--dicomdir <path>',
'Base directory path where binary .mht files will be written in DICOMweb structure',
'~/dicomweb'
)
.action(async (fileName, options) => {
updateVerboseLog();
const instanceOptions = {};
Expand All @@ -51,8 +68,15 @@ program
.command('series')
.description('Generate series metadata files')
.argument('<studyUID>', 'Study Instance UID')
.option('--dicomdir <path>', 'Base directory path where DICOMweb structure is located', '~/dicomweb')
.option('--series-uid <seriesUID>', 'Specific Series Instance UID to process (if not provided, processes all series in the study)')
.option(
'--dicomdir <path>',
'Base directory path where DICOMweb structure is located',
'~/dicomweb'
)
.option(
'--series-uid <seriesUID>',
'Specific Series Instance UID to process (if not provided, processes all series in the study)'
)
.action(async (studyUID, options) => {
updateVerboseLog();
const seriesOptions = {};
Expand All @@ -69,7 +93,11 @@ program
.command('study')
.description('Generate study metadata files')
.argument('<studyUID>', 'Study Instance UID')
.option('--dicomdir <path>', 'Base directory path where DICOMweb structure is located', '~/dicomweb')
.option(
'--dicomdir <path>',
'Base directory path where DICOMweb structure is located',
'~/dicomweb'
)
.action(async (studyUID, options) => {
updateVerboseLog();
const studyOptions = {};
Expand All @@ -81,12 +109,24 @@ program

program
.command('create')
.description('Process instances and generate series and study metadata for all discovered studies')
.description(
'Process instances and generate series and study metadata for all discovered studies'
)
.argument('<part10...>', 'part 10 file(s) or directory(ies)')
.option('--dicomdir <path>', 'Base directory path where DICOMweb structure is located', '~/dicomweb')
.option(
'--dicomdir <path>',
'Base directory path where DICOMweb structure is located',
'~/dicomweb'
)
.option('--no-study-index', 'Skip creating/updating studies/index.json.gz file')
.option('--bulkdata-size <size>', 'Size threshold in bytes for public bulkdata tags (default: 131074, i.e. 128k + 2)')
.option('--private-bulkdata-size <size>', 'Size threshold in bytes for private bulkdata tags (default: 128)')
.option(
'--bulkdata-size <size>',
'Size threshold in bytes for public bulkdata tags (default: 131074, i.e. 128k + 2)'
)
.option(
'--private-bulkdata-size <size>',
'Size threshold in bytes for private bulkdata tags (default: 128)'
)
.action(async (fileNames, options) => {
updateVerboseLog();
const createOptions = {};
Expand Down Expand Up @@ -235,28 +275,71 @@ function parseFrameNumbers(frameNumbersStr) {
program
.command('thumbnail')
.description('Generate thumbnail(s) for DICOM instance(s)')
.argument('<studyUID>', 'Study Instance UID (required)')
.option('--dicomdir <path>', 'Base directory path where DICOMweb structure is located', '~/dicomweb')
.option('--series-uid <seriesUID>', 'Specific Series Instance UID to process (if not provided, uses first series from study query)')
.option('--sop-uid <sopUID>', 'Specific SOP Instance UID to process (if not provided, uses first instance from series)')
.option('--frame-numbers <frames>', 'Frame numbers to generate thumbnails for (comma-separated, supports ranges, e.g., "1-3,17")', '1')
.option('--series-thumbnail', 'Generate thumbnails for series (middle SOP instance, middle frame for multiframe)')
.action(async (studyUID, options) => {
updateVerboseLog();
.argument(
'[studySelector]',
'Study selector: StudyInstanceUID, query pattern (e.g. PatientID=25), or true for all studies'
)
.option(
'--dicomdir <path>',
'Base directory path where DICOMweb structure is located',
'~/dicomweb'
)
.option(
'--output-dicomdir <path>',
'Output directory for generated thumbnails (required when --dicomdir is http/https)'
)
.option(
'--series-uid <seriesUID>',
'Specific Series Instance UID to process (if not provided, uses first series from study query)'
)
.option(
'--sop-uid <sopUID>',
'Specific SOP Instance UID to process (if not provided, uses first instance from series)'
)
.option(
'--frame-numbers <frames>',
'Frame numbers to generate thumbnails for (comma-separated, supports ranges, e.g., "1-3,17")',
'1'
)
.option(
'--study-thumbnail',
'Representative mode: include study-level thumbnail and only instance JPEGs for the study-representative SOP (middle series, middle instance). Combine with --series-thumbnail so every series also gets a series-level thumbnail.'
)
.option(
'--series-thumbnail',
'Representative mode: include series-level thumbnail(s) from each series’ middle SOP and only instance JPEGs for those representative SOPs (plus study-representative SOP when --study-thumbnail is set). Omit both flags for full study thumbnail generation.'
)
.option('--force', 'Regenerate thumbnails even if they already exist at the output location')
.action(async (studySelectorArg, options, command) => {
const merged =
typeof command?.optsWithGlobals === 'function'
? command.optsWithGlobals()
: { ...program.opts(), ...options };
createVerboseLog(!!merged.verbose, { quiet: !!merged.quiet });
const thumbnailOptions = {};
if (options.dicomdir) {
thumbnailOptions.dicomdir = handleHomeRelative(options.dicomdir);
}
if (options.outputDicomdir) {
thumbnailOptions.outputDicomdir = handleHomeRelative(options.outputDicomdir);
}
thumbnailOptions.studySelector = studySelectorArg || options.studySelector || 'true';
if (options.seriesUid) {
thumbnailOptions.seriesUid = options.seriesUid;
}
if (options.sopUid) {
thumbnailOptions.instanceUid = options.sopUid;
}
if (options.studyThumbnail) {
thumbnailOptions.studyThumbnail = true;
}
if (options.seriesThumbnail) {
thumbnailOptions.seriesThumbnail = true;
}

if (options.force) {
thumbnailOptions.force = true;
}

// Parse frame numbers
try {
const frameNumbers = parseFrameNumbers(options.frameNumbers);
Expand All @@ -265,15 +348,22 @@ program
console.error(`Error parsing frame numbers: ${error.message}`);
process.exit(1);
}
await thumbnailMain(studyUID, thumbnailOptions);

await thumbnailMain(thumbnailOptions.studySelector, thumbnailOptions);
});

program
.command('index')
.description('Create or update studies/index.json.gz file by adding/updating study information')
.argument('[studyUIDs...]', 'Optional Study Instance UID(s) to process (if not provided, scans all studies)')
.option('--dicomdir <path>', 'Base directory path where DICOMweb structure is located', '~/dicomweb')
.argument(
'[studyUIDs...]',
'Optional Study Instance UID(s) to process (if not provided, scans all studies)'
)
.option(
'--dicomdir <path>',
'Base directory path where DICOMweb structure is located',
'~/dicomweb'
)
.action(async (studyUIDs, options) => {
updateVerboseLog();
const indexOptions = {};
Expand All @@ -287,8 +377,15 @@ program
.command('part10')
.description('Export DICOMweb metadata to Part 10 DICOM files')
.argument('<studyUID>', 'Study Instance UID')
.option('--dicomdir <path>', 'Base directory path where DICOMweb structure is located', '~/dicomweb')
.option('--series-uid <seriesUID>', 'Specific Series Instance UID to export (if not provided, exports all series)')
.option(
'--dicomdir <path>',
'Base directory path where DICOMweb structure is located',
'~/dicomweb'
)
.option(
'--series-uid <seriesUID>',
'Specific Series Instance UID to export (if not provided, exports all series)'
)
.option('--sop-uid <sopUIDs>', 'Comma-separated list of SOP Instance UIDs to export')
.option('-o, --output <dir>', 'Output directory for Part 10 files', '.')
.option('--format <format>', 'Output format: dicom, multipart, zip', 'dicom')
Expand All @@ -312,4 +409,4 @@ program
await part10Main(studyUID, part10Options);
});

program.parse();
program.parse();
Loading
Loading