EASI JS 1.0.1 is here

MEET EASI JS · THE DATA PIPELINE LIBRARY

Your DICOM workflow.
One fluent pipeline.

Read, select, transform, write and exchange imaging data with an expressive JavaScript API. Process incoming data through a streaming parser, then produce the result your application needs.

  • Browser + Node.js
  • Native ES modules
  • No required runtime dependencies

Public source. Free community licensing for eligible users. Explore community and commercial use.

ONE PIPELINE. YOUR APPLICATION.
read-from-dicomweb.mjsJavaScript
import EASI from '@xinonix/easi-js';

const url = 'https://pacs.example/dicom-web/studies/2.25.1';

const result = await EASI.pipelineBuilder()
  .fromDicomweb()
  .ofDicomData()
  .toInstances()
  .build()
  .process({
    source: url,
    sourceOptions: {
      mode: 'wado-instance',
      accept: 'multipart/related; type="application/dicom"'
    }
  });

console.log(result.toArray());
01SourceDICOMweb URL
02ParseDICOM
03OutputInstances

Browser + Node.js Read a study from a DICOMweb URL and work with its instance objects.

Replace this example with your complete WADO-RS study URL. Configure archive authentication and allow your browser origin through the archive’s CORS settings. Explore pipeline sources

ONE COMMAND TO BEGINCore DICOM capabilities. In your application.
npm install @xinonix/easi-js

COMPOSE THE WORK. CHOOSE THE RESULT.

Read. Shape. Deliver.
Keep the workflow clear.

Keep the source, input format and output strategy separate. Reuse the same building blocks as your application moves from a file to a network response or an imaging archive.

01

Read while bytes arrive.

Parse native DICOM, DICOM JSON or Native DICOM Model XML. Feed a file or HTTP response into a pipeline without first reading the whole source into an application buffer.

Stream input · incremental parsingUnderstand the pipeline
02

Produce what you need.

Build a DICOM object model, select a few attributes, or apply a custom mapping. Create FHIR R4 ImagingStudy resources with the patient references and context your application supplies.

Instances · selections · custom mappings · FHIRExplore mapping and FHIR
03

Write the next step.

Serialize DICOM data into bytes, send output to a destination, or consume writer chunks with backpressure. Compose reading and writing into your application’s workflow.

DICOM output · destinations · chunk callbacksRead the writing guide
04

Bring DICOMweb closer.

Process DICOM and metadata returned by HTTP and DICOMweb endpoints through the same source, parser and output model. Your application controls requests, credentials and archive access.

HTTP streams · single-part + multipart responsesConnect an HTTP source
05

Talk to DICOM peers.

Verify peers and send or receive stored instances with Node.js transports. Use supported Study Root query/retrieve clients for C-FIND, C-GET or a configured C-MOVE route.

Node.js · C-ECHO · C-STORE · C-FIND/GET/MOVEExplore the DIMSE profile
06

Get from data to pixels.

Read image attributes and decode supported pixel formats to build previews, image processing or your own viewer. Add optional codec backends where your transfer syntaxes need them.

Pixel access · frame decoding · optional codecsTry the browser image demo
Streaming is a design choice. So is retention.

The reader parses incoming chunks; the output you choose determines what stays in memory. DIMSE transports retain complete datasets before pipeline processing. Plan pixel retention, output collection and retrieval limits for your workload.

Understand memory use

BUILT FOR WHAT YOU’RE BUILDING

AI datasets.
Imaging services.
Browser apps.

Parse DICOM as bytes arrive. Compose the result your application needs, then connect it to the work you’re building.

AI dataset preparation
Node.js

Find the images your dataset needs.

Build a manifest of study identifiers, modality, and dimensions before choosing which pixels to decode.

  1. DICOM file
  2. Incremental parsing
  3. Dataset manifest

Keep selected metadata for dataset indexing.

View a working example
dataset-manifest.mjs
import EASI, { Tag } from '@xinonix/easi-js';

async function buildDatasetManifest(path) {
  const selection = EASI.selectionBuilder()
    .include([Tag.StudyInstanceUID, Tag.SeriesInstanceUID, Tag.SOPInstanceUID,
              Tag.Modality, Tag.Rows, Tag.Columns])
    .build();
  const result = await EASI.pipelineBuilder()
    .fromFileStream()
    .ofDicomData()
    .withBulkDataPolicy({ mode: 'auto' })
    .toSelection(selection)
    .build()
    .process({ source: path });

  return result.toArray().map(attributes => ({
    studyUid: attributes.value(Tag.StudyInstanceUID),
    seriesUid: attributes.value(Tag.SeriesInstanceUID),
    instanceUid: attributes.value(Tag.SOPInstanceUID),
    modality: attributes.value(Tag.Modality),
    rows: attributes.value(Tag.Rows),
    columns: attributes.value(Tag.Columns)
  }));
}

