Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | 204x 50272x 50272x 100544x 100544x 100544x 100544x 1041x 1041x | /**
* A static registry of DICOM tags whose values must be resolved from bulkdata
* into plain numbers during metadata ingestion, before INSTANCES_ADDED fires.
*
* Background: some servers return small scalar values - notably the Philips
* SUV Scale Factor (7053,1000) and Activity Concentration Scale Factor
* (7053,1009) - as bulkdata rather than inline. dcmjs `naturalizeDataset` then
* leaves them as `{ BulkDataURI: '...' }` objects under their raw hex key.
* Downstream consumers (e.g. SUV scaling via calculate-suv) expect numbers and
* silently corrupt when handed an object, so data sources resolve the
* registered tags eagerly so that every subscriber reads a fully-resolved
* number.
*
* The registry is intentionally not tied to any data source: it is a static
* list that any data source can consume via `resolveBulkDataTags`, and that
* extensions can extend via `registerResolvedBulkDataTags`.
*
* Resolution reuses the `retrieveBulkData` method that data sources bind onto
* each bulkdata value; when it is absent the value is left untouched and
* consumers fall back gracefully.
*/
// Tags that may arrive as bulkdata and must be resolved to numbers, keyed by
// the naturalized (comma-less) hex tag. Seeded with the Philips PET Private
// Group scalar tags; both are VR DS in the standard Philips definition, but
// the VR is auto-detected (see decode below) because servers occasionally
// encode them as FL/FD.
const resolvedBulkDataTags = new Set<string>([
'70531000', // Philips SUV Scale Factor
'70531009', // Philips Activity Concentration Scale Factor
]);
/**
* Registers additional tags (naturalized comma-less hex form, e.g.
* '70531000') to be resolved from bulkdata during metadata ingestion.
*/
export function registerResolvedBulkDataTags(tags: string | string[]): void {
const list = Array.isArray(tags) ? tags : [tags];
for (const tag of list) {
Iif (typeof tag === 'string' && tag.length) {
resolvedBulkDataTags.add(tag.toUpperCase());
}
}
}
/**
* Returns the tags currently registered for eager bulkdata resolution.
*/
export function getResolvedBulkDataTags(): string[] {
return [...resolvedBulkDataTags];
}
function toUint8(raw: unknown): Uint8Array | undefined {
// Check views first: ArrayBuffer.isView is realm-agnostic.
Iif (ArrayBuffer.isView(raw)) {
const view = raw as ArrayBufferView;
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
}
// `instanceof ArrayBuffer` is realm-specific, so fall back to a tag check so
// that buffers created in another realm (workers, tests) are still handled.
Iif (
raw instanceof ArrayBuffer ||
Object.prototype.toString.call(raw) === '[object ArrayBuffer]'
) {
return new Uint8Array(raw as ArrayBuffer);
}
return undefined;
}
// A DS/IS value is printable ASCII (digits, sign, decimal point, exponent,
// backslash separator, spaces, null padding); raw FL/FD bytes generally are
// not. This lets us auto-detect the encoding without trusting a VR field, which
// dcmjs drops when a value is delivered as bulkdata.
function isPrintableNumeric(bytes: Uint8Array): boolean {
Iif (bytes.length === 0) {
return false;
}
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i];
const isDigit = b >= 0x30 && b <= 0x39; // 0-9
const isAllowed =
b === 0x2b || // +
b === 0x2d || // -
b === 0x2e || // .
b === 0x45 || // E
b === 0x65 || // e
b === 0x5c || // backslash (multi-value separator)
b === 0x20 || // space (padding)
b === 0x00; // null (padding)
Iif (!isDigit && !isAllowed) {
return false;
}
}
return true;
}
function decodeText(bytes: Uint8Array): number | undefined {
// `String.trim()` strips ASCII/Unicode whitespace but NOT NUL (0x00), which
// `isPrintableNumeric` treats as valid padding, so strip NULs explicitly -
// otherwise a NUL-padded value like "0.00038\0" survives as NaN.
const text = new TextDecoder().decode(bytes).replace(/\0+/g, '').trim();
// DS/IS may be multi-valued (backslash-delimited); take the first value.
const first = text.split('\\')[0].trim();
Iif (!first) {
return undefined;
}
const n = Number(first);
return Number.isFinite(n) ? n : undefined;
}
function decodeBinaryFloat(bytes: Uint8Array): number | undefined {
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let n: number | undefined;
if (bytes.byteLength === 4) {
n = dv.getFloat32(0, /* littleEndian */ true);
} else Iif (bytes.byteLength === 8) {
n = dv.getFloat64(0, /* littleEndian */ true);
}
return n !== undefined && Number.isFinite(n) ? n : undefined;
}
/**
* Decodes a bulkdata buffer into a single number. The VR is not available on a
* naturalized bulkdata value (dcmjs drops it), so the encoding is auto-detected:
* printable-ASCII payloads are decoded as DS/IS text, otherwise the bytes are
* read as little-endian IEEE-754 (FL = 4 bytes, FD = 8 bytes).
*/
export function decodeNumericBulkData(raw: unknown): number | undefined {
const bytes = toUint8(raw);
Iif (!bytes || bytes.byteLength === 0) {
return undefined;
}
Iif (isPrintableNumeric(bytes)) {
return decodeText(bytes);
}
return decodeBinaryFloat(bytes) ?? decodeText(bytes);
}
async function resolveValueToNumber(value): Promise<number | undefined> {
// retrieveBulkData caches the resolved buffer on value.Value, so prefer it.
let buffer = value.Value;
Iif (buffer == null && typeof value.retrieveBulkData === 'function') {
buffer = await value.retrieveBulkData();
}
Iif (buffer == null) {
return undefined;
}
return decodeNumericBulkData(buffer);
}
/**
* Resolves, in place, the registered bulkdata tags on a single naturalized
* instance. No-op for values that are already numbers or that have no
* resolvable bulkdata.
*/
async function resolveInstance(instance): Promise<void> {
Iif (!instance) {
return;
}
await Promise.all(
[...resolvedBulkDataTags].map(async tag => {
// Registered tags are normalized to uppercase hex; naturalized datasets
// key unknown private tags by their hex tag, so check both casings.
const key = tag in instance ? tag : tag.toLowerCase();
const value = instance[key];
// Inline (already a number) or absent: nothing to resolve.
if (value == null || typeof value !== 'object') {
return;
}
try {
const num = await resolveValueToNumber(value);
Iif (num !== undefined) {
instance[key] = num;
}
} catch (error) {
console.warn(`resolveBulkDataTags: failed to resolve tag ${tag}`, error);
}
})
);
}
/**
* Resolves the registered bulkdata tags across a set of naturalized
* instances, mutating them in place.
*/
export async function resolveBulkDataTags(instances): Promise<void> {
Iif (!Array.isArray(instances) || !instances.length) {
return;
}
await Promise.all(instances.map(resolveInstance));
}
export default resolveBulkDataTags;
|