dicom-synth 0.1.0 → 1.0.0
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.
- package/dist/esm/index.js +79 -9
- package/dist/esm/negativeCases/index.js +17 -0
- package/dist/esm/nonStandardDicom/dicomdir.js +30 -0
- package/dist/esm/syntheticFixtures/generator.js +26 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/negativeCases/index.d.ts +11 -0
- package/dist/types/nonStandardDicom/dicomdir.d.ts +15 -0
- package/dist/types/syntheticFixtures/generator.d.ts +16 -0
- package/package.json +1 -1
- package/dist/esm/publicCorpus.js +0 -72
- package/dist/esm/synthetic.js +0 -109
- package/dist/types/publicCorpus.d.ts +0 -25
- package/dist/types/synthetic.d.ts +0 -11
package/dist/esm/index.js
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
// src/negativeCases/index.ts
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
function writeFakeDicomSignatureFile(filePath) {
|
|
5
|
+
const buf = Buffer.alloc(200, 0);
|
|
6
|
+
buf.write("XXXX", 128, 4, "ascii");
|
|
7
|
+
mkdirSync(resolve(filePath, ".."), { recursive: true });
|
|
8
|
+
writeFileSync(filePath, buf);
|
|
9
|
+
}
|
|
10
|
+
function writeNonDicomFile(filePath, content = "not dicom") {
|
|
11
|
+
mkdirSync(resolve(filePath, ".."), { recursive: true });
|
|
12
|
+
writeFileSync(filePath, content);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/nonStandardDicom/dicomdir.ts
|
|
16
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
17
|
+
import { resolve as resolve2 } from "node:path";
|
|
18
|
+
var DICOMDIR_SOP_CLASS_UID = "1.2.840.10008.1.3.10";
|
|
19
|
+
function buildDicomdirBuffer() {
|
|
20
|
+
const uid = DICOMDIR_SOP_CLASS_UID;
|
|
21
|
+
const buf = Buffer.alloc(256, 0);
|
|
22
|
+
buf.write("DICM", 128, 4, "ascii");
|
|
23
|
+
let off = 132;
|
|
24
|
+
buf.writeUInt16LE(2, off);
|
|
25
|
+
off += 2;
|
|
26
|
+
buf.writeUInt16LE(2, off);
|
|
27
|
+
off += 2;
|
|
28
|
+
buf.write("UI", off, 2, "ascii");
|
|
29
|
+
off += 2;
|
|
30
|
+
buf.writeUInt16LE(uid.length, off);
|
|
31
|
+
off += 2;
|
|
32
|
+
buf.write(uid, off, uid.length, "ascii");
|
|
33
|
+
off += uid.length;
|
|
34
|
+
return buf.subarray(0, off);
|
|
35
|
+
}
|
|
36
|
+
function writeDicomdirFile(filePath) {
|
|
37
|
+
mkdirSync2(resolve2(filePath, ".."), { recursive: true });
|
|
38
|
+
writeFileSync2(filePath, buildDicomdirBuffer());
|
|
39
|
+
}
|
|
40
|
+
|
|
1
41
|
// src/public-fixtures/catalog.ts
|
|
2
42
|
import { readFileSync } from "node:fs";
|
|
3
43
|
import { dirname, join } from "node:path";
|
|
@@ -31,7 +71,7 @@ function loadCaseById(casesJsonPath, id) {
|
|
|
31
71
|
|
|
32
72
|
// src/public-fixtures/fetch.ts
|
|
33
73
|
import { createHash } from "node:crypto";
|
|
34
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
74
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
35
75
|
import { homedir } from "node:os";
|
|
36
76
|
import { join as join2 } from "node:path";
|
|
37
77
|
var DEFAULT_CACHE_ROOT = join2(homedir(), ".cache", "dicom-synth-testcases");
|
|
@@ -58,7 +98,7 @@ async function fetchPublicCaseToCache(record, cacheRoot = DEFAULT_CACHE_ROOT) {
|
|
|
58
98
|
verifySha256(buf2, record.sha256);
|
|
59
99
|
return dest;
|
|
60
100
|
}
|
|
61
|
-
|
|
101
|
+
mkdirSync3(join2(cacheRoot, record.sha256), { recursive: true });
|
|
62
102
|
const res = await fetch(record.source.url);
|
|
63
103
|
if (!res.ok) {
|
|
64
104
|
throw new Error(
|
|
@@ -67,13 +107,13 @@ async function fetchPublicCaseToCache(record, cacheRoot = DEFAULT_CACHE_ROOT) {
|
|
|
67
107
|
}
|
|
68
108
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
69
109
|
verifySha256(buf, record.sha256);
|
|
70
|
-
|
|
110
|
+
writeFileSync3(dest, buf);
|
|
71
111
|
return dest;
|
|
72
112
|
}
|
|
73
113
|
|
|
74
114
|
// src/syntheticFixtures/generator.ts
|
|
75
|
-
import { mkdirSync as
|
|
76
|
-
import { resolve } from "node:path";
|
|
115
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
116
|
+
import { resolve as resolve3 } from "node:path";
|
|
77
117
|
|
|
78
118
|
// src/loadDcmjs.ts
|
|
79
119
|
import dcmjsDefaultImport, * as dcmjsNamespace from "dcmjs";
|
|
@@ -183,13 +223,38 @@ var SYNTHETIC_FIXTURES = [
|
|
|
183
223
|
description: "Valid UIDs plus empty Laterality and zero PatientWeight"
|
|
184
224
|
}
|
|
185
225
|
];
|
|
226
|
+
function writeMinimalDicomFile(filePath, tags) {
|
|
227
|
+
const patientId = tags?.patientId ?? "TEST-PATIENT-001";
|
|
228
|
+
const patientName = tags?.patientName ?? "TEST^PATIENT";
|
|
229
|
+
const sopUID = tags?.sopInstanceUid ?? "1.2.3.4.5.6.7.8.9.0.1";
|
|
230
|
+
const dataset = {
|
|
231
|
+
PatientName: patientName,
|
|
232
|
+
PatientID: patientId,
|
|
233
|
+
Modality: "CT",
|
|
234
|
+
SOPClassUID: CT_SOP_CLASS,
|
|
235
|
+
SOPInstanceUID: sopUID,
|
|
236
|
+
SeriesInstanceUID: "1.2.3.4.5.6.7.8",
|
|
237
|
+
StudyInstanceUID: "1.2.3.4.5.6.7",
|
|
238
|
+
SeriesNumber: "1",
|
|
239
|
+
Rows: 1,
|
|
240
|
+
Columns: 1,
|
|
241
|
+
BitsAllocated: 16,
|
|
242
|
+
PixelRepresentation: 0,
|
|
243
|
+
PixelData: new Uint8Array([0, 0]).buffer
|
|
244
|
+
};
|
|
245
|
+
const dicomDict = new dcmjs.data.DicomDict({});
|
|
246
|
+
dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset);
|
|
247
|
+
const buffer = Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
|
|
248
|
+
mkdirSync4(resolve3(filePath, ".."), { recursive: true });
|
|
249
|
+
writeFileSync4(filePath, buffer);
|
|
250
|
+
}
|
|
186
251
|
function writeSyntheticFixturesToDir(outDir) {
|
|
187
|
-
const root =
|
|
188
|
-
|
|
252
|
+
const root = resolve3(outDir);
|
|
253
|
+
mkdirSync4(root, { recursive: true });
|
|
189
254
|
const written = [];
|
|
190
255
|
for (const { filename, variant } of SYNTHETIC_FIXTURES) {
|
|
191
|
-
const path =
|
|
192
|
-
|
|
256
|
+
const path = resolve3(root, filename);
|
|
257
|
+
writeFileSync4(path, buildSyntheticCtBuffer(variant));
|
|
193
258
|
written.push(path);
|
|
194
259
|
}
|
|
195
260
|
return written;
|
|
@@ -197,6 +262,7 @@ function writeSyntheticFixturesToDir(outDir) {
|
|
|
197
262
|
export {
|
|
198
263
|
SYNTHETIC_FIXTURES,
|
|
199
264
|
buildDicomDict,
|
|
265
|
+
buildDicomdirBuffer,
|
|
200
266
|
buildSyntheticCtBuffer,
|
|
201
267
|
caseCachePath,
|
|
202
268
|
defaultPublicCasesPath,
|
|
@@ -205,5 +271,9 @@ export {
|
|
|
205
271
|
loadCasesFromJson,
|
|
206
272
|
loadDefaultCases,
|
|
207
273
|
verifySha256,
|
|
274
|
+
writeDicomdirFile,
|
|
275
|
+
writeFakeDicomSignatureFile,
|
|
276
|
+
writeMinimalDicomFile,
|
|
277
|
+
writeNonDicomFile,
|
|
208
278
|
writeSyntheticFixturesToDir
|
|
209
279
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/negativeCases/index.ts
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
function writeFakeDicomSignatureFile(filePath) {
|
|
5
|
+
const buf = Buffer.alloc(200, 0);
|
|
6
|
+
buf.write("XXXX", 128, 4, "ascii");
|
|
7
|
+
mkdirSync(resolve(filePath, ".."), { recursive: true });
|
|
8
|
+
writeFileSync(filePath, buf);
|
|
9
|
+
}
|
|
10
|
+
function writeNonDicomFile(filePath, content = "not dicom") {
|
|
11
|
+
mkdirSync(resolve(filePath, ".."), { recursive: true });
|
|
12
|
+
writeFileSync(filePath, content);
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
writeFakeDicomSignatureFile,
|
|
16
|
+
writeNonDicomFile
|
|
17
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/nonStandardDicom/dicomdir.ts
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
var DICOMDIR_SOP_CLASS_UID = "1.2.840.10008.1.3.10";
|
|
5
|
+
function buildDicomdirBuffer() {
|
|
6
|
+
const uid = DICOMDIR_SOP_CLASS_UID;
|
|
7
|
+
const buf = Buffer.alloc(256, 0);
|
|
8
|
+
buf.write("DICM", 128, 4, "ascii");
|
|
9
|
+
let off = 132;
|
|
10
|
+
buf.writeUInt16LE(2, off);
|
|
11
|
+
off += 2;
|
|
12
|
+
buf.writeUInt16LE(2, off);
|
|
13
|
+
off += 2;
|
|
14
|
+
buf.write("UI", off, 2, "ascii");
|
|
15
|
+
off += 2;
|
|
16
|
+
buf.writeUInt16LE(uid.length, off);
|
|
17
|
+
off += 2;
|
|
18
|
+
buf.write(uid, off, uid.length, "ascii");
|
|
19
|
+
off += uid.length;
|
|
20
|
+
return buf.subarray(0, off);
|
|
21
|
+
}
|
|
22
|
+
function writeDicomdirFile(filePath) {
|
|
23
|
+
mkdirSync(resolve(filePath, ".."), { recursive: true });
|
|
24
|
+
writeFileSync(filePath, buildDicomdirBuffer());
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
DICOMDIR_SOP_CLASS_UID,
|
|
28
|
+
buildDicomdirBuffer,
|
|
29
|
+
writeDicomdirFile
|
|
30
|
+
};
|
|
@@ -110,6 +110,31 @@ var SYNTHETIC_FIXTURES = [
|
|
|
110
110
|
description: "Valid UIDs plus empty Laterality and zero PatientWeight"
|
|
111
111
|
}
|
|
112
112
|
];
|
|
113
|
+
function writeMinimalDicomFile(filePath, tags) {
|
|
114
|
+
const patientId = tags?.patientId ?? "TEST-PATIENT-001";
|
|
115
|
+
const patientName = tags?.patientName ?? "TEST^PATIENT";
|
|
116
|
+
const sopUID = tags?.sopInstanceUid ?? "1.2.3.4.5.6.7.8.9.0.1";
|
|
117
|
+
const dataset = {
|
|
118
|
+
PatientName: patientName,
|
|
119
|
+
PatientID: patientId,
|
|
120
|
+
Modality: "CT",
|
|
121
|
+
SOPClassUID: CT_SOP_CLASS,
|
|
122
|
+
SOPInstanceUID: sopUID,
|
|
123
|
+
SeriesInstanceUID: "1.2.3.4.5.6.7.8",
|
|
124
|
+
StudyInstanceUID: "1.2.3.4.5.6.7",
|
|
125
|
+
SeriesNumber: "1",
|
|
126
|
+
Rows: 1,
|
|
127
|
+
Columns: 1,
|
|
128
|
+
BitsAllocated: 16,
|
|
129
|
+
PixelRepresentation: 0,
|
|
130
|
+
PixelData: new Uint8Array([0, 0]).buffer
|
|
131
|
+
};
|
|
132
|
+
const dicomDict = new dcmjs.data.DicomDict({});
|
|
133
|
+
dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset);
|
|
134
|
+
const buffer = Buffer.from(dicomDict.write({ allowInvalidVRLength: true }));
|
|
135
|
+
mkdirSync(resolve(filePath, ".."), { recursive: true });
|
|
136
|
+
writeFileSync(filePath, buffer);
|
|
137
|
+
}
|
|
113
138
|
function writeSyntheticFixturesToDir(outDir) {
|
|
114
139
|
const root = resolve(outDir);
|
|
115
140
|
mkdirSync(root, { recursive: true });
|
|
@@ -125,5 +150,6 @@ export {
|
|
|
125
150
|
SYNTHETIC_FIXTURES,
|
|
126
151
|
buildDicomDict,
|
|
127
152
|
buildSyntheticCtBuffer,
|
|
153
|
+
writeMinimalDicomFile,
|
|
128
154
|
writeSyntheticFixturesToDir
|
|
129
155
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { writeFakeDicomSignatureFile, writeNonDicomFile, } from './negativeCases/index';
|
|
2
|
+
export { buildDicomdirBuffer, writeDicomdirFile, } from './nonStandardDicom/dicomdir';
|
|
1
3
|
export { defaultPublicCasesPath, loadCaseById, loadCasesFromJson, loadDefaultCases, type PublicCaseRecord, type PublicCaseSource, } from './public-fixtures/catalog';
|
|
2
4
|
export { caseCachePath, fetchPublicCaseToCache, verifySha256, } from './public-fixtures/fetch';
|
|
3
|
-
export { buildDicomDict, buildSyntheticCtBuffer, SYNTHETIC_FIXTURES, type SyntheticCtVariant, writeSyntheticFixturesToDir, } from './syntheticFixtures/generator';
|
|
5
|
+
export { buildDicomDict, buildSyntheticCtBuffer, type MinimalDicomTags, SYNTHETIC_FIXTURES, type SyntheticCtVariant, writeMinimalDicomFile, writeSyntheticFixturesToDir, } from './syntheticFixtures/generator';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a file large enough to pass size checks but with `XXXX` at
|
|
3
|
+
* offset 128 instead of the `DICM` magic bytes.
|
|
4
|
+
* The parent directory is created if it does not exist.
|
|
5
|
+
*/
|
|
6
|
+
export declare function writeFakeDicomSignatureFile(filePath: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Write arbitrary non-DICOM content to `filePath`.
|
|
9
|
+
* The parent directory is created if it does not exist.
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeNonDicomFile(filePath: string, content?: string): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const DICOMDIR_SOP_CLASS_UID = "1.2.840.10008.1.3.10";
|
|
2
|
+
/**
|
|
3
|
+
* Build a minimal DICOM Part 10 buffer whose MediaStorageSOPClassUID
|
|
4
|
+
* identifies it as a DICOMDIR (1.2.840.10008.1.3.10).
|
|
5
|
+
*
|
|
6
|
+
* The file has a valid DICM signature and a single meta tag (0002,0002)
|
|
7
|
+
* so it passes signature checks but is detectable by SOP class UID.
|
|
8
|
+
* dcmjs cannot write DICOMDIR, so this uses a raw byte layout.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildDicomdirBuffer(): Buffer;
|
|
11
|
+
/**
|
|
12
|
+
* Write a minimal DICOMDIR-signature file to `filePath`.
|
|
13
|
+
* The parent directory is created if it does not exist.
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeDicomdirFile(filePath: string): void;
|
|
@@ -7,5 +7,21 @@ export declare const SYNTHETIC_FIXTURES: {
|
|
|
7
7
|
variant: SyntheticCtVariant;
|
|
8
8
|
description: string;
|
|
9
9
|
}[];
|
|
10
|
+
export type MinimalDicomTags = {
|
|
11
|
+
patientId?: string;
|
|
12
|
+
patientName?: string;
|
|
13
|
+
sopInstanceUid?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Write a minimal CT DICOM file to `filePath`.
|
|
17
|
+
*
|
|
18
|
+
* The output is intentionally bare: it contains no Part 10 group-0002
|
|
19
|
+
* file-meta header (no MediaStorageSOPClassUID, TransferSyntaxUID, etc.).
|
|
20
|
+
* Use `buildDicomDict` / `buildSyntheticCtBuffer` when a conformant
|
|
21
|
+
* Part 10 file with a complete meta header is required.
|
|
22
|
+
*
|
|
23
|
+
* The parent directory is created if it does not exist.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeMinimalDicomFile(filePath: string, tags?: MinimalDicomTags): void;
|
|
10
26
|
/** Write all catalogue fixtures to a directory (created if missing). */
|
|
11
27
|
export declare function writeSyntheticFixturesToDir(outDir: string): string[];
|
package/package.json
CHANGED
package/dist/esm/publicCorpus.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// src/publicCorpus.ts
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
var DEFAULT_CACHE_ROOT = join(homedir(), ".cache", "dicom-synth-testcases");
|
|
8
|
-
var bundledCatalogPath = join(
|
|
9
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
10
|
-
"../data/public-cases.json"
|
|
11
|
-
);
|
|
12
|
-
function defaultPublicCasesPath() {
|
|
13
|
-
return bundledCatalogPath;
|
|
14
|
-
}
|
|
15
|
-
function loadDefaultCases() {
|
|
16
|
-
return loadCasesFromJson(defaultPublicCasesPath());
|
|
17
|
-
}
|
|
18
|
-
function loadCasesFromJson(casesJsonPath) {
|
|
19
|
-
const j = JSON.parse(readFileSync(casesJsonPath, "utf8"));
|
|
20
|
-
return j.cases;
|
|
21
|
-
}
|
|
22
|
-
function loadCaseById(casesJsonPath, id) {
|
|
23
|
-
const found = loadCasesFromJson(casesJsonPath).find((r) => r.id === id);
|
|
24
|
-
if (!found) {
|
|
25
|
-
throw new Error(`Unknown case id "${id}" in ${casesJsonPath}`);
|
|
26
|
-
}
|
|
27
|
-
return found;
|
|
28
|
-
}
|
|
29
|
-
function caseCachePath(sha256, root = DEFAULT_CACHE_ROOT) {
|
|
30
|
-
return join(root, sha256, "file.dcm");
|
|
31
|
-
}
|
|
32
|
-
function verifySha256(buffer, expected) {
|
|
33
|
-
const h = createHash("sha256").update(buffer).digest("hex");
|
|
34
|
-
if (h !== expected) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`SHA256 mismatch: expected ${expected}, got ${h}. Upstream may have changed; bump metadata after review.`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async function fetchPublicCaseToCache(record, cacheRoot = DEFAULT_CACHE_ROOT) {
|
|
41
|
-
if (record.source.kind !== "url") {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`Case ${record.id}: only url sources are supported (s3 requires credentials).`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
const dest = caseCachePath(record.sha256, cacheRoot);
|
|
47
|
-
if (existsSync(dest)) {
|
|
48
|
-
const buf2 = readFileSync(dest);
|
|
49
|
-
verifySha256(buf2, record.sha256);
|
|
50
|
-
return dest;
|
|
51
|
-
}
|
|
52
|
-
mkdirSync(join(cacheRoot, record.sha256), { recursive: true });
|
|
53
|
-
const res = await fetch(record.source.url);
|
|
54
|
-
if (!res.ok) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`Fetch failed ${res.status} ${res.statusText} for ${record.source.url}`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
60
|
-
verifySha256(buf, record.sha256);
|
|
61
|
-
writeFileSync(dest, buf);
|
|
62
|
-
return dest;
|
|
63
|
-
}
|
|
64
|
-
export {
|
|
65
|
-
caseCachePath,
|
|
66
|
-
defaultPublicCasesPath,
|
|
67
|
-
fetchPublicCaseToCache,
|
|
68
|
-
loadCaseById,
|
|
69
|
-
loadCasesFromJson,
|
|
70
|
-
loadDefaultCases,
|
|
71
|
-
verifySha256
|
|
72
|
-
};
|
package/dist/esm/synthetic.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// src/synthetic.ts
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import * as dcmjs from "dcmjs";
|
|
5
|
-
var CT_SOP_CLASS = "1.2.840.10008.5.1.4.1.1.2";
|
|
6
|
-
var VARIANT_INDEX = {
|
|
7
|
-
"minimal-invalid-uid": 0,
|
|
8
|
-
"valid-uid": 1,
|
|
9
|
-
"vendor-warnings": 2
|
|
10
|
-
};
|
|
11
|
-
function uidFor(variant, role) {
|
|
12
|
-
const i = VARIANT_INDEX[variant];
|
|
13
|
-
if (variant === "minimal-invalid-uid") {
|
|
14
|
-
return `2.25.100000000000000000000000000000.${role}.${i}`;
|
|
15
|
-
}
|
|
16
|
-
const uidRoots = [
|
|
17
|
-
"1000000000000000000000000000000000001",
|
|
18
|
-
"1000000000000000000000000000000000002",
|
|
19
|
-
"1000000000000000000000000000000000003"
|
|
20
|
-
];
|
|
21
|
-
const roleSuffix = role === "study" ? 1 : role === "series" ? 2 : 3;
|
|
22
|
-
return `2.25.${uidRoots[i]}.${roleSuffix}`;
|
|
23
|
-
}
|
|
24
|
-
function naturalDataset(variant) {
|
|
25
|
-
const studyUID = uidFor(variant, "study");
|
|
26
|
-
const seriesUID = uidFor(variant, "series");
|
|
27
|
-
const sopUID = uidFor(variant, "sop");
|
|
28
|
-
const dataset = {
|
|
29
|
-
PatientName: "SYNTH^SUBJECT",
|
|
30
|
-
PatientID: "SYNTH_PID",
|
|
31
|
-
Modality: "CT",
|
|
32
|
-
SOPClassUID: CT_SOP_CLASS,
|
|
33
|
-
SOPInstanceUID: sopUID,
|
|
34
|
-
SeriesInstanceUID: seriesUID,
|
|
35
|
-
StudyInstanceUID: studyUID,
|
|
36
|
-
StudyDescription: "Synthetic study",
|
|
37
|
-
SeriesDescription: "Synthetic series",
|
|
38
|
-
Rows: 1,
|
|
39
|
-
Columns: 1,
|
|
40
|
-
BitsAllocated: 16,
|
|
41
|
-
BitsStored: 16,
|
|
42
|
-
HighBit: 15,
|
|
43
|
-
SamplesPerPixel: 1,
|
|
44
|
-
PhotometricInterpretation: "MONOCHROME2",
|
|
45
|
-
PixelRepresentation: 0,
|
|
46
|
-
PixelData: new Uint8Array([0, 0]).buffer
|
|
47
|
-
};
|
|
48
|
-
if (variant === "vendor-warnings") {
|
|
49
|
-
dataset.Laterality = "";
|
|
50
|
-
dataset.PatientWeight = "0";
|
|
51
|
-
}
|
|
52
|
-
return dataset;
|
|
53
|
-
}
|
|
54
|
-
function buildDicomDict(variant) {
|
|
55
|
-
const meta = {
|
|
56
|
-
FileMetaInformationVersion: new Uint8Array([0, 1]).buffer,
|
|
57
|
-
MediaStorageSOPClassUID: CT_SOP_CLASS,
|
|
58
|
-
MediaStorageSOPInstanceUID: uidFor(variant, "sop"),
|
|
59
|
-
TransferSyntaxUID: "1.2.840.10008.1.2.1",
|
|
60
|
-
ImplementationClassUID: "1.2.3.4",
|
|
61
|
-
ImplementationVersionName: "SYNTH"
|
|
62
|
-
};
|
|
63
|
-
const dicomDict = new dcmjs.data.DicomDict(
|
|
64
|
-
dcmjs.data.DicomMetaDictionary.denaturalizeDataset(meta)
|
|
65
|
-
);
|
|
66
|
-
dicomDict.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(
|
|
67
|
-
naturalDataset(variant)
|
|
68
|
-
);
|
|
69
|
-
return dicomDict;
|
|
70
|
-
}
|
|
71
|
-
function buildSyntheticCtBuffer(variant) {
|
|
72
|
-
return Buffer.from(
|
|
73
|
-
buildDicomDict(variant).write({ allowInvalidVRLength: true })
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
var SYNTHETIC_FIXTURES = [
|
|
77
|
-
{
|
|
78
|
-
filename: "minimal-ct-0.dcm",
|
|
79
|
-
variant: "minimal-invalid-uid",
|
|
80
|
-
description: "Minimal CT with text in UIDs (many dciodvfy errors)"
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
filename: "valid-uid-ct-0.dcm",
|
|
84
|
-
variant: "valid-uid",
|
|
85
|
-
description: "Minimal CT with numeric UIDs only (missing-module noise)"
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
filename: "vendor-warnings-ct-0.dcm",
|
|
89
|
-
variant: "vendor-warnings",
|
|
90
|
-
description: "Valid UIDs plus empty Laterality and zero PatientWeight"
|
|
91
|
-
}
|
|
92
|
-
];
|
|
93
|
-
function writeSyntheticFixturesToDir(outDir) {
|
|
94
|
-
const root = resolve(outDir);
|
|
95
|
-
mkdirSync(root, { recursive: true });
|
|
96
|
-
const written = [];
|
|
97
|
-
for (const { filename, variant } of SYNTHETIC_FIXTURES) {
|
|
98
|
-
const path = resolve(root, filename);
|
|
99
|
-
writeFileSync(path, buildSyntheticCtBuffer(variant));
|
|
100
|
-
written.push(path);
|
|
101
|
-
}
|
|
102
|
-
return written;
|
|
103
|
-
}
|
|
104
|
-
export {
|
|
105
|
-
SYNTHETIC_FIXTURES,
|
|
106
|
-
buildDicomDict,
|
|
107
|
-
buildSyntheticCtBuffer,
|
|
108
|
-
writeSyntheticFixturesToDir
|
|
109
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export type PublicCaseSource = {
|
|
2
|
-
kind: 'url';
|
|
3
|
-
url: string;
|
|
4
|
-
} | {
|
|
5
|
-
kind: 's3';
|
|
6
|
-
bucket: string;
|
|
7
|
-
key: string;
|
|
8
|
-
version_id?: string;
|
|
9
|
-
};
|
|
10
|
-
export type PublicCaseRecord = {
|
|
11
|
-
id: string;
|
|
12
|
-
violation_class: string;
|
|
13
|
-
source: PublicCaseSource;
|
|
14
|
-
sha256: string;
|
|
15
|
-
notes?: string;
|
|
16
|
-
dciodvfy_skip?: boolean;
|
|
17
|
-
};
|
|
18
|
-
/** Path to bundled `public-cases.json` (works from `src/` and `dist/esm/`). */
|
|
19
|
-
export declare function defaultPublicCasesPath(): string;
|
|
20
|
-
export declare function loadDefaultCases(): PublicCaseRecord[];
|
|
21
|
-
export declare function loadCasesFromJson(casesJsonPath: string): PublicCaseRecord[];
|
|
22
|
-
export declare function loadCaseById(casesJsonPath: string, id: string): PublicCaseRecord;
|
|
23
|
-
export declare function caseCachePath(sha256: string, root?: string): string;
|
|
24
|
-
export declare function verifySha256(buffer: Buffer, expected: string): void;
|
|
25
|
-
export declare function fetchPublicCaseToCache(record: PublicCaseRecord, cacheRoot?: string): Promise<string>;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import * as dcmjs from 'dcmjs';
|
|
2
|
-
export type SyntheticCtVariant = 'minimal-invalid-uid' | 'valid-uid' | 'vendor-warnings';
|
|
3
|
-
export declare function buildDicomDict(variant: SyntheticCtVariant): dcmjs.data.DicomDict;
|
|
4
|
-
export declare function buildSyntheticCtBuffer(variant: SyntheticCtVariant): Buffer;
|
|
5
|
-
export declare const SYNTHETIC_FIXTURES: {
|
|
6
|
-
filename: string;
|
|
7
|
-
variant: SyntheticCtVariant;
|
|
8
|
-
description: string;
|
|
9
|
-
}[];
|
|
10
|
-
/** Write all catalogue fixtures to a directory (created if missing). */
|
|
11
|
-
export declare function writeSyntheticFixturesToDir(outDir: string): string[];
|