// Runnable synthetic example. Replace path with a file from your dataset.
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

// Generate a tiny raw DICOM dataset for learning, not a clinical image.
const metadata = {
  '00080016': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.7'] },
  '00080018': { vr: 'UI', Value: ['2.25.202610120001.1.1'] },
  '00080060': { vr: 'CS', Value: ['OT'] },
  '00100020': { vr: 'LO', Value: ['SYNTHETIC-EXAMPLE'] },
  '0020000D': { vr: 'UI', Value: ['2.25.202610120001'] },
  '0020000E': { vr: 'UI', Value: ['2.25.202610120001.1'] },
  '00280002': { vr: 'US', Value: [1] },
  '00280004': { vr: 'CS', Value: ['MONOCHROME2'] },
  '00280010': { vr: 'US', Value: [2] },
  '00280011': { vr: 'US', Value: [2] },
  '00280100': { vr: 'US', Value: [8] },
  '00280101': { vr: 'US', Value: [8] },
  '00280102': { vr: 'US', Value: [7] },
  '00280103': { vr: 'US', Value: [0] },
  '7FE00010': { vr: 'OB', InlineBinary: 'ACCA/w==' }
};
const generated = await EASI.pipelineBuilder()
  .fromByteStream()
  .ofDicomMetadata()
  .toDicomData()
  .build()
  .process({ source: new TextEncoder().encode(JSON.stringify(metadata)) });
const dicomBytes = generated.first();

const directory = await mkdtemp(join(tmpdir(), 'easi-manifest-'));
try {
  const path = join(directory, 'synthetic.dcm');
  await writeFile(path, dicomBytes);
  console.log(JSON.stringify(await buildDatasetManifest(path), null, 2));
} finally {
  await rm(directory, { recursive: true, force: true });
}

Generated sample data. Complete setup included.

Select dataset metadata
Server applications
Node.js

Process imaging uploads as they arrive.

Parse a DICOM upload stream and write serialized chunks to your storage sink through one pipeline.

  1. Upload stream
  2. Incremental parsing
  3. Storage sink

Write chunks without collecting the entire output.

View a working example
stream-upload.mjs
import EASI from '@xinonix/easi-js';

async function storeUpload(uploadStream, saveChunk) {
  const result = await EASI.pipelineBuilder()
    .fromNodeStreamAdapter()
    .ofDicomData()
    .withBulkDataPolicy({ mode: 'auto' })
    .toDicomData({
      collectOutput: false,
      onChunk: saveChunk
    })
    .build()
    .process({ source: uploadStream });
  return result.first();
}

// Runnable synthetic example. A real server supplies its request and storage sink.
import { createReadStream } from 'node:fs';
import { mkdtemp, open, rm, stat } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Readable } from 'node:stream';

// Generate a tiny raw DICOM dataset for learning, not a clinical image.
const metadata = {
  '00080016': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.7'] },
  '00080018': { vr: 'UI', Value: ['2.25.202610120001.1.1'] },
  '00080060': { vr: 'CS', Value: ['OT'] },
  '00100020': { vr: 'LO', Value: ['SYNTHETIC-EXAMPLE'] },
  '0020000D': { vr: 'UI', Value: ['2.25.202610120001'] },
  '0020000E': { vr: 'UI', Value: ['2.25.202610120001.1'] },
  '00280002': { vr: 'US', Value: [1] },
  '00280004': { vr: 'CS', Value: ['MONOCHROME2'] },
  '00280010': { vr: 'US', Value: [2] },
  '00280011': { vr: 'US', Value: [2] },
  '00280100': { vr: 'US', Value: [8] },
  '00280101': { vr: 'US', Value: [8] },
  '00280102': { vr: 'US', Value: [7] },
  '00280103': { vr: 'US', Value: [0] },
  '7FE00010': { vr: 'OB', InlineBinary: 'ACCA/w==' }
};
const generated = await EASI.pipelineBuilder()
  .fromByteStream()
  .ofDicomMetadata()
  .toDicomData()
  .build()
  .process({ source: new TextEncoder().encode(JSON.stringify(metadata)) });
const dicomBytes = generated.first();

