dcmjs
Version:
Javascript implementation of DICOM manipulation
146 lines (131 loc) • 4.22 kB
JavaScript
/**
* Create the dictionary.js DICOM dictionary file from the Standard.
* Reformat The DICOM dictionary PS3.6 and PS3.7 docbook XML files (from e.g. standard docs) to JavaScript syntax.
* Based on https://github.com/pydicom/pydicom/blob/8112bb69bfc0423c3a08cb89e7960defbe7237bf/source/generate_dict/generate_dicom_dict.py
*/
const fs = require('fs/promises');
const https = require('https');
const xml2js = require('xml2js');
require('@babel/register');
const DICTIONARY_PATH = './src/dictionary.js';
const dictionary = require(DICTIONARY_PATH).default;
const { Tag } = require('./src/Tag');
async function main() {
const tags = [];
/**
* Collect DICOM tags from XML documents
*/
const part06 = await getDocbook('part06/part06.xml');
const part06Rows = part06.book.chapter.find(chapter => chapter.$['xml:id'] === 'chapter_6').table[0].tbody[0].tr;
tags.push(...part06Rows.map(row => {
const retired = getCellData(row.td[5])?.startsWith('RET');
const name = getCellData(row.td[2]);
return {
tag: getCellData(row.td[0]),
vr: getCellData(row.td[3]),
name: retired ? `RETIRED_${name}` : name,
vm: getCellData(row.td[4]),
version: retired ? 'DICOM/retired' : 'DICOM',
}
}));
const part07 = await getDocbook('part07/part07.xml');
const chapterE = part07.book.chapter.find(chapter => chapter.$['xml:id'] === 'chapter_E');
const commandFields = chapterE.section[0].table[0].tbody[0].tr;
tags.push(...commandFields.map(row => {
return {
tag: getCellData(row.td[0]),
vr: getCellData(row.td[3]),
name: getCellData(row.td[2]),
vm: getCellData(row.td[4]),
version: 'DICOM',
}
}));
const retiredCommandFields = chapterE.section[1].table[0].tbody[0].tr;
tags.push(...retiredCommandFields.map(row => {
return {
tag: getCellData(row.td[0]),
vr: getCellData(row.td[3]),
name: `RETIRED_${getCellData(row.td[2])}`,
vm: getCellData(row.td[4]),
version: 'DICOM/retired',
}
}));
const newTags = tags.filter(tag => tag.vr && tag.name && tag.vm)
.filter(tag => !(tag.tag in dictionary)) // filter already defined
.filter(tag => !/[(,\dA-F]x+[A-F\d,)]/.test(tag.tag)); // filter repeater tags
/**
* Insert new tags into dictionary, ordered among tags with the same version
*/
const dictionaryArray = Object.values(dictionary);
for (const newTag of newTags) {
const parsedTag = Tag.fromPString(newTag.tag);
const insertIndex = dictionaryArray.findIndex(tag => {
if (tag.version !== newTag.version) {
return false;
}
const thisTag = Tag.fromPString(tag.tag);
return thisTag.toCleanString() > parsedTag.toCleanString();
});
dictionaryArray.splice(insertIndex, 0, newTag);
}
await writeDictionary(dictionaryArray);
}
async function writeDictionary(tags) {
let data = 'const dictionary = {';
for (const tag of tags) {
if (!tag.tag) {
data += `
"": {
tag: ""
},`
continue;
}
const tagKey = tag.tag.includes('"') ? `'${tag.tag}'` : `"${tag.tag}"`;
data += `
${tagKey}: {
tag: ${tagKey},
vr: "${tag.vr}",
name: "${tag.name}",
vm: "${tag.vm}",
version: "${tag.version ?? 'PrivateTag'}"
},`;
}
data += `
};
export default dictionary;
`;
await fs.writeFile(DICTIONARY_PATH, data);
}
async function getDocbook(part) {
const source = await getUrl(`https://dicom.nema.org/medical/dicom/current/source/docbook/${part}`);
return xml2js.parseStringPromise(source);
}
function getCellData(td) {
const para = td.para?.[0];
if (!para) {
return undefined;
}
const text = para.emphasis ? para.emphasis[0]._ : para._;
return text?.trim().replace(/[\u200b\uffff]/g, '');
}
function getUrl(url) {
return new Promise((resolve, reject) => {
https.get(url, request => {
let data = '';
request.on('error', () => {
reject(error);
});
request.on('end', () => {
resolve(data);
});
request.on('data', chunk => {
data += chunk;
});
});
});
}
if (require.main === module) {
main().catch(error => {
console.log(error);
});
}