@radicalimaging/static-wado-creator 1.7.3 → 1.7.6

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.
Files changed (48) hide show
  1. package/dist/DeleteStudy.js +16 -0
  2. package/dist/RejectInstance.js +25 -0
  3. package/dist/StaticWado.js +419 -0
  4. package/dist/createMain.js +18 -0
  5. package/dist/createPart10.js +113 -0
  6. package/dist/createThumbnail.js +163 -0
  7. package/dist/deleteMain.js +12 -0
  8. package/dist/groupMain.js +17 -0
  9. package/dist/index.mjs +18 -0
  10. package/dist/indexMain.js +10 -0
  11. package/dist/instanceMain.js +16 -0
  12. package/dist/metadataMain.js +15 -0
  13. package/dist/mkdicomwebConfig.js +342 -0
  14. package/dist/operation/InstanceDeduplicate.js +103 -0
  15. package/dist/operation/ScanStudy.js +47 -0
  16. package/dist/operation/StudyData.js +520 -0
  17. package/dist/operation/ThumbnailService.js +198 -0
  18. package/dist/operation/adapter/decodeImage.js +12 -0
  19. package/dist/operation/adapter/getImageInfo.js +38 -0
  20. package/dist/operation/adapter/transcodeImage.js +519 -0
  21. package/dist/operation/extractImageFrames.js +55 -0
  22. package/dist/operation/getDataSet.js +65 -0
  23. package/dist/operation/getEncapsulatedImageFrame.js +35 -0
  24. package/dist/operation/getNumberOfFrames.js +13 -0
  25. package/dist/operation/getUncompressedImageFrame.js +7 -0
  26. package/dist/operation/getVR.js +24 -0
  27. package/dist/operation/getValue.js +216 -0
  28. package/dist/operation/validateMetadata.js +40 -0
  29. package/dist/program/index.js +14 -0
  30. package/dist/rejectMain.js +12 -0
  31. package/dist/serverMain.js +21 -0
  32. package/dist/util/IdCreator.js +42 -0
  33. package/dist/util/adaptProgramOpts.js +39 -0
  34. package/dist/util/compressionOptionParser.js +13 -0
  35. package/dist/util/resultsReport.ts +87 -0
  36. package/dist/writer/CompleteStudyWriter.js +129 -0
  37. package/dist/writer/DeduplicateWriter.js +33 -0
  38. package/dist/writer/ExpandUriPath.js +16 -0
  39. package/dist/writer/HashDataWriter.js +74 -0
  40. package/dist/writer/ImageFrameWriter.js +59 -0
  41. package/dist/writer/MultipartAttribute.js +8 -0
  42. package/dist/writer/MultipartHeader.js +14 -0
  43. package/dist/writer/RawDicomWriter.js +44 -0
  44. package/dist/writer/ThumbnailWriter.js +14 -0
  45. package/dist/writer/VideoWriter.js +53 -0
  46. package/dist/writer/WriteMultipart.js +22 -0
  47. package/dist/writer/WriteStream.js +64 -0
  48. package/package.json +11 -6
