The PACS viewer simulates a Picture Archiving and Communication System on a separate monitor, displaying mammogram images for the current participant during image reading.
In the real breast screening service, when a radiologist opens a participant for reading, their mammograms automatically appear in a PACS viewer on a separate monitor. As they advance to the next case, PACS updates to show the next set of mammograms.
This prototype simulates that behaviour using:
- BroadcastChannel API for communication between reading pages and viewer window
- Seeded random image selection so the same participant always shows the same images
- Context-aware selection matching image sets to participant context (symptoms, implants, imperfect images, repeats)
- Weighted distribution to control the mix of normal/abnormal/indeterminate/technical cases
- Composite sets allowing reuse of views from other sets and individual images from the image library
- Manual: Click "Open PACS viewer" link in header navigation (visible during reading workflow)
- Auto-open: If
autoOpenPacsViewersetting is enabled, viewer opens automatically when entering reading workflow - From an issue: "Open study in PACS" on an issue page opens the viewer for that issue's images (see issues.md)
Pages give the viewer their study with _includes/images/pacs-viewer-meta.njk, included in the head after setting viewerAppointment, viewerParticipant and viewerAutoOpen. The reading layout and the issue page use it.
- Reading workflow pages include meta tags with participant and image data
mammogram-channel.jsbroadcasts participant data on page load- Viewer listens on BroadcastChannel (
mammogram-viewer) and updates display - Same appointmentId messages are ignored (prevents flash when navigating within same case)
- When leaving reading workflow,
clearmessage shows placeholder in viewer
Before auto-opening, the parent page checks if a viewer is already open:
- Parent sends "ping" message via BroadcastChannel
- If viewer responds with "pong" within 100ms → already open, don't auto-open
- If no response → open new viewer window
This prevents focus stealing and duplicate windows.
When the viewer opens (manually or auto), it requests current participant data:
- Viewer sends "request-current" message
- Any parent page with participant data responds with current info
- Viewer immediately shows correct participant (instead of "Waiting for participant")
- Zoom: Click any image in the main grid to view fullscreen; click or press Escape to close
- Keyboard navigation: Arrow keys cycle through zoomed images (ordered by breast: right then left). When not zoomed, left arrow opens first right breast image, right arrow opens first left breast image.
- Opinion shortcuts: Press N (normal), T (technical recall), or R (recall for assessment) to trigger the corresponding opinion on the reading page - works even when PACS viewer is focused
- Additional images: When a case has repeat/retake images, "View additional images" button appears; modal shows all attempts with labels
- Debug mode: Press 'd' to toggle debug bar showing set ID, tag, description, and context flags
- Diagrams:
app/assets/images/mammogram-diagrams/- Complete sets:
set-XX/folders with 4 views - Image library:
image-library/folder with individual images
- Complete sets:
- Real images (future):
app/assets/images/mammogram-sets/
Each complete set contains four views: rcc.png, lcc.png, rmlo.png, lmlo.png
manifest.json in each source folder defines both the image library and available sets:
{
"imageLibrary": [
{
"id": "blur-2-rmlo",
"path": "image-library/blur-2-rmlo.png",
"view": "rmlo",
"status": "technical",
"issue": "blur"
}
],
"sets": [
{
"id": "set-17",
"tag": "normal",
"description": "Normal mammogram"
},
{
"id": "repeat-normal-blur-rmlo",
"tag": "normal",
"hasRepeat": true,
"description": "Normal mammogram with RMLO retake (first was blurred)",
"views": {
"rmlo": [
{ "image": "blur-2-rmlo" },
{ "from": "set-17" }
],
"lmlo": { "from": "set-17" },
"rcc": { "from": "set-17" },
"lcc": { "from": "set-17" }
}
}
]
}Tags: normal, abnormal, indeterminate, technical
Image sets are selected based on participant/appointment context:
| Context Flag | Behaviour |
|---|---|
hasSymptoms |
Increases probability of abnormal sets; prefers abnormality on symptom side |
hasImplants |
Only selects sets with hasImplants: true flag |
isImperfect |
Increases probability of technical sets |
hasRepeat |
Only selects sets with hasRepeat: true flag |
Context is extracted from appointment data by extractAppointmentContext() in mammogram-images.js.
When no context requires hard filtering, sets are selected using weighted random based on tag:
- Default (from
app/config.js): 70% normal, 15% abnormal, 10% indeterminate, 5% technical - With symptoms: 30% normal, 50% abnormal, 10% indeterminate, 10% technical
- With imperfect images: 10% normal, 10% abnormal, 0% indeterminate, 80% technical
- Configured in
config.reading.mammogramTagWeights - Selection is deterministic per appointment ID (seeded random)
If a mammogram view is missing from appointment.mammogramData.views, the viewer shows "No image" placeholder for that panel. The image selection system filters paths to only include views present in the appointment's mammogram data.
Sets can reference other sets or the image library to create composites:
View definition options:
{ "from": "set-17" }- Use view from another set{ "image": "blur-2-rmlo" }- Use image from library{ "path": "custom/path.png" }- Use explicit path- Omitted - Defaults to
{set-id}/{view}.png
Array format for repeats:
{
"rmlo": [
{ "image": "blur-2-rmlo" },
{ "from": "set-17" }
]
}First element is the initial (failed) attempt, last element is the final (used) image.
When sets have arrays for views (repeat scenarios), the viewer:
- Shows the latest (last) image in the main grid
- Displays "View additional images" button
- Modal shows all attempts labelled "Attempt 1", "Attempt 2", "Latest"
- Click any image in the modal to zoom
| Setting | Default | Description |
|---|---|---|
debugMode |
'false' |
Show debug info (tags like Has repeat, Imperfect, etc.) |
mammogramViewOrder |
'cc-first' |
View order: 'cc-first' (CC on top) or 'mlo-first' (MLO on top) |
| Setting | Default | Description |
|---|---|---|
autoOpenPacsViewer |
'false' |
Auto-open viewer when entering reading workflow |
| Setting | Default | Description |
|---|---|---|
mammogramImageSource |
'diagrams' |
Image source: 'diagrams' or 'real' |
mammogramTagWeights |
{ normal: 0.7, abnormal: 0.15, indeterminate: 0.1, technical: 0.05 } |
Distribution weights for image set tags |
| File | Purpose |
|---|---|
app/lib/utils/mammogram-images.js |
Image selection utility (weighted seeded random, path generation) |
app/assets/javascript/mammogram-channel.js |
BroadcastChannel communication (broadcast, listen, ping/pong, auto-open) |
app/views/reading/mammogram-viewer.html |
Viewer page (dark theme, 2×2 grid, placeholder states) |
| File | Purpose |
|---|---|
app/views/_templates/layout-reading.html |
Meta tags for viewer data, missing view detection |
app/views/_templates/layout-base.html |
"Open PACS viewer" link in header nav |
app/views/_includes/scripts.html |
mammogram-channel.js include |
| File | Purpose |
|---|---|
app/assets/images/mammogram-diagrams/manifest.json |
Image set metadata (90 sets) |
app/data/session-data-defaults.js |
autoOpenPacsViewer setting default |
app/config.js |
mammogramImageSource and mammogramTagWeights |
| File | Purpose |
|---|---|
scripts/process-mammogram-sets.sh |
Process raw mammogram set screenshots (rename, grayscale, compress) |
scripts/process-mammogram-images.sh |
Process individual mammogram images (grayscale, compress, no rename) |
The scripts/process-mammogram-sets.sh script processes raw mammogram screenshots:
- Renames screenshots to view names (
rmlo.png,lmlo.png,rcc.png,lcc.png) based on chronological order - Converts to grayscale
- Compresses with pngquant (8 colours)
- Optionally resizes
- ImageMagick (
magick) - pngquant
# Preview changes (dry run)
./scripts/process-mammogram-sets.sh reference/test-sets --dry-run
# Process to output directory
./scripts/process-mammogram-sets.sh reference/test-sets --output processed-sets
# Process with resize (825px width)
./scripts/process-mammogram-sets.sh reference/test-sets --output processed-sets --resize 825input-dir/
set-02/
Screenshot 2026-01-23 at 10.15.01.png → rmlo.png
Screenshot 2026-01-23 at 10.15.02.png → lmlo.png
Screenshot 2026-01-23 at 10.15.03.png → rcc.png
Screenshot 2026-01-23 at 10.15.04.png → lcc.png
set-03/
...
The scripts/process-mammogram-images.sh script processes individual mammogram images (not sets):
- Converts to grayscale
- Compresses with pngquant (8 colours)
- Optionally resizes
- Preserves original filenames
- Accepts a folder or individual file paths
# Process all PNGs in a folder
./scripts/process-mammogram-images.sh reference/image-library-source --output reference/image-library-full
# Process specific files
./scripts/process-mammogram-images.sh reference/source/normal-lcc.png reference/source/normal-lmlo.png --output reference/image-library-full
# With resize
./scripts/process-mammogram-images.sh reference/source/file.png --output reference/image-library-half --resize 825- Test/verify probability distribution (debug route to sample many appointments)
- Real mammogram images integration (local gitignored folder)
- Annotation inheritance for composite sets (abnormality markers)
- Breast features alignment (moles, scars visible in mammograms)
The following planned features have been implemented:
- Image library: Individual images stored in
image-library/folder with metadata - Composite sets: Sets can reference other sets (
from) or image library (image) - Per-breast metadata:
leftandrightstatus on sets - Context-aware selection: Symptoms, implants, imperfect images, repeats
- Side preference: Abnormalities matched to symptom side (70% probability)
- View filtering: Only show views present in mammogram data
- Additional images: Support for repeat/retake scenarios with modal viewer
- Debug display: Press 'd' in viewer to see set info and context flags
- Repeat sets: 7 composite sets with
hasRepeat: trueflag
Real mammogram images cannot be committed to the public repo. Strategy:
- Store in a gitignored local folder (e.g.,
app/assets/images/mammogram-sets/) - Create a manifest file (
manifest.json) in that folder, same structure as diagrams - Existing config
mammogramImageSource: 'diagrams' | 'real'already supports switching - Code already looks for manifest in appropriate folder based on source
- Document the expected folder structure so images can be added locally
Current manifest only has overall tag (normal/abnormal/indeterminate). Enhancements:
Per-breast detail:
{
"id": "set-03",
"tag": "abnormal",
"description": "Implants + cancer RUOQ M5",
"left": { "status": "normal" },
"right": { "status": "abnormal", "location": "UOQ", "finding": "mass" }
}Participant matching criteria:
{
"hasImplants": true,
"technicalIssue": null,
"findings": ["mass", "calcification"]
}This enables smarter selection:
- Participant has symptoms in left breast → prefer sets with left breast abnormality
- Participant has implants → select implant sets
- Technical recall case → select sets with technical issues
- Appointment has repeat images → select repeat sets
The manifest supports composing sets from other sets and the image library:
-
Image library: Individual images stored with metadata in
image-library/folder.{ "imageLibrary": [ { "id": "normal-rmlo-01", "path": "image-library/normal-rmlo-01.png", "view": "rmlo", "status": "normal" }, { "id": "blurry-lcc-01", "path": "image-library/blurry-lcc-01.png", "view": "lcc", "status": "technical", "issue": "blurry" } ] } -
Set definitions: Sets can use default paths, reference other sets, or reference image library.
{ "sets": [ { "id": "set-17", "tag": "normal", "description": "Normal mammogram" }, { "id": "tech-blurry-lcc", "tag": "technical", "description": "Blurry LCC view", "views": { "rmlo": { "from": "set-17" }, "lmlo": { "from": "set-17" }, "rcc": { "from": "set-17" }, "lcc": { "image": "blurry-lcc-01" } } } ] }View object properties:
from: Reference another set (inherits that set's image for this view)image: Reference an image from the image library by IDpath: Override the image path (defaults to{set-id}/{view}.png)
Resolution order:
- If
imagespecified, load from image library - If
fromspecified, inherit from that set's view - Apply any explicit path override
- If view omitted entirely, defaults to
{set-id}/{view}.png
-
Array format for repeats: Views can be arrays to represent multiple attempts.
{ "id": "repeat-normal-blur-rmlo", "tag": "normal", "hasRepeat": true, "views": { "rmlo": [ { "image": "blur-2-rmlo" }, { "from": "set-17" } ], "lmlo": { "from": "set-17" }, "rcc": { "from": "set-17" }, "lcc": { "from": "set-17" } } } -
Per-breast metadata: Specified at set level for selection logic.
{ "id": "set-03", "tag": "abnormal", "left": { "status": "normal" }, "right": { "status": "abnormal" } } -
Special flags:
hasImplants: true- Set shows implants, only selected for participants with implantshasRepeat: true- Set has repeat images, only selected for appointments with repeatsdisabled: true- Set excluded from selection
Current technical issue images in library:
- Blur (various views)
- Positioning short
- Overexposed
- Underexposed
- Skin fold
Annotations are tied to a breast (pair of views), not individual images. Future implementation:
- When both views for a side come from the same source set, inherit that set's annotations
- Composite sets can optionally override with explicit
annotationsarray - Image library items can have annotations for abnormality images
Long-term: Align mammographer-marked breast features (moles, scars) with mammogram sets.
Options:
- Hardcode breast features for specific sets
- Hardcode mammogram sets with visible marks
- Dynamically overlay marks on "clean" sets (complex)
Don't show implant images where implants have been removed bird's eye viewer doens't update when moving between participant - reset to normal view