// Simulate an upload whose chunks cross DICOM attribute boundaries.
function* incomingChunks() {
  for (let offset = 0; offset < dicomBytes.length; offset += 17) {
    yield dicomBytes.subarray(offset, offset + 17);
  }
}
const uploadStream = Readable.from(incomingChunks());
const directory = await mkdtemp(join(tmpdir(), 'easi-upload-'));
try {
  const destination = join(directory, 'stored.dcm');
  const storage = await open(destination, 'w');
  let position = 0;
  let writeChunks = 0;
  const saveChunk = async chunk => {
    let offset = 0;
    while (offset < chunk.byteLength) {
      const { bytesWritten } = await storage.write(
        chunk, offset, chunk.byteLength - offset, position);
      if (bytesWritten === 0) throw new Error('Storage write made no progress.');
      offset += bytesWritten;
      position += bytesWritten;
    }
    writeChunks++;
  };
  let receipt;
  try {
    receipt = await storeUpload(uploadStream, saveChunk);
  } finally {
    await storage.close();
  }

  // Check this generated fixture without loading the stored file into memory.
  const storedHash = createHash('sha256');
  for await (const chunk of createReadStream(destination)) storedHash.update(chunk);
  console.log(JSON.stringify({
    bytesWritten: receipt.bytesWritten,
    storedBytes: (await stat(destination)).size,
    writeChunks,
    storedSha256: storedHash.digest('hex'),
    sourceSha256: createHash('sha256').update(dicomBytes).digest('hex')
  }, null, 2));
} finally {
  await rm(directory, { recursive: true, force: true });
}

Generated sample data. Complete setup included.

Plan streaming I/O
Browser applications
Bundled browser module

Inspect a DICOM file in your app.

Read a user-selected File or Blob and return the metadata your interface needs, locally in the browser.

  1. Browser File
  2. Incremental parsing
  3. UI metadata

A file in. The fields your UI needs out.

View a working example
inspect-file.js
import EASI, { Tag } from '@xinonix/easi-js';

async function inspectDicomFile(file) {
  const selection = EASI.selectionBuilder()
    .include([Tag.Modality, Tag.Rows, Tag.Columns])
    .build();
  const result = await EASI.pipelineBuilder()
    .fromFileStream()
    .ofDicomData()
    .withBulkDataPolicy({ mode: 'auto' })
    .toSelection(selection)
    .build()
    .process({ source: file });

  const attributes = result.first();
  return {
    modality: attributes.value(Tag.Modality),
    rows: attributes.value(Tag.Rows),
    columns: attributes.value(Tag.Columns)
  };
}

// Runnable synthetic example. In your app, use the File from a file input.
// Generate a tiny raw DICOM dataset for learning, not a clinical image.
const metadata = {
  '00080016': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.7'] },
  '00080018': { vr: 'UI', Value: ['2.25.202610120001.1.1'] },
  '00080060': { vr: 'CS', Value: ['OT'] },
  '00100020': { vr: 'LO', Value: ['SYNTHETIC-EXAMPLE'] },
  '0020000D': { vr: 'UI', Value: ['2.25.202610120001'] },
  '0020000E': { vr: 'UI', Value: ['2.25.202610120001.1'] },
  '00280002': { vr: 'US', Value: [1] },
  '00280004': { vr: 'CS', Value: ['MONOCHROME2'] },
  '00280010': { vr: 'US', Value: [2] },
  '00280011': { vr: 'US', Value: [2] },
  '00280100': { vr: 'US', Value: [8] },
  '00280101': { vr: 'US', Value: [8] },
  '00280102': { vr: 'US', Value: [7] },
  '00280103': { vr: 'US', Value: [0] },
  '7FE00010': { vr: 'OB', InlineBinary: 'ACCA/w==' }
};
const generated = await EASI.pipelineBuilder()
  .fromByteStream()
  .ofDicomMetadata()
  .toDicomData()
  .build()
  .process({ source: new TextEncoder().encode(JSON.stringify(metadata)) });
const dicomBytes = generated.first();

const file = new File([dicomBytes], 'synthetic.dcm', { type: 'application/dicom' });
console.log(JSON.stringify(await inspectDicomFile(file), null, 2));

Generated sample data. Complete setup included.

Build a browser pipeline

STREAMING YOU CAN SEE

Start processing while the study
is still arriving.

Watch each instance reach your application as the rest of the stream continues.

INCOMING DICOM STREAM3 generated instances
0%
Instance 1QueuedInstance 2QueuedInstance 3Queued
ONE EXPRESSIVE PIPELINENative DICOM
EASI.pipelineBuilder()
  .fromPartStream()
  .ofDicomData()
  .withBulkDataPolicy({ mode: 'auto' })
  .withOnEmit(showLatestInstance)
  .toInstances()
  .build();

