@cosyte/dicom 0.0.11 → 0.0.12

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/index.d.cts CHANGED
@@ -307,6 +307,26 @@ interface ParseOptions {
307
307
  * When `true`, every Tier-2 warning is escalated to a thrown
308
308
  * `DicomParseError` carrying the warning code. Default `false`.
309
309
  *
310
+ * 🛑 **THE ESCALATED DIAGNOSTIC CARRIES SOURCE BYTES THAT THE WARNING DOES
311
+ * NOT.** A `DicomParseWarning.message` is a frozen registry string with only
312
+ * structural tokens filled in, so it is safe to log whole; the
313
+ * `DicomParseError` this option raises in its place also carries `snippet`,
314
+ * **16 raw bytes, unredacted** (D-10), read at the warning's own `byteOffset`.
315
+ * **Which element those bytes belong to is not contracted**: that offset's
316
+ * frame follows where the element was read - file-absolute at the root,
317
+ * relative to the enclosing slice inside a defined-length Sequence or Item,
318
+ * and into the inflated stream under Deflated Explicit VR LE - while the
319
+ * snippet is cut from whichever buffer the parse is holding, so the two can
320
+ * disagree and the bytes can be an unrelated element's value. Do not reason
321
+ * from a code's message to what its snippet holds - **measure it, and treat
322
+ * every one of them as document content.**
323
+ *
324
+ * That is the documented design of `snippet` rather than a defect in any one
325
+ * code, and turning this option on does not change what any of them means -
326
+ * but a message-only PHI review of the lenient path does not transfer to the
327
+ * strict one. Log `err.code`, `err.byteOffset` and `err.message`; treat
328
+ * `err.snippet` as PHI.
329
+ *
310
330
  * Omit (do not pass `undefined`) to use the default.
311
331
  */
312
332
  readonly strict?: boolean;
@@ -420,6 +440,7 @@ declare const WARNING_CODES: {
420
440
  readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
421
441
  readonly DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED";
422
442
  readonly DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED";
443
+ readonly DICOM_DEIDENT_METHOD_NOT_LO: "DICOM_DEIDENT_METHOD_NOT_LO";
423
444
  readonly DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED";
424
445
  readonly DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE";
425
446
  readonly DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE";
@@ -2692,6 +2713,19 @@ interface DeidentifyOptions {
2692
2713
  * Text **added to** `(0012,0063)` De-identification Method. Default names the
2693
2714
  * Basic Profile and the active options.
2694
2715
  *
2716
+ * **The default is multi-valued, one Value per name, and that is a conformance
2717
+ * fix rather than a style choice.** PS3.5 2026c Table 6.2-1 caps an `LO` at
2718
+ * **64 characters per Value**, and `(0012,0063)` is `1-n`; the single-value
2719
+ * default this replaced measured 76 characters with no options and 272 with
2720
+ * all nine, so every run this library ever made wrote a value no `LO` may
2721
+ * legally carry. The Profile name is now one Value of 61 characters and each
2722
+ * active option is its own, so no option subset can exceed the maximum.
2723
+ *
2724
+ * **Your string is not bounded here.** A value of your own longer than 64
2725
+ * characters is written through as given, with no warning: it is yours, and
2726
+ * splitting or truncating it would invent a record you did not write. Split it
2727
+ * on `\` yourself if a strict receiver is in your path.
2728
+ *
2695
2729
  * PS3.15 E.1.1 says this string is "inserted in or added to" the attribute, so
2696
2730
  * a value the incoming Data Set already carried is kept and this one is
2697
2731
  * appended after a `\` as a further value of the `1-n` attribute - the
@@ -2721,7 +2755,10 @@ interface DeidentifyOptions {
2721
2755
  *
2722
2756
  * `DICOM_DEIDENT_METHOD_NOT_ADDED` means "the length ceiling was reached", never
2723
2757
  * "every fallback is disclosed": a `(0012,0063)` a file encoded under a VR other
2724
- * than `LO` is also replaced, and that one is silent.
2758
+ * than `LO` is also replaced, and that one raises `DICOM_DEIDENT_METHOD_NOT_LO`.
2759
+ * Two codes rather than one because the causes are unrelated - the chain outgrew
2760
+ * the VR, or the bytes were never in that VR at all - and a prior value that is
2761
+ * empty or padding only raises neither, because nothing was lost.
2725
2762
  *
2726
2763
  * When the prior value **is** kept, `report.warnings` carries
2727
2764
  * `DICOM_DEIDENT_METHOD_PRIOR_RETAINED`: `(0012,0063)` is not in Table E.1-1, so
package/dist/index.d.ts CHANGED
@@ -307,6 +307,26 @@ interface ParseOptions {
307
307
  * When `true`, every Tier-2 warning is escalated to a thrown
308
308
  * `DicomParseError` carrying the warning code. Default `false`.
309
309
  *
310
+ * 🛑 **THE ESCALATED DIAGNOSTIC CARRIES SOURCE BYTES THAT THE WARNING DOES
311
+ * NOT.** A `DicomParseWarning.message` is a frozen registry string with only
312
+ * structural tokens filled in, so it is safe to log whole; the
313
+ * `DicomParseError` this option raises in its place also carries `snippet`,
314
+ * **16 raw bytes, unredacted** (D-10), read at the warning's own `byteOffset`.
315
+ * **Which element those bytes belong to is not contracted**: that offset's
316
+ * frame follows where the element was read - file-absolute at the root,
317
+ * relative to the enclosing slice inside a defined-length Sequence or Item,
318
+ * and into the inflated stream under Deflated Explicit VR LE - while the
319
+ * snippet is cut from whichever buffer the parse is holding, so the two can
320
+ * disagree and the bytes can be an unrelated element's value. Do not reason
321
+ * from a code's message to what its snippet holds - **measure it, and treat
322
+ * every one of them as document content.**
323
+ *
324
+ * That is the documented design of `snippet` rather than a defect in any one
325
+ * code, and turning this option on does not change what any of them means -
326
+ * but a message-only PHI review of the lenient path does not transfer to the
327
+ * strict one. Log `err.code`, `err.byteOffset` and `err.message`; treat
328
+ * `err.snippet` as PHI.
329
+ *
310
330
  * Omit (do not pass `undefined`) to use the default.
311
331
  */
312
332
  readonly strict?: boolean;
@@ -420,6 +440,7 @@ declare const WARNING_CODES: {
420
440
  readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
421
441
  readonly DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED";
422
442
  readonly DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED";
443
+ readonly DICOM_DEIDENT_METHOD_NOT_LO: "DICOM_DEIDENT_METHOD_NOT_LO";
423
444
  readonly DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED";
424
445
  readonly DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE";
425
446
  readonly DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE";
@@ -2692,6 +2713,19 @@ interface DeidentifyOptions {
2692
2713
  * Text **added to** `(0012,0063)` De-identification Method. Default names the
2693
2714
  * Basic Profile and the active options.
2694
2715
  *
2716
+ * **The default is multi-valued, one Value per name, and that is a conformance
2717
+ * fix rather than a style choice.** PS3.5 2026c Table 6.2-1 caps an `LO` at
2718
+ * **64 characters per Value**, and `(0012,0063)` is `1-n`; the single-value
2719
+ * default this replaced measured 76 characters with no options and 272 with
2720
+ * all nine, so every run this library ever made wrote a value no `LO` may
2721
+ * legally carry. The Profile name is now one Value of 61 characters and each
2722
+ * active option is its own, so no option subset can exceed the maximum.
2723
+ *
2724
+ * **Your string is not bounded here.** A value of your own longer than 64
2725
+ * characters is written through as given, with no warning: it is yours, and
2726
+ * splitting or truncating it would invent a record you did not write. Split it
2727
+ * on `\` yourself if a strict receiver is in your path.
2728
+ *
2695
2729
  * PS3.15 E.1.1 says this string is "inserted in or added to" the attribute, so
2696
2730
  * a value the incoming Data Set already carried is kept and this one is
2697
2731
  * appended after a `\` as a further value of the `1-n` attribute - the
@@ -2721,7 +2755,10 @@ interface DeidentifyOptions {
2721
2755
  *
2722
2756
  * `DICOM_DEIDENT_METHOD_NOT_ADDED` means "the length ceiling was reached", never
2723
2757
  * "every fallback is disclosed": a `(0012,0063)` a file encoded under a VR other
2724
- * than `LO` is also replaced, and that one is silent.
2758
+ * than `LO` is also replaced, and that one raises `DICOM_DEIDENT_METHOD_NOT_LO`.
2759
+ * Two codes rather than one because the causes are unrelated - the chain outgrew
2760
+ * the VR, or the bytes were never in that VR at all - and a prior value that is
2761
+ * empty or padding only raises neither, because nothing was lost.
2725
2762
  *
2726
2763
  * When the prior value **is** kept, `report.warnings` carries
2727
2764
  * `DICOM_DEIDENT_METHOD_PRIOR_RETAINED`: `(0012,0063)` is not in Table E.1-1, so
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/version.ts
12
- var VERSION = "0.0.11";
12
+ var VERSION = "0.0.12";
13
13
 
14
14
  // src/dictionary/index.ts
15
15
  var dictionary_exports = {};
@@ -11746,6 +11746,8 @@ var WARNING_CODES = {
11746
11746
  // emitted by deidentify(), never by the parser
11747
11747
  DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED",
11748
11748
  // emitted by deidentify(), never by the parser
11749
+ DICOM_DEIDENT_METHOD_NOT_LO: "DICOM_DEIDENT_METHOD_NOT_LO",
11750
+ // emitted by deidentify(), never by the parser
11749
11751
  DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED",
11750
11752
  // emitted by deidentify(), never by the parser
11751
11753
  DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE",
@@ -11795,6 +11797,11 @@ var WARNING_MESSAGES = Object.freeze({
11795
11797
  // constant of this code rather than composed from input, as in the two
11796
11798
  // (0002,0000) messages above.
11797
11799
  DICOM_DEIDENT_METHOD_NOT_ADDED: "The De-identification Method this run recorded could not be added beside the value (0012,0063) already carried without exceeding the largest Value Length that VR can encode, so the earlier value was replaced (PS3.15 E.1.1). The replaced text is not in the output.",
11800
+ // No value, no length AND NO VR. The VR is two bytes read from the file, and a
11801
+ // fabricated header makes those two bytes document content - the shape that
11802
+ // put "ITHS" into DICOM_NONZERO_RESERVED_BYTES. Naming the VR here would read
11803
+ // as harmless and is exactly the same defect.
11804
+ DICOM_DEIDENT_METHOD_NOT_LO: "The (0012,0063) value the source file carried is encoded under a Value Representation other than LO, which is not a De-identification Method this run can add its own text to (PS3.15 E.1.1), so the earlier value was replaced. The replaced text is not in the output. The text and the VR are both withheld from this message.",
11798
11805
  // No value and no length, for the same reason as the code above: the retained
11799
11806
  // text is the file's own. The tag is a constant of this code.
11800
11807
  DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "A De-identification Method (0012,0063) value the source file already carried was kept beside the one this run recorded, as PS3.15 E.1.1 requires. That attribute is not in Table E.1-1, so no rule in this run inspected, audited or redacted those bytes: if the sender wrote identifying text there it is in the de-identified output, under (0012,0062) = YES. The text is withheld from this message.",
@@ -11913,6 +11920,9 @@ function burnedInAnnotationNotRemoved(position) {
11913
11920
  function deidentMethodNotAdded(position) {
11914
11921
  return build(WARNING_CODES.DICOM_DEIDENT_METHOD_NOT_ADDED, position);
11915
11922
  }
11923
+ function deidentMethodNotLo(position) {
11924
+ return build(WARNING_CODES.DICOM_DEIDENT_METHOD_NOT_LO, position);
11925
+ }
11916
11926
  function deidentMethodPriorRetained(position) {
11917
11927
  return build(WARNING_CODES.DICOM_DEIDENT_METHOD_PRIOR_RETAINED, position);
11918
11928
  }
@@ -15349,19 +15359,23 @@ function rebuildFileMeta(fileMeta, ctx) {
15349
15359
  mediaStorageSOPInstanceUID: ctx.remap.map(fileMeta.mediaStorageSOPInstanceUID)
15350
15360
  };
15351
15361
  }
15362
+ var DEFAULT_METHOD_PROFILE = "@cosyte/dicom Basic Application Level Confidentiality Profile";
15352
15363
  function defaultMethod(active) {
15353
- const base = "Cosyte @cosyte/dicom: PS3.15 Basic Application Level Confidentiality Profile";
15354
- return active.size === 0 ? base : `${base} + ${[...active].join(", ")}`;
15364
+ const options = DEIDENTIFY_OPTIONS.filter((option) => active.has(option));
15365
+ return [DEFAULT_METHOD_PROFILE, ...options].join(String.fromCharCode(VALUE_DELIMITER));
15355
15366
  }
15356
15367
  var VALUE_DELIMITER = 92;
15357
15368
  var MAX_SHORT_FORM_VALUE_BYTES = 65534;
15358
15369
  function addDeidentificationMethod(existing, method) {
15359
15370
  const added = trimTrailingPad(Buffer.from(method, "latin1"));
15360
- if (existing === void 0 || existing.vr !== "LO") {
15361
- return { value: added, replacedPrior: false, retainedPrior: false };
15371
+ const none = { replacedPrior: false, retainedPrior: false, replacedNonLoPrior: false };
15372
+ if (existing === void 0) return { value: added, ...none };
15373
+ if (existing.vr !== "LO") {
15374
+ const lost = trimTrailingPad(existing.rawBytes).length > 0;
15375
+ return { value: added, ...none, replacedNonLoPrior: lost };
15362
15376
  }
15363
15377
  const kept = Buffer.from(trimTrailingPad(existing.rawBytes));
15364
- if (kept.length === 0) return { value: added, replacedPrior: false, retainedPrior: false };
15378
+ if (kept.length === 0) return { value: added, ...none };
15365
15379
  const keptValues = splitValues(kept).map(trimTrailingPad);
15366
15380
  const missing = added.length === 0 ? [] : splitValues(added).filter((v) => !keptValues.some((k) => k.equals(trimTrailingPad(v))));
15367
15381
  const parts = [kept];
@@ -15370,9 +15384,9 @@ function addDeidentificationMethod(existing, method) {
15370
15384
  }
15371
15385
  const value = trimTrailingPad(Buffer.concat(parts));
15372
15386
  if (value.length > MAX_SHORT_FORM_VALUE_BYTES) {
15373
- return { value: added, replacedPrior: true, retainedPrior: false };
15387
+ return { value: added, ...none, replacedPrior: true };
15374
15388
  }
15375
- return { value, replacedPrior: false, retainedPrior: true };
15389
+ return { value, ...none, retainedPrior: true };
15376
15390
  }
15377
15391
  function trimTrailingPad(value) {
15378
15392
  let end = value.length;
@@ -15444,6 +15458,11 @@ function deidentify(ds, options = {}) {
15444
15458
  deidentMethodPriorRetained({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15445
15459
  );
15446
15460
  }
15461
+ if (deidentMethod.replacedNonLoPrior) {
15462
+ warnings.push(
15463
+ deidentMethodNotLo({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15464
+ );
15465
+ }
15447
15466
  if (hasUncleanedBurnedIn(ds)) {
15448
15467
  const offset = ds.get(TAG_PIXEL_DATA)?.byteOffset ?? 0;
15449
15468
  warnings.push(burnedInAnnotationNotRemoved({ byteOffset: offset, fileMeta: false }));