@@ -0,0 +1,74 @@
1
+ const hashFactory = require('node-object-hash');
2
+ const { createHash } = require('crypto');
3
+
4
+ const path = require('path');
5
+ const { Tags } = require('@radicalimaging/static-wado-util');
6
+ const WriteStream = require('./WriteStream');
7
+ const WriteMultipart = require('./WriteMultipart');
8
+ const ExpandUriPath = require('./ExpandUriPath');
9
+ const { MultipartHeader } = require('./MultipartHeader');
10
+
11
+ // Extensions for encapsulated content. Do NOT add any executable content extensions here.
12
+ const extensions = {
13
+ 'application/pdf': '.pdf',
14
+ 'text/json': '.json',
15
+ 'application/xml+cda': '.cda.xml',
16
+ };
17
+
18
+ const baseHasher = hashFactory.hasher();
19
+
20
+ const hasher = {
21
+ hash: v => {
22
+ const type = v.constructor?.name;
23
+ if (type === 'Buffer') {
24
+ return createHash('sha256').update(v).digest('hex');
25
+ }
26
+ return baseHasher.hash(v);
27
+ },
28
+ };
29
+
30
+ /** Writes out JSON files to the given file name. Automatically GZips them, and adds the extension */
31
+ const HashDataWriter =
32
+ options =>
33
+ async (id, key, data, additionalOptions = {}) => {
34
+ const isRaw = ArrayBuffer.isView(data);
35
+ const { mimeType } = additionalOptions;
36
+ // If the file has an extension, it should be directly accessible as that file type.
37
+ const gzip = !isRaw || (data.length > 1024 && !mimeType);
38
+ const { dirName, fileName } = HashDataWriter.createHashPath(data, options);
39
+ const absolutePath = path.join(id.studyPath, dirName);
40
+ const rawData = isRaw ? data : JSON.stringify(data, null, 1);
41
+ const writeStream = WriteStream(absolutePath, fileName, {
42
+ mkdir: true,
43
+ gzip,
44
+ });
45
+ if (isRaw) {
46
+ await WriteMultipart(
47
+ writeStream,
48
+ [new MultipartHeader('Content-Type', 'application/octet-stream')],
49
+ rawData
50
+ );
51
+ } else {
52
+ await writeStream.write(rawData);
53
+ }
54
+ await writeStream.close();
55
+ return ExpandUriPath(id, `${dirName}/${fileName}`, options);
56
+ };
57
+
58
+ /**
59
+ * Returns a hash path relative to the objectUID directory.
60
+ */
61
+ HashDataWriter.createHashPath = (data, options = {}) => {
62
+ const { mimeType } = options;
63
+ const isRaw = ArrayBuffer.isView(data);
64
+ const extension = isRaw ? (mimeType && extensions[mimeType]) || '' : '.json';
65
+ const hashValue = Tags.getValue(data, Tags.DeduppedHash) || hasher.hash(data);
66
+
67
+ return {
68
+ // Use string concat as this value is used for the BulkDataURI which needs forward slashes
69
+ dirName: `bulkdata/${hashValue.substring(0, 3)}/${hashValue.substring(3, 5)}`,
70
+ fileName: hashValue.substring(5) + extension,
71
+ };
72
+ };
73
+
74
+ module.exports = HashDataWriter;
@@ -0,0 +1,59 @@
1
+ const { uids } = require('@radicalimaging/static-wado-util');
2
+ const WriteStream = require('./WriteStream');
3
+ const WriteMultipart = require('./WriteMultipart');
4
+ const ExpandUriPath = require('./ExpandUriPath');
5
+ const { MultipartHeader, MultipartAttribute } = require('./MultipartHeader');
6
+
7
+ const ImageFrameWriter = options => {
8
+ const { encapsulatedImage, singlePartImage } = options;
9
+ return async (id, index, imageFrame) => {
10
+ const { transferSyntaxUid } = id;
11
+ const type = uids[transferSyntaxUid] || uids.default;
12
+ const extension = type.extension;
13
+ let content;
14
+ if (imageFrame instanceof Uint8Array) {
15
+ content = imageFrame;
16
+ } else {
17
+ content = Buffer.from(imageFrame.buffer);
18
+ }
19
+
20
+ if (encapsulatedImage || !extension) {
21
+ const writeStream = WriteStream(id.imageFrameRootPath, `${1 + index}.mht`, {
22
+ gzip: type.gzip,
23
+ mkdir: true,
24
+ });
25
+ await WriteMultipart(
26
+ writeStream,
27
+ [
28
+ new MultipartHeader('Content-Type', type.contentType, [
29
+ new MultipartAttribute('transfer-syntax', transferSyntaxUid),
30
+ ]),
31
+ ],
32
+ content
33
+ );
34
+ await writeStream.close();
35
+ console.verbose(
36
+ 'Wrote encapsulated image frame',
37
+ id.sopInstanceUid,
38
+ index + 1,
39
+ type.contentType
40
+ );
41
+ }
42
+ if (extension && singlePartImage) {
43
+ const writeStreamSingle = WriteStream(id.imageFrameRootPath, `${1 + index}${extension}`, {
44
+ gzip: type.gzip,
45
+ mkdir: true,
46
+ });
47
+ await writeStreamSingle.write(content);
48
+ await writeStreamSingle.close();
49
+ console.verbose('Wrote single part image frame', id.sopInstanceUid, index + 1, extension);
50
+ }
51
+ const includeSeries = true;
52
+ return ExpandUriPath(id, `instances/${id.sopInstanceUid}/frames`, {
53
+ includeSeries,
54
+ ...options,
55
+ });
56
+ };
57
+ };
58
+
59
+ module.exports = ImageFrameWriter;
@@ -0,0 +1,8 @@
1
+ class MultipartAttribute {
2
+ constructor(attributeName, attributeValue) {
3
+ this.attributeName = attributeName;
4
+ this.attributeValue = attributeValue;
5
+ }
6
+ }
7
+
8
+ module.exports = MultipartAttribute;
@@ -0,0 +1,14 @@
1
+ const MultipartAttribute = require('./MultipartAttribute');
2
+
3
+ class MultipartHeader {
4
+ constructor(headerName, headerValue, attributes = []) {
5
+ this.headerName = headerName;
6
+ this.headerValue = headerValue;
7
+ this.attributes = attributes;
8
+ }
9
+ }
10
+
11
+ module.exports = {
12
+ MultipartAttribute,
13
+ MultipartHeader,
14
+ };
@@ -0,0 +1,44 @@
1
+ const { Tags } = require('@radicalimaging/static-wado-util');
2
+ const WriteStream = require('./WriteStream');
3
+ const WriteMultipart = require('./WriteMultipart');
4
+ const { MultipartHeader } = require('./MultipartHeader');
5
+
6
+ const modalitiesToRawWrite = ['SEG', 'SR'];
7
+ const sopClassesToRawWrite = ['1.2.840.10008.5.1.4.1.1.30'];
8
+
9
+ const defaultSelector = instance => {
10
+ const modality = instance.metadata[Tags.Modality]?.Value?.[0];
11
+ const sopClass = instance.metadata[Tags.SOPClassUID]?.Value?.[0];
12
+ return (
13
+ modalitiesToRawWrite.indexOf(modality) !== -1 || sopClassesToRawWrite.indexOf(sopClass) !== -1
14
+ );
15
+ };
16
+
17
+ /** Writes out raw DICOM files, encapsulated in multipart and gzipped - currently just DICOM SR and SEG */
18
+ const RawDicomWriter =
19
+ () =>
20
+ async (id, instance, content, options = { gzip: true }) => {
21
+ if (!instance) {
22
+ return undefined;
23
+ }
24
+ const selector = options.selector || defaultSelector;
25
+ if (!selector(instance)) {
26
+ return undefined;
27
+ }
28
+ const contentType = options.contentType || 'application/dicom';
29
+ const writeStream = WriteStream(id.sopInstanceRootPath, 'index.mht', {
30
+ gzip: options.gzip,
31
+ mkdir: true,
32
+ });
33
+ const { length } = content;
34
+ const buffer = content.slice(0, length);
35
+ await WriteMultipart(
36
+ writeStream,
37
+ [new MultipartHeader('Content-Type', contentType, [])],
38
+ buffer
39
+ );
40
+ await writeStream.close();
41
+ };
42
+ RawDicomWriter.defaultSelector = defaultSelector;
43
+
44
+ module.exports = RawDicomWriter;
@@ -0,0 +1,14 @@
1
+ const WriteStream = require('./WriteStream');
2
+
3
+ const ThumbnailWriter = () => async (filePath, fileName, thumbBuffer) => {
4
+ const writeStream = WriteStream(filePath, fileName, {
5
+ mkdir: true,
6
+ });
7
+
8
+ await writeStream.write(thumbBuffer);
9
+
10
+ console.verbose('Wrote thumbnail frame', filePath, fileName);
11
+ return writeStream.close();
12
+ };
13
+
14
+ module.exports = ThumbnailWriter;
@@ -0,0 +1,53 @@
1
+ /** Write raw video file */
2
+ const { Tags } = require('@radicalimaging/static-wado-util');
3
+ const WriteStream = require('./WriteStream');
4
+
5
+ const MPEG2 = 'mpg';
6
+ const H264 = 'mp4';
7
+ const H265 = 'h265';
8
+
9
+ const VIDEO_TYPES = {
10
+ '1.2.840.10008.1.2.4.100': MPEG2,
11
+ '1.2.840.10008.1.2.4.101': MPEG2,
12
+ '1.2.840.10008.1.2.4.102': H264,
13
+ '1.2.840.10008.1.2.4.103': H264,
14
+ '1.2.840.10008.1.2.4.104': H264,
15
+ '1.2.840.10008.1.2.4.105': H264,
16
+ '1.2.840.10008.1.2.4.106': H264,
17
+ '1.2.840.10008.1.2.4.107': H265,
18
+ '1.2.840.10008.1.2.4.108': H265,
19
+ // TODO - add the new multi-segment MPEG2 and H264 variants
20
+ };
21
+
22
+ const isVideo = value =>
23
+ VIDEO_TYPES[value && value.string ? value.string(Tags.RawTransferSyntaxUID) : value];
24
+
25
+ const VideoWriter = () =>
26
+ async function run(id, dataSet) {
27
+ console.log(`Writing video ${id.sopInstanceUid}`);
28
+ const extension = VIDEO_TYPES[dataSet.string(Tags.RawTransferSyntaxUID)];
29
+ const filename = `index.${extension}`;
30
+ const writeStream = WriteStream(`${id.sopInstanceRootPath}/rendered`, filename, {
31
+ mkdir: true,
32
+ });
33
+ let length = 0;
34
+ const { fragments } = dataSet.elements.x7fe00010;
35
+
36
+ if (!fragments) {
37
+ console.warn('No video data');
38
+ return '';
39
+ }
40
+ // The zero position fragment isn't available, even though present in the original data
41
+ for (let i = 0; i < fragments.length; i++) {
42
+ const fragment = fragments[i];
43
+ const blob = dataSet.byteArray.slice(fragment.position, fragment.position + fragment.length);
44
+ length += blob.length;
45
+ await writeStream.write(blob);
46
+ }
47
+ await writeStream.close();
48
+ console.log(`Done video ${id.sopInstanceRootPath}\\${filename} of length ${length}`);
49
+ return `series/${id.seriesInstanceUid}/instances/${id.sopInstanceUid}/rendered?length=${length}&offset=0&accept=video/mp4`;
50
+ };
51
+
52
+ module.exports = VideoWriter;
53
+ VideoWriter.isVideo = isVideo;
@@ -0,0 +1,22 @@
1
+ const { v4: uuid } = require('uuid');
2
+
3
+ const WriteMultipart = async (writeStream, headers, content) => {
4
+ const boundaryId = uuid();
5
+ await writeStream.write(`--BOUNDARY_${boundaryId}\r\n`);
6
+ for (let i = 0; i < headers.length; i++) {
7
+ const header = headers[i];
8
+ await writeStream.write(`${header.headerName}: ${header.headerValue}`);
9
+ if (header.attributes) {
10
+ for (let j = 0; j < header.attributes.length; j++) {
11
+ const attribute = header.attributes[j];
12
+ await writeStream.write(`;${attribute.attributeName}=${attribute.attributeValue}`);
13
+ }
14
+ }
15
+ await writeStream.write('\r\n');
16
+ }
17
+ await writeStream.write('\r\n');
18
+ await writeStream.write(content);
19
+ await writeStream.write(`\r\n--BOUNDARY_${boundaryId}--`);
20
+ };
21
+
22
+ module.exports = WriteMultipart;
@@ -0,0 +1,64 @@
1
+ const fs = require('fs');
2
+ const zlib = require('zlib');
3
+ const path = require('path');
4
+
5
+ let writeCount = 0;
6
+
7
+ /** Create an optionally gzipped stream,
8
+ * where the write operations are performed in order executed,
9
+ * and don't require synchronization, only the 'close' operation
10
+ * requires syncing.
11
+ */
12
+ const WriteStream = (dir, nameSrc, options = {}) => {
13
+ const isGzip = nameSrc.indexOf('.gz') != -1 || options.gzip;
14
+ const name = (isGzip && nameSrc.indexOf('.gz') === -1 && `${nameSrc}.gz`) || nameSrc;
15
+ if (options.mkdir && !fs.existsSync(dir)) {
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ const tempName = path.join(dir, `tempFile-${Math.round(Math.random() * 1000000000)}`);
20
+ const finalName = path.join(dir, name);
21
+ writeCount++;
22
+ if (writeCount > 10) {
23
+ console.log('Write count', tempName, finalName, writeCount);
24
+ if (writeCount > 100) {
25
+ console.error('Too many open writes', new Error());
26
+ throw new Error(`Write count too high ${writeCount} destination ${finalName}`);
27
+ }
28
+ }
29
+ const rawStream = fs.createWriteStream(tempName);
30
+ const closePromise = new Promise(resolve => {
31
+ rawStream.on('close', () => {
32
+ resolve('closed');
33
+ });
34
+ });
35
+
36
+ let writeStream = rawStream;
37
+ if (isGzip) {
38
+ writeStream = zlib.createGzip();
39
+ writeStream.pipe(rawStream);
40
+ writeStream.on('close', () => {
41
+ rawStream.close();
42
+ });
43
+ }
44
+
45
+ async function close() {
46
+ await this.writeStream.end();
47
+ await this.closePromise;
48
+ await fs.rename(tempName, finalName, () => true);
49
+ writeCount--;
50
+ }
51
+
52
+ return {
53
+ writeStream,
54
+ closePromise,
55
+
56
+ write(data) {
57
+ return this.writeStream.write(data);
58
+ },
59
+
60
+ close,
61
+ };
62
+ };
63
+
64
+ module.exports = WriteStream;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radicalimaging/static-wado-creator",
3
- "version": "1.7.3",
3
+ "version": "1.7.6",
4
4
  "description": "Library to convert DICOMP10 to DICOMweb format. Original upstream: https://github.com/chafey/dicomp10-to-dicomweb-js",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,16 +31,21 @@
31
31
  "homepage": "https://github.com/radicalimaging/static-dicomweb#readme",
32
32
  "license": "ISC",
33
33
  "main": "lib/index.mjs",
34
- "module": "dist/index.js",
34
+ "module": "lib/index.mjs",
35
+ "exports": {
36
+ ".": {
37
+ "default": "./lib/index.mjs",
38
+ "esm": "./lib/index.mjs"
39
+ }
40
+ },
35
41
  "directories": {
36
42
  "lib": "lib",
37
- "dist": "dist",
38
43
  "bin": "bin"
39
44
  },
40
45
  "files": [
41
46
  "bin",
42
- "dist",
43
- "lib"
47
+ "lib",
48
+ "dist"
44
49
  ],
45
50
  "bin": {
46
51
  "mkdicomweb": "bin/mkdicomweb.mjs"
@@ -48,7 +53,7 @@
48
53
  "scripts": {
49
54
  "test": "jest --config ./jest.config.js",
50
55
  "build": "bun run build:esm",
51
- "build:esm": "bun build lib/index.mjs --target node --outdir dist",
56
+ "build:esm": "bun -e \"import { mkdirSync, cpSync } from 'node:fs'; mkdirSync('dist', { recursive: true }); cpSync('lib', 'dist', { recursive: true });\"",
52
57
  "clean": "rimraf dist",
53
58
  "link:exec": "bun link",
54
59
  "lint": "npx eslint --color \"**/*.{js,mjs,cjs}\"",