Integration Reference

Mirth Connect Transformer Reference

40 JavaScript transformer snippets for 40 DICOM attributes — production-adjacent patterns for DICOM-to-HL7v2 mapping, modality-based channel routing, study de-duplication, SOP class whitelist filters, MPPS workflow, and more.

Reference only. These snippets are production-adjacent starting points, not turnkey solutions. Always validate against your channel's specific message structure and your facility's interface specification. See the Mirth Connect User Guide + your vendor conformance statement for authoritative references.

All Snippets

Click a snippet title to expand the code; the attribute link at the right takes you to the full per-tag reference (with HL7 v2 mappings, FHIR paths, cloud support, common errors, and more).

  • Route DICOM by Accession Number — IHE OBR-18 mapping (0008,0050) Accession Number

    Per IHE RAD-TF, Accession Number lives in OBR-18 (Placer Field 1) for radiology orders. Some sites use OBR-3 or a Z-segment instead.

    // (0008,0050) Accession Number — primary identifier for radiology orders
    var accNum = String(msg.dataset.attr.(@tag == '00080050').value).trim();
    if (!accNum) {
      // Reject incoming DICOM without an Accession Number — common WL/MWL workflow assumption
      destinationSet.removeAll();
      return;
    }
    tmp['OBR'][18] = accNum;
    channelMap.put('accession', accNum); // also expose for downstream filters
  • Sanity-check Bits Allocated against expected modality range (0028,0100) Bits Allocated

    BitsAllocated >32 is invalid for non-floating-point modalities. Bad values often indicate corruption or vendor non-conformance.

    // (0028,0100) Bits Allocated — must be 8, 16, or 32 for image storage SOP classes
    var ba = parseInt(String(msg.dataset.attr.(@tag == '00280100').value), 10);
    if (ba !== 8 && ba !== 16 && ba !== 32) {
      logger.error('Invalid Bits Allocated: ' + ba + ' — DICOM file likely corrupted');
      router.routeMessageByChannelName('DICOM_QC_Quarantine', message.getRawData());
      destinationSet.removeAll();
      return;
    }
  • Normalize BodyPartExamined for routing (HEAD → BRAIN, etc.) (0018,0015) Body Part Examined

    BodyPartExamined values are loosely standardized — different modalities emit different strings for the same anatomy. Normalize before routing to PACS cohorts.

    // (0018,0015) Body Part Examined — CS, defined terms in PS3.16 Table L
    var raw = String(msg.dataset.attr.(@tag == '00180015').value).trim().toUpperCase();
    var synonyms = {
      'HEAD': 'BRAIN', 'CRANIUM': 'BRAIN', 'SKULL': 'HEAD',
      'CSPINE': 'CSPINE', 'C-SPINE': 'CSPINE', 'CERVICAL': 'CSPINE',
      'LSPINE': 'LSPINE', 'L-SPINE': 'LSPINE', 'LUMBAR': 'LSPINE',
      'ABD': 'ABDOMEN', 'ABDOMINAL': 'ABDOMEN',
      'CHEST': 'CHEST', 'THORAX': 'CHEST', 'THORACIC': 'CHEST'
    };
    var normalized = synonyms[raw] || raw;
    channelMap.put('body_part', normalized);
    // Override outbound to the normalized form so downstream consumers see consistent values
    tmp.dataset.attr.(@tag == '00180015').value = normalized;
  • Image Position (Patient) bounds check for cardiac MR slabs (0020,0032) Image Position (Patient)

    CardiacMR series often have ImagePosition values outside expected slab bounds — common indicator of patient repositioning mid-series.

    // (0020,0032) Image Position (Patient) — multi-value DS xyz in mm
    var posStr = String(msg.dataset.attr.(@tag == '00200032').value).split('\\');
    var x = parseFloat(posStr[0]); var y = parseFloat(posStr[1]); var z = parseFloat(posStr[2]);
    var prevZ = parseFloat(channelMap.get('prev_image_z') || z);
    if (Math.abs(z - prevZ) > 50) { // 50mm gap suggests patient repositioning
      channelMap.put('series_jump_warning', 'Z gap ' + Math.abs(z - prevZ).toFixed(1) + 'mm at instance');
    }
    channelMap.put('prev_image_z', z);
  • Route ORIGINAL vs DERIVED images to separate downstream channels (0008,0008) Image Type

    ImageType is a multi-valued CS (backslash-separated). Position 0 is ORIGINAL or DERIVED; position 1 is PRIMARY or SECONDARY. Derived images (MIP/MPR/3D) often need different handling than originals.

    // (0008,0008) Image Type — CS, DICOM multi-value (backslash separator)
    var it = String(msg.dataset.attr.(@tag == '00080008').value).split('\\');
    var pixelOrigin = (it[0] || '').trim().toUpperCase();
    var pixelType = (it[1] || '').trim().toUpperCase();
    channelMap.put('image_origin', pixelOrigin);
    if (pixelOrigin === 'DERIVED') {
      router.routeMessageByChannelName('DICOM_Derived_Store', message.getRawData());
      return; // don't forward to the primary archive
    }
    // Secondary captures (screen grabs, scanned forms) get a separate flag
    if (pixelType === 'SECONDARY') channelMap.put('is_sc', 'true');
  • Append free-text NTE comments to ImagingServiceRequestComments (0040,2400) Imaging Service Request Comments

    ORM messages with multiple NTE segments concatenate into ImagingServiceRequestComments — preserve order and source.

    // (0040,2400) Imaging Service Request Comments — concatenate HL7 v2 NTE segments
    var nteCount = msg['NTE'].length();
    var comments = [];
    for (var i = 0; i < nteCount; i++) {
      var c = String(msg['NTE'][i][3] || '').trim();
      if (c) comments.push(c);
    }
    if (comments.length > 0) {
      // DICOM LT max 10240 chars
      var combined = comments.join('\n').substring(0, 10240);
      channelMap.put('dicom_request_comments', combined);
    }
  • Set Institution Name from MSH-4 Sending Facility (0008,0080) Institution Name

    When constructing DICOM SR (Structured Report) from HL7 v2 ORU, derive InstitutionName from the v2 sending facility.

    // (0008,0080) Institution Name — derive from inbound HL7 MSH-4 Sending Facility
    var sendingFacility = String(msg['MSH'][4][1] || '').trim();
    if (sendingFacility) {
      // DICOM LO max 64 chars
      var instName = sendingFacility.substring(0, 64);
      channelMap.put('dicom_institution_name', instName);
    }
  • Manufacturer-specific quirk dispatcher (0008,0070) Manufacturer

    Different scanner vendors emit DICOM with vendor-specific quirks (private tag conventions, character set differences). Apply per-vendor patches in a dedicated transformer.

    // (0008,0070) Manufacturer — branch on vendor for quirk handling
    var mfr = String(msg.dataset.attr.(@tag == '00080070').value).trim().toUpperCase();
    if (mfr.indexOf('GE') === 0 || mfr.indexOf('G.E') === 0) {
      channelMap.put('vendor_quirk', 'GE');
      // GE scanners may emit private tags 0009,xxxx — preserve
    } else if (mfr.indexOf('SIEMENS') >= 0) {
      channelMap.put('vendor_quirk', 'SIEMENS');
      // Siemens uses 0029,xxxx for CSA headers
    } else if (mfr.indexOf('PHILIPS') >= 0 || mfr.indexOf('TOSHIBA') >= 0 || mfr.indexOf('CANON') >= 0) {
      channelMap.put('vendor_quirk', mfr.split(/\s+/)[0]);
    }
  • Manufacturer Model Name → equipment registry lookup (0008,1090) Manufacturer's Model Name

    Cross-reference the modality model against an internal registry to pull asset tag, calibration date, etc.

    // (0008,1090) Manufacturer's Model Name — look up against asset registry
    var model = String(msg.dataset.attr.(@tag == '00081090').value).trim();
    var mfr   = String(msg.dataset.attr.(@tag == '00080070').value).trim();
    var registryKey = (mfr + '|' + model).toUpperCase();
    // Mirth Database Reader pattern — query asset DB by composite key
    var dbConn = DatabaseConnectionFactory.createDatabaseConnection(
      'org.postgresql.Driver',
      configurationMap.get('ASSET_DB_URL'),
      configurationMap.get('ASSET_DB_USER'),
      configurationMap.get('ASSET_DB_PASS')
    );
    try {
      var rs = dbConn.executeCachedQuery('SELECT asset_tag FROM equipment WHERE registry_key = ?', [registryKey]);
      if (rs.next()) channelMap.put('asset_tag', rs.getString('asset_tag'));
    } finally { dbConn.close(); }
  • ModalitiesInStudy aggregate for Q/R STUDY-level replies (0008,0061) Modalities in Study

    When a PACS responds to C-FIND at STUDY level, (0008,0061) ModalitiesInStudy aggregates all series-level modalities. Mirth gateways often reconstruct this when proxying Q/R between legacy and modern backends.

    // (0008,0061) Modalities in Study — CS, backslash-separated unique modality set
    var seriesMods = channelMap.get('study_modalities'); // populated over series of C-STOREs
    var modSet = {};
    if (seriesMods) { String(seriesMods).split(',').forEach(function (m) { if (m) modSet[m.toUpperCase()] = true; }); }
    // Add the current series' modality if Q/R assembly is in-flight
    var current = String(msg.dataset.attr.(@tag == '00080060').value).trim().toUpperCase();
    if (current) modSet[current] = true;
    var mods = Object.keys(modSet).sort();
    channelMap.put('modalities_in_study', mods.join(','));
    // Write back to outbound DICOM as backslash-separated CS
    tmp.dataset.attr.(@tag == '00080061').value = mods.join('\\');
  • Modality-based channel routing (0008,0060) Modality

    Common Mirth pattern: a single DICOM Reader feeds modality-specific downstream channels via routeMessage.

    // (0008,0060) Modality — route to vendor-specific PACS channels
    var modality = String(msg.dataset.attr.(@tag == '00080060').value).trim().toUpperCase();
    var routes = {
      'CT':  'CT_PACS',
      'MR':  'MR_PACS',
      'US':  'Ultrasound_VNA',
      'NM':  'NM_PACS',
      'CR':  'CR_PACS',
      'DX':  'CR_PACS', // share with CR
      'MG':  'Mammo_VNA'
    };
    var targetChannel = routes[modality];
    if (targetChannel) {
      router.routeMessageByChannelName(targetChannel, message.getRawData());
    } else {
      logger.warn('Unhandled modality: ' + modality);
    }
  • Map DICOM Patient ID to HL7 v2 PID-3 with identifier type (0010,0020) Patient ID

    DICOM-to-HL7v2 channel. PID-3 is repeating CX; the MRN entry needs PID-3.5='MR' to be parseable by EHRs.

    // (0010,0020) Patient ID — most facilities expect MRN form in PID-3.1 with MR as type
    var mrn = String(msg.dataset.attr.(@tag == '00100020').value);
    tmp['PID'][3][1][1] = mrn;            // ID Number
    tmp['PID'][3][1][4][1] = 'MyHospital'; // Assigning Authority Namespace ID
    tmp['PID'][3][1][5] = 'MR';            // Identifier Type Code
  • Patient Position parse — HFS / FFP / LFS for worklist display (0018,5100) Patient Position

    PatientPosition is a DICOM defined term (HFS=Head First Supine, FFS=Feet First Supine, etc.). RIS worklist tickets often want the expanded form.

    // (0018,5100) Patient Position — CS defined term
    var pp = String(msg.dataset.attr.(@tag == '00185100').value).trim().toUpperCase();
    var positions = {
      'HFS': 'Head First - Supine',
      'HFP': 'Head First - Prone',
      'HFDR': 'Head First - Decubitus Right',
      'HFDL': 'Head First - Decubitus Left',
      'FFS': 'Feet First - Supine',
      'FFP': 'Feet First - Prone',
      'FFDR': 'Feet First - Decubitus Right',
      'FFDL': 'Feet First - Decubitus Left'
    };
    channelMap.put('patient_position_code', pp);
    channelMap.put('patient_position_text', positions[pp] || pp);
  • Convert DICOM Patient Age (AS) to numeric years for FHIR (0010,1010) Patient's Age

    DICOM AS encodes age + unit (e.g., '045Y', '009M', '012W', '003D'). FHIR Patient.birthDate is a date — but for derived FHIR.Observation patient-age, we need a numeric.

    // (0010,1010) Patient Age — AS format: nnnY/M/W/D
    var ageStr = String(msg.dataset.attr.(@tag == '00101010').value).trim();
    var match = ageStr.match(/^(\d{3})([YMWD])$/);
    if (match) {
      var n = parseInt(match[1], 10);
      var unit = match[2];
      var years = unit === 'Y' ? n
                : unit === 'M' ? n / 12
                : unit === 'W' ? n / 52.143
                : n / 365.25; // D
      channelMap.put('patient_age_years', years.toFixed(2));
    }
  • Convert DICOM Patient Birth Date (DA) to HL7 v2 TS (0010,0030) Patient's Birth Date

    DICOM DA is YYYYMMDD; HL7 v2 PID-7 is TS (timestamp) where the time portion is optional.

    // (0010,0030) Patient's Birth Date — DA is already YYYYMMDD which is also valid TS
    var dob = String(msg.dataset.attr.(@tag == '00100030').value).trim();
    if (/^\d{8}$/.test(dob)) {
      tmp['PID'][7][1] = dob; // already in correct YYYYMMDD form
    } else {
      channelMap.put('birthdate_warning', 'Malformed DOB: ' + dob);
      tmp['PID'][7][1] = ''; // signal absence to downstream
    }
  • Map DICOM Patient Name to HL7 v2 PID-5 (0010,0010) Patient's Name

    DICOM-to-HL7v2 channel. Inbound DICOM parsed to E4X XML by Mirth's DICOM Reader. Outbound HL7 v2 ADT or ORM message.

    // (0010,0010) Patient's Name uses 5-component PN format: family^given^middle^prefix^suffix
    var pn = String(msg.dataset.attr.(@tag == '00100010').value);
    var parts = pn.split('^');
    tmp['PID'][5][1] = parts[0] || ''; // family name complex
    tmp['PID'][5][2] = parts[1] || ''; // given name complex
    tmp['PID'][5][3] = parts[2] || ''; // middle name
    tmp['PID'][5][4] = parts[3] || ''; // prefix
    tmp['PID'][5][5] = parts[4] || ''; // suffix
  • Validate DICOM Patient Sex against HL7 v2 PID-8 enum (0010,0040) Patient's Sex

    Both standards use M/F/O/U code values. Some legacy modalities emit non-standard codes; normalize before transmission.

    // (0010,0040) Patient's Sex — normalize to HL7 v2 administrative-sex codes
    var sexRaw = String(msg.dataset.attr.(@tag == '00100040').value).toUpperCase().trim();
    var valid = { 'M': 'M', 'F': 'F', 'O': 'O', 'U': 'U', 'A': 'A' };
    tmp['PID'][8] = valid[sexRaw] || 'U'; // U = Unknown when source is non-standard
  • Patient Weight unit normalization (lb to kg) (0010,1030) Patient's Weight

    DICOM PatientWeight is in kilograms. Some HIS sources transmit pounds — apply unit conversion when sourcing from upstream HL7 v2 OBX.

    // (0010,1030) Patient Weight (DS, kg) — convert from lb if HIS source flagged units
    var wgtSrc = channelMap.get('hl7_obx_weight_value');     // raw OBX-5 value
    var wgtUnit = channelMap.get('hl7_obx_weight_units');    // OBX-6.1 typically 'kg' or 'lb'
    if (wgtSrc) {
      var w = parseFloat(wgtSrc);
      if (wgtUnit && wgtUnit.toLowerCase() === 'lb') w = w / 2.20462;
      if (!isNaN(w) && w > 0 && w < 600) {
        tmp.dataset.attr += <attr tag='00101030' vr='DS'>{w.toFixed(2)}</attr>;
      }
    }
  • Performed Procedure Step ID — link MPPS to Requested Procedure (0040,0253) Performed Procedure Step ID

    When MPPS arrives (N-CREATE / N-SET), ties back to the original Requested Procedure via (0040,0253) PerformedProcedureStepID. RIS needs this link to reconcile scheduled vs performed.

    // (0040,0253) Performed Procedure Step ID — SH, links MPPS N-message to the RIS procedure
    var ppsid = String(msg.dataset.attr.(@tag == '00400253').value).trim();
    if (!ppsid) { logger.warn('MPPS received without PerformedProcedureStepID — cannot reconcile'); return; }
    // Look up the original requested procedure (stored in DB at schedule time)
    var ctx = new JavaScriptContextFactory().call(new org.mozilla.javascript.ContextAction() {});
    var rpIdQuery = "SELECT requested_procedure_id FROM ris_schedule WHERE performed_step_id = ? LIMIT 1";
    var rpId = DatabaseConnectionFactory.createDatabaseConnection('jdbc:postgresql://...', 'user', 'pass').executeCachedQuery(rpIdQuery, [ppsid]).next() ? resultSet.getString(1) : null;
    if (rpId) {
      tmp.dataset.attr.(@tag == '00401001').value = rpId; // Requested Procedure ID
      channelMap.put('mpps_reconciled', 'true');
    } else {
      channelMap.put('mpps_unmatched', ppsid);
      router.routeMessageByChannelName('MPPS_Manual_Review', message.getRawData());
    }
  • MPPS performed-procedure-step start timestamp (0040,0244) Performed Procedure Step Start Date

    MPPS (Modality Performed Procedure Step) workflows fire N-CREATE / N-SET messages. Mirth often acts as the receiver; persist the start time for downstream billing or workflow.

    // (0040,0244) Performed Procedure Step Start Date + (0040,0245) PPSStartTime
    var d = String(msg.dataset.attr.(@tag == '00400244').value).trim();
    var t = String(msg.dataset.attr.(@tag == '00400245').value).trim();
    if (d.length === 8 && t.length >= 6) {
      var ts = d + t.substring(0, 6); // YYYYMMDDHHMMSS — HL7 v2 TS format
      channelMap.put('mpps_start_ts', ts);
    }
  • PhotometricInterpretation guard — reject unsupported color spaces (0028,0004) Photometric Interpretation

    Archive may only accept MONOCHROME2 or RGB; legacy ultrasounds sometimes emit YBR_FULL_422 or PALETTE COLOR that the downstream viewer mis-renders.

    // (0028,0004) Photometric Interpretation — CS defined terms
    var pi = String(msg.dataset.attr.(@tag == '00280004').value).trim().toUpperCase();
    var accepted = ['MONOCHROME1','MONOCHROME2','RGB','YBR_FULL'];
    if (accepted.indexOf(pi) < 0) {
      logger.warn('Rejecting PhotometricInterpretation: ' + pi + ' for SOP ' + String(msg.dataset.attr.(@tag == '00080018').value));
      channelMap.put('rejection_reason', 'unsupported_photometric:' + pi);
      destinationSet.removeAll();
      return;
    }
    channelMap.put('photometric', pi);
  • Strip PixelData when forwarding metadata-only to FHIR ImagingStudy (7FE0,0010) Pixel Data

    When generating a FHIR ImagingStudy resource that references the DICOM via WADO-RS endpoint, you want metadata only — keep payload size manageable.

    // (7FE0,0010) Pixel Data — exclude from outbound metadata-only forward
    // In a Mirth source-to-destination flow building a FHIR resource:
    var metadata = msg.copy();
    // E4X delete pixel data attribute
    delete metadata.dataset.attr.(@tag == '7FE00010');
    channelMap.put('metadata_only_size', String(metadata.toString().length));
    return metadata;
  • Extract Pixel Spacing for measurement-aware downstream (0028,0030) Pixel Spacing

    PixelSpacing (DS \\ DS, mm) drives quantitative measurements. Convert to a per-axis float pair and stash in channelMap for downstream FHIR enrichment.

    // (0028,0030) Pixel Spacing — multi-value DS
    var ps = String(msg.dataset.attr.(@tag == '00280030').value).split('\\');
    var rowSpacing = parseFloat(ps[0]);
    var colSpacing = parseFloat(ps[1]);
    if (!isNaN(rowSpacing) && !isNaN(colSpacing)) {
      channelMap.put('pixel_spacing_row', rowSpacing.toFixed(6));
      channelMap.put('pixel_spacing_col', colSpacing.toFixed(6));
    }
  • Pregnancy Status — alert routing for radiation modalities (0010,21C0) Pregnancy Status

    When inbound DICOM is from CT/RT/Mammo and PregnancyStatus is 'pregnant' or 'unknown', flag for additional review per facility radiation safety policy.

    // (0010,21C0) Pregnancy Status — radiation safety flag for ionizing-radiation modalities
    var modality = String(msg.dataset.attr.(@tag == '00080060').value).trim().toUpperCase();
    var isIonizing = ['CT','RT','MG','XA','RF','PT','NM','CR','DX','BMD'].indexOf(modality) >= 0;
    if (!isIonizing) return;
    var preg = String(msg.dataset.attr.(@tag == '001021C0').value).trim();
    // 1=Not Pregnant, 2=Possibly Pregnant, 3=Definitely Pregnant, 4=Unknown
    if (preg === '2' || preg === '3' || preg === '4') {
      router.routeMessageByChannelName('Pregnancy_Alert', message.getRawData());
      channelMap.put('safety_flag', 'pregnancy:' + preg);
    }
  • Map DICOM Referring Physician Name to HL7 v2 OBR-16 (0008,0090) Referring Physician's Name

    PN format on both sides. ReferringPhysicianName mainly populates ORM/ORU OBR-16 (Ordering Provider) for radiology results.

    // (0008,0090) Referring Physician's Name — XCN target on the HL7 v2 side
    var pn = String(msg.dataset.attr.(@tag == '00080090').value).split('^');
    tmp['OBR'][16][1] = ''; // ID Number — usually empty unless there's a separate identifier
    tmp['OBR'][16][2] = pn[0] || ''; // family
    tmp['OBR'][16][3] = pn[1] || ''; // given
    tmp['OBR'][16][4] = pn[2] || ''; // middle
  • Echo IHE RAD RequestedProcedureID through to Performed Procedure Step (0040,1001) Requested Procedure ID

    IHE Scheduled Workflow expects the RequestedProcedureID (0040,1001) from the order to be echoed in the MPPS (0040,0253) when the procedure is performed.

    // (0040,1001) Requested Procedure ID — echo through to outbound MPPS
    var reqProcId = String(msg.dataset.attr.(@tag == '00401001').value).trim();
    if (reqProcId) {
      channelMap.put('mpps_requested_procedure_id', reqProcId);
      // Also stash for OBR-19 mapping in HL7 v2 ORU
      tmp['OBR'][19] = reqProcId;
    }
  • Map HL7 v2 Order Priority (TQ1-9) to DICOM RequestedProcedurePriority (0040,1003) Requested Procedure Priority

    HL7 v2.5+ uses TQ1-9; older messages use OBR-27.6. DICOM uses STAT/HIGH/ROUTINE/MEDIUM/LOW.

    // (0040,1003) Requested Procedure Priority — map HL7 v2 priority codes
    var tq19 = String(msg['TQ1'] && msg['TQ1'][9] && msg['TQ1'][9][1] || '').trim().toUpperCase();
    var obr276 = String(msg['OBR'] && msg['OBR'][27] && msg['OBR'][27][6] || '').trim().toUpperCase();
    var hl7Prio = tq19 || obr276;
    var map = { 'S': 'STAT', 'A': 'HIGH', 'P': 'HIGH', 'R': 'ROUTINE', 'T': 'MEDIUM', 'C': 'LOW' };
    var dicomPrio = map[hl7Prio] || 'ROUTINE';
    channelMap.put('dicom_request_priority', dicomPrio);
  • Skip thumbnail-only DICOM with degenerate Rows (0028,0010) Rows

    Some PACS / VNA vendors push tiny preview images alongside full-resolution data. Filter these from full ingest pipeline.

    // (0028,0010) Rows — drop suspiciously small images on the perimeter channel
    var rows = parseInt(String(msg.dataset.attr.(@tag == '00280010').value), 10);
    if (rows < 64) {
      logger.info('Dropping low-resolution image (Rows=' + rows + ')');
      destinationSet.removeAll();
      return;
    }
  • Bridge HL7 v2 ORC-3 (Filler Order Number) to DICOM Scheduled Procedure Step ID (0040,0009) Scheduled Procedure Step ID

    Common in HL7 v2 ORM → DICOM MWL (Modality Worklist) bridges where the modality requests work scheduled by the order entry system.

    // (0040,0009) Scheduled Procedure Step ID — bridge from HL7 v2 ORC-3 Filler Order Number
    var fillerOrderNum = String(msg['ORC'] && msg['ORC'][3] && msg['ORC'][3][1] || '').trim();
    if (fillerOrderNum) {
      channelMap.put('dicom_sps_id', fillerOrderNum);
    } else {
      // Fallback to OBR-3 (Filler Order Number, redundant in older messages)
      var obr3 = String(msg['OBR'] && msg['OBR'][3] && msg['OBR'][3][1] || '').trim();
      if (obr3) channelMap.put('dicom_sps_id', obr3);
    }
  • Series Instance UID validation (0020,000E) Series Instance UID

    DICOM UIDs must be ≤64 chars and contain only [0-9.]. Some bad implementations emit UIDs with spaces or trailing nulls — validate before propagating.

    // (0020,000E) Series Instance UID — strict validation
    var uid = String(msg.dataset.attr.(@tag == '0020000E').value).replace(/[\u0000-\u001F]/g, '').trim();
    if (!/^[0-9.]{1,64}$/.test(uid) || uid.length > 64) {
      logger.error('Invalid Series Instance UID: "' + uid + '" — rejecting');
      destinationSet.removeAll();
      return;
    }
    channelMap.put('series_uid', uid);
  • Series Number collision detection across multi-study imports (0020,0011) Series Number

    Series Number is NOT globally unique — only unique within a Study. When merging series from multiple studies under a new Study UID, collisions break Q/R by series.

    // (0020,0011) Series Number — IS, expected unique within a study
    var sn = parseInt(String(msg.dataset.attr.(@tag == '00200011').value).trim(), 10);
    if (isNaN(sn)) { channelMap.put('invalid_series_number', 'true'); return; }
    // Track seen numbers during multi-study merge
    var seenKey = 'seen_series_' + channelMap.get('target_study_uid');
    var seen = JSON.parse(globalChannelMap.get(seenKey) || '[]');
    if (seen.indexOf(sn) >= 0) {
      // Collision: remap by shifting into a high range (10000+) to avoid displacing originals
      var shifted = 10000 + seen.length;
      tmp.dataset.attr.(@tag == '00200011').value = String(shifted);
      channelMap.put('series_remapped', sn + ' -> ' + shifted);
      sn = shifted;
    }
    seen.push(sn);
    globalChannelMap.put(seenKey, JSON.stringify(seen));
  • Filter DICOM by SOP Class UID (whitelist storage SOP classes) (0008,0016) SOP Class UID

    Mirth channels often need to drop non-image SOP Classes (Encapsulated PDFs, Structured Reports) at the perimeter and forward them to dedicated channels.

    // (0008,0016) SOP Class UID — whitelist filter
    var sopClass = String(msg.dataset.attr.(@tag == '00080016').value).trim();
    var imageStorageClasses = [
      '1.2.840.10008.5.1.4.1.1.2',     // CT Image
      '1.2.840.10008.5.1.4.1.1.4',     // MR Image
      '1.2.840.10008.5.1.4.1.1.6.1',   // Ultrasound Image
      '1.2.840.10008.5.1.4.1.1.7',     // Secondary Capture
      '1.2.840.10008.5.1.4.1.1.20'     // Nuclear Medicine
    ];
    if (imageStorageClasses.indexOf(sopClass) === -1) {
      router.routeMessageByChannelName('NonImage_DICOM', message.getRawData());
      destinationSet.removeAll();
      return;
    }
  • Build WADO-RS retrieval URL from SOP Instance UID (0008,0018) SOP Instance UID

    Common bridge pattern when forwarding DICOM references to a FHIR ImagingStudy.endpoint or to a viewer.

    // (0008,0018) SOP Instance UID — use to construct WADO-RS instance retrieval URL
    var wadoBase = configurationMap.get('WADO_RS_BASE'); // e.g. https://dicomweb.example.org/wado-rs
    var studyUid  = String(msg.dataset.attr.(@tag == '0020000D').value);
    var seriesUid = String(msg.dataset.attr.(@tag == '0020000E').value);
    var instanceUid = String(msg.dataset.attr.(@tag == '00080018').value);
    var wadoUrl = wadoBase + '/studies/' + studyUid + '/series/' + seriesUid + '/instances/' + instanceUid;
    channelMap.put('wado_url', wadoUrl);
  • Specific Character Set normalization for non-ASCII Patient Names (0008,0005) Specific Character Set

    Modalities in non-English-speaking countries emit DICOM with ISO-IR 100 (Latin-1), ISO 2022 IR 13 (Japanese Romaji), or ISO IR 192 (UTF-8). Mirth must declare/convert.

    // (0008,0005) Specific Character Set — ensure UTF-8 in outbound JSON
    var charset = String(msg.dataset.attr.(@tag == '00080005').value).trim();
    if (charset && charset !== 'ISO_IR 192' && charset !== 'ISO_IR 100') {
      channelMap.put('charset_warning', 'Source uses ' + charset + ' — verify display rendering downstream');
    }
    // Force UTF-8 in outbound — Mirth's String coercion handles the conversion
    var pn = new java.lang.String(msg.dataset.attr.(@tag == '00100010').value.toString().getBytes('UTF-8'), 'UTF-8');
    channelMap.put('patient_name_utf8', String(pn));
  • Convert DICOM Study Date + Time to ISO 8601 (0008,0020) Study Date

    Useful for FHIR ImagingStudy.started or external systems expecting ISO timestamps.

    // (0008,0020) Study Date YYYYMMDD + (0008,0030) Study Time HHMMSS.FFFFFF
    var d = String(msg.dataset.attr.(@tag == '00080020').value).trim();
    var t = String(msg.dataset.attr.(@tag == '00080030').value).trim();
    if (d.length === 8) {
      var iso = d.substr(0,4) + '-' + d.substr(4,2) + '-' + d.substr(6,2);
      if (t.length >= 6) iso += 'T' + t.substr(0,2) + ':' + t.substr(2,2) + ':' + t.substr(4,2);
      channelMap.put('study_started', iso); // YYYY-MM-DDTHH:MM:SS
    }
  • Sanitize Study Description for downstream display (0008,1030) Study Description

    Vendor scanners sometimes emit ALL-CAPS, weird punctuation, or extra whitespace. Normalize on ingest.

    // (0008,1030) Study Description — clean for downstream display
    var desc = String(msg.dataset.attr.(@tag == '00081030').value).trim();
    // Title-case, collapse whitespace, strip CR/LF (occasionally embedded)
    desc = desc.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
    desc = desc.toLowerCase().replace(/\b\w/g, function(c) { return c.toUpperCase(); });
    channelMap.put('study_description', desc);
  • Generate Study ID when missing (legacy modality workaround) (0020,0010) Study ID

    DICOM Study ID (0020,0010) is SH 16 chars and not always populated by older modalities. Generate from Accession Number or Study UID hash for downstream consistency.

    // (0020,0010) Study ID — derive from Accession Number when source omitted it
    var studyId = String(msg.dataset.attr.(@tag == '00200010').value).trim();
    if (!studyId) {
      var accNum = channelMap.get('accession');
      if (accNum) {
        studyId = accNum.substring(0, 16); // SH 16 char cap
      } else {
        var studyUid = String(msg.dataset.attr.(@tag == '0020000D').value);
        studyId = String(studyUid.substring(studyUid.length - 16)); // last 16 chars
      }
      channelMap.put('study_id_generated', 'true');
    }
  • Use Study Instance UID for de-duplication (0020,000D) Study Instance UID

    Idempotent ingestion pattern. Hashed UIDs land in the global channel map as a TTL-style cache.

    // (0020,000D) Study Instance UID — globally unique per study
    var studyUID = String(msg.dataset.attr.(@tag == '0020000D').value).trim();
    var seen = $gc('seen_studies') || new java.util.HashMap();
    var now = Date.now();
    // Expire entries older than 7 days
    var iter = seen.entrySet().iterator();
    while (iter.hasNext()) { if (now - iter.next().getValue() > 7 * 86400 * 1000) iter.remove(); }
    if (seen.containsKey(studyUID)) {
      logger.info('Duplicate study, skipping: ' + studyUID);
      destinationSet.removeAll();
      return;
    }
    seen.put(studyUID, now);
    $gc('seen_studies', seen);
  • Combine StudyDate + StudyTime into a single ISO 8601 timestamp (0008,0030) Study Time

    DICOM splits date and time into DA + TM; downstream systems (FHIR ImagingStudy.started, HL7 OBR-7) want a combined DateTime.

    // (0008,0020) StudyDate (DA: YYYYMMDD) + (0008,0030) StudyTime (TM: HHMMSS.FFFFFF)
    var date = String(msg.dataset.attr.(@tag == '00080020').value).trim();
    var time = String(msg.dataset.attr.(@tag == '00080030').value).trim();
    if (date.length !== 8) { channelMap.put('invalid_study_date', date); return; }
    if (!time) time = '000000';
    // Strip fractional seconds for HL7 compatibility (keep ISO 8601 for FHIR)
    var ts = date + time.replace('.', '').substring(0, 6);
    // HL7 v2 TS format: YYYYMMDDHHMMSS
    tmp['OBR'][7][1] = ts;
    // FHIR-style ISO 8601 for channelMap consumers
    var iso = date.substring(0,4) + '-' + date.substring(4,6) + '-' + date.substring(6,8) + 'T' + time.substring(0,2) + ':' + time.substring(2,4) + ':' + time.substring(4,6);
    channelMap.put('study_iso_dt', iso);
  • WindowCenter / WindowWidth — multi-value pair for radiology presets (0028,1050) Window Center

    WindowCenter and WindowWidth are paired; both can carry multiple values (e.g., '40\400\-50\1000' for brain + bone windows). Extract the first pair as the default preset.

    // (0028,1050) Window Center + (0028,1051) Window Width — DS, backslash-separated pairs
    var wc = String(msg.dataset.attr.(@tag == '00281050').value).split('\\');
    var ww = String(msg.dataset.attr.(@tag == '00281051').value).split('\\');
    if (wc.length === 0 || ww.length === 0) return; // skip if unset
    var n = Math.min(wc.length, ww.length);
    var presets = [];
    for (var i = 0; i < n; i++) {
      var c = parseFloat(wc[i]);
      var w = parseFloat(ww[i]);
      if (!isNaN(c) && !isNaN(w)) presets.push({ center: c, width: w });
    }
    if (presets.length === 0) return;
    channelMap.put('default_window_center', presets[0].center);
    channelMap.put('default_window_width', presets[0].width);
    if (presets.length > 1) channelMap.put('window_preset_count', presets.length);

Mirth Connect Engagements

Saga IT is one of the few firms that has run production Mirth Connect channels at scale across DICOM, HL7 v2, and FHIR. If you're standing up a new integration engine or hardening an existing one, talk to our Mirth team. We also maintain MirthSync (open-source, MIT) for Mirth channel version-control and CI/CD.