This replay supplies a generated byte stream. DICOMweb, uploads, and files can feed the same parser.

YOUR APPLICATION, ALREADY WORKING0 / 3
  1. Instance 1Waiting for DICOM data
    01
  2. Instance 2Waiting for DICOM data
    02
  3. Instance 3Waiting for DICOM data
    03

Waiting for the first instance

Synthetic DICOM stream · Real EASI JS parsing · Runs in your browser. Delivery is paced to make the stream visible.

FROM PIPELINE TO PIXELS

A DICOM image.
Just a browser.

Build the foundation of a zero-footprint viewer. EASI JS reads the file, parses DICOM, and decodes its pixels. Your application puts them on screen.

TRY AN IMAGE
Your file stays in your browser.
Choose a sample or your own file IN YOUR BROWSER

A real DICOM image, rendered here.

Choose a sample or open a local DICOM file.

Choose an image to see the pipeline turn DICOM into pixels.

  1. 01 Read a file stream
  2. 02 Parse DICOM
  3. 03 Decode a frame
  4. 04 Draw on canvas
See the pipeline behind the image
A browser File + a canvas
import EASI from '@xinonix/easi-js';
import { Image } from '@xinonix/easi-js/dicom';

// A native little-endian monochrome DICOM File; rendering retains pixel bytes.
async function viewDicom(file, canvas) {
    const result = await EASI.pipelineBuilder()
        .fromFileStream()
        .ofDicomData()
        .withBulkDataPolicy({ mode: 'materialize', hardSafetyCap: 64 * 1024 * 1024 })
        .toInstances()
        .build()
        .process({ source: file, sourceOptions: { contentType: 'application/dicom' } });

    const image = new Image(result.first().dataSet);
    const { rows, columns } = image.imagePixelModule;
    const pixels = new Uint8Array(rows * columns * 4);
    const frameIndex = 0; // Zero-based: choose any supported frame.
    if (!image.decodeFrame(pixels, null, frameIndex)) throw new Error('Unsupported image pixels.');
    canvas.width = columns;
    canvas.height = rows;
    canvas.getContext('2d').putImageData(
        new ImageData(new Uint8ClampedArray(pixels), columns, rows), 0, 0
    );
}

The viewer retains pixel data for decoding. The stream replay above shows the alternative when only metadata is needed.

Small viewer demonstration, built on EASI JS 1.0.1. Supports common native little-endian and baseline JPEG images; other encodings show a clear message. This is a foundation for an application viewer, not a diagnostic tool.

MR, CT, and X-ray samples are derived from the provided anonymized collection with identifying metadata omitted; the X-ray is reduced for faster loading. US is a generated ultrasound-style cine demonstration. Local files are never uploaded. Sample details

A SMALL EXAMPLE. A REAL PIPELINE.

Same bytes.
Different possibilities.

Choose a synthetic sample and an output. EASI JS runs here in your browser, so you can see the transformation for yourself.

02   Output product

Tiny generated datasets with fictional identifiers. No patient files are loaded or uploaded.

Inspect the generated input metadata
Preparing the synthetic sample…
YOUR PIPELINE
Loading the example…
THE RESULT
Choose an output and run the pipeline.

This demonstrates metadata processing using small synthetic DICOM datasets. It is not a clinical viewer or a diagnostic tool.

BEYOND THE FIRST FILE

Imaging doesn’t live in isolation.
Your workflows shouldn’t either.

Built to be composed.

EASI JS is a library for imaging workflows. Your application supplies its UI, identity, storage, and deployment. Some pixel formats need optional codec backends.

Understand the boundaries

FROM FIRST RUN TO YOUR APPLICATION

A clear way in.
Room to go deeper.

Start with a complete example, then use the guides and public source to adapt the pipeline to your own project.

THE FOUNDATION. YOUR NEXT STEP.

Need data? Use EASI JS.
Need more? Build on it.

Use the library on its own, or choose an independent EASI product that puts the pipeline to work. Each has its own package, documentation and release cycle.

PUBLIC SOURCE. ROOM TO GROW.

Build something useful.
Start with EASI JS.

Source-available under the EASI JS Community License. Free use is available to eligible individuals, qualifying research institutions and nonprofits, and organization groups with annual gross revenue strictly below US$5 million.

Larger organization groups need written commercial terms unless a stated exception applies. Proprietary applications and distribution follow the full license.

Source-available, not an OSI-approved open-source license. Read the full EASI JS license.