# HG changeset patch # User Sebastien Jodogne # Date 1670225389 -3600 # Node ID 04148de691a7ae37e7e80eb60e015088605a6e30 # Parent 37d6805b80ee3ed62cdda79d5207d235b65d973b# Parent 8ff083f67628fe74719ba49170da89e4cda6b660 integration mainline->deep-learning diff -r 37d6805b80ee -r 04148de691a7 Applications/StoneWebViewer/NEWS --- a/Applications/StoneWebViewer/NEWS Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/NEWS Mon Dec 05 08:29:49 2022 +0100 @@ -2,6 +2,24 @@ =============================== +Version 2.5 (2022-12-05) +======================== + +* Click-drag is available on the vertical slider +* Added key bindings: + - Left/right arrows to change the active frame + - Up/down arrows to change the active series + - Page up/down to change the active study + - Space bar to play/pause videos +* New URL argument "menu" to change the layout of the list of studies/series +* The first series to be loaded is now automatically opened +* Annotations are grouped into one submenu for narrow screens +* Support generation of ZIP archives in the presence of authorization tokens +* Fix measurement of arcs +* Width of the vertical slider has doubled to ease user interactions +* Patient sex is displayed in the top-left information panel + + Version 2.4 (2022-11-02) ======================== diff -r 37d6805b80ee -r 04148de691a7 Applications/StoneWebViewer/NOTES.txt --- a/Applications/StoneWebViewer/NOTES.txt Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/NOTES.txt Mon Dec 05 08:29:49 2022 +0100 @@ -12,14 +12,8 @@ in the background. -- Contrarily to the Osimis Web viewer, the Stone Web viewer does not - currently support annotations, and will not support Live Share. - - -- The Stone Web viewer has no "timeline" bar to see the position of - the current frame in the series. However, pressing the "Ctrl" key - together with mouse wheel enables fast move, i.e. this changes the - current frame by skipping 1/20th of the frames in the series. +- The Stone Web viewer does not support Live Share that was available + in old versions of the Osimis Web viewer. - The Stone Web viewer displays a color block at the bottom-right of @@ -130,6 +124,16 @@ "Authorization: Bearer Hello" +Additional options +================== + +- If present in the URL, the "menu" argument can be used to set the + default layout of the left-hand list of studies/series. Its allowed + values are "hidden", "small", "grid" (default value at the study + level), or "full" (default value at the series level). (new in Stone + Web viewer 2.5) + + Dynamic actions using messages ============================== diff -r 37d6805b80ee -r 04148de691a7 Applications/StoneWebViewer/Version.cmake diff -r 37d6805b80ee -r 04148de691a7 Applications/StoneWebViewer/WebApplication/app-fixes.css --- a/Applications/StoneWebViewer/WebApplication/app-fixes.css Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebApplication/app-fixes.css Mon Dec 05 08:29:49 2022 +0100 @@ -50,12 +50,12 @@ top: 0; bottom: 0; right: 0; - width: 10px; + width: 20px; background-color: #1b663e; } .wvInfoRightMargin { - right: 10px !important; /* must match the "width" of "wvVerticalScrollbar" */ + right: 20px !important; /* must match the "width" of "wvVerticalScrollbar" */ } .wvVerticalScrollbarHighlight { diff -r 37d6805b80ee -r 04148de691a7 Applications/StoneWebViewer/WebApplication/app.js --- a/Applications/StoneWebViewer/WebApplication/app.js Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebApplication/app.js Mon Dec 05 08:29:49 2022 +0100 @@ -34,6 +34,7 @@ var SERIES_DESCRIPTION = '0008,103e'; var MODALITY = '0008,0060'; var PATIENT_BIRTH_DATE = '0010,0030'; +var PATIENT_SEX = '0010,0040'; // Registry of the PDF series for which the instance metadata is still waiting var pendingSeriesPdf_ = {}; @@ -52,6 +53,9 @@ var MOUSE_TOOL_CREATE_TEXT_ANNOTATION = 12; // New in 2.4 var MOUSE_TOOL_MAGNIFYING_GLASS = 13; // New in 2.4 +var hasAuthorizationToken = false; +var axiosHeaders = {}; + function getParameterFromUrl(key) { var url = window.location.search.substring(1); @@ -124,6 +128,17 @@ } +function LookupIndexOfResource(array, tag, value) { + for (var i = 0; i < array.length; i++) { + if (array[i].tags[tag] == value) { + return i; + } + } + + return -1; +} + + /** * Enable support for tooltips in Bootstrap. This function must be * called after each modification to the DOM that introduces new @@ -139,6 +154,63 @@ } +function TriggerDownloadFromUri(uri, filename, mime) +{ + if (hasAuthorizationToken) { + axios.get(uri, { + headers: axiosHeaders, + responseType: 'arraybuffer' + }) + .then(function(response) { + const blob = new Blob([ response.data ], { type: mime }); + const url = URL.createObjectURL(blob); + + //window.open(url, '_blank'); + + // https://stackoverflow.com/a/19328891 + var a = document.createElement("a"); + document.body.appendChild(a); + a.style = "display: none"; + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); + }); + + } else { + // This version was used in Stone Web viewer <= 2.4, but doesn't + // work with authorization headers + + /** + * The use of "window.open()" below might be blocked (depending on + * the browser criteria to block popup). As a consequence, we + * prefer to set "window.location". + * https://www.nngroup.com/articles/the-top-ten-web-design-mistakes-of-1999/ + **/ + // window.open(uri, '_blank'); + window.location.href = uri; + } +} + + +/** + * The "mousemove" and "mouseup" events were added in Stone Web viewer + * 2.5 to allow click/drag on the vertical scrollbar. + **/ +var activeVerticalScrollbarViewport = null; +var activeVerticalScrollbarTarget = null; + +window.addEventListener('mousemove', function(event) { + if (activeVerticalScrollbarViewport !== null) { + activeVerticalScrollbarViewport.ClickVerticalScrollbar(event, activeVerticalScrollbarTarget); + event.preventDefault(); + } +}); + +window.addEventListener('mouseup', function(event) { + activeVerticalScrollbarViewport = null; +}); + Vue.component('viewport', { props: [ 'left', 'top', 'width', 'height', 'canvasId', 'active', 'content', 'viewportIndex', @@ -250,19 +322,33 @@ this.videoUri = ''; if (this.globalConfiguration.OrthancApiRoot) { var that = this; - axios.post(that.globalConfiguration.OrthancApiRoot + '/tools/find', - { - Level : 'Instance', - Query : { - StudyInstanceUID: studyInstanceUid - } - }) + axios.post(that.globalConfiguration.OrthancApiRoot + '/tools/find', { + Level : 'Instance', + Query : { + StudyInstanceUID: studyInstanceUid + } + }, { + headers: axiosHeaders + }) .then(function(response) { if (response.data.length != 1) { throw(''); } else { - that.videoUri = that.globalConfiguration.OrthancApiRoot + '/instances/' + response.data[0] + '/frames/0/raw'; + var uri = that.globalConfiguration.OrthancApiRoot + '/instances/' + response.data[0] + '/frames/0/raw'; + + if (hasAuthorizationToken) { + axios.get(uri, { + headers: axiosHeaders, + responseType: 'arraybuffer' + }) + .then(function(response) { + const blob = new Blob([ response.data ]); + that.videoUri = URL.createObjectURL(blob); + }); + } else { + that.videoUri = uri; + } } }) .catch(function(error) { @@ -331,6 +417,12 @@ that.windowingWidth = args.detail.windowingWidth; } }); + + window.addEventListener('KeyCineSwitch', function(args) { + if (that.active) { + that.KeyCineSwitch(); + } + }); }, methods: { DragDrop: function(event) { @@ -353,7 +445,7 @@ }, CinePlay: function() { this.cineControls = true; - this.cineIncrement = 1; + this.cineIncrement = -1; this.UpdateCine(); }, CinePause: function() { @@ -367,9 +459,16 @@ }, CineBackward: function() { this.cineControls = true; - this.cineIncrement = -1; + this.cineIncrement = 1; this.UpdateCine(); }, + KeyCineSwitch: function() { + if (this.cineIncrement != 0) { + this.CinePause(); + } else { + this.CinePlay(); + } + }, UpdateCine: function() { // Cancel the previous cine loop, if any if (this.cineTimeoutId !== null) { @@ -411,6 +510,23 @@ if (reschedule) { this.cineTimeoutId = setTimeout(this.CineCallback, 1000.0 / this.cineFramesPerSecond); } + }, + ClickVerticalScrollbar: function(event, target) { + if (target == undefined) { + target = event.currentTarget; + activeVerticalScrollbarViewport = this; + activeVerticalScrollbarTarget = target; + } + + var offset = target.getClientRects()[0]; + var y = event.clientY - offset.top; + var height = target.offsetHeight; + var frame = Math.min(this.numberOfFrames - 1, Math.floor(y * this.numberOfFrames / (height - 1))); + + if (frame >= 0 && + frame < this.numberOfFrames) { + this.currentFrame = frame; + } } } }); @@ -426,6 +542,7 @@ leftVisible: true, viewportLayoutButtonsVisible: false, mouseActionsVisible: false, + annotationActionsVisible: false, activeViewport: 0, showInfo: true, showReferenceLines: true, @@ -437,6 +554,7 @@ orthancSystem: {}, // Only available if "OrthancApiRoot" configuration option is set stoneWebViewerVersion: '...', emscriptenVersion: '...', + isFirstSeries: true, modalWarning: false, modalNotDiagnostic: false, @@ -541,8 +659,26 @@ }); } }, + + GetActiveViewportSeriesTags: function() { + if (this.activeViewport == 1) { + return this.viewport1Content.series.tags; + } + else if (this.activeViewport == 2) { + return this.viewport2Content.series.tags; + } + else if (this.activeViewport == 3) { + return this.viewport3Content.series.tags; + } + else if (this.activeViewport == 4) { + return this.viewport4Content.series.tags; + } + else { + return null; + } + }, - GetActiveSeries: function() { + GetActiveSeriesInstanceUid: function() { var s = []; if ('tags' in this.viewport1Content.series) @@ -689,6 +825,15 @@ } }, + SetViewportVirtualSeries: function(viewportIndex, seriesInstanceUid, virtualSeriesId) { + if (seriesInstanceUid in this.seriesIndex) { + this.SetViewportSeries(viewportIndex, { + seriesIndex: this.seriesIndex[seriesInstanceUid], + virtualSeriesId: virtualSeriesId + }); + } + }, + SetViewportSeries: function(viewportIndex, info) { var series = this.series[info.seriesIndex]; @@ -716,6 +861,9 @@ virtualSeriesId: info.virtualSeriesId }; } + + // Give the focus to this viewport (new in Stone Web viewer 2.5) + this.activeViewport = viewportIndex; }, ClickSeries: function(seriesIndex) { @@ -1071,22 +1219,16 @@ this.archiveJob.length > 0) { var that = this; - axios.get(that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob) + axios.get(that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob, { + headers: axiosHeaders + }) .then(function(response) { console.log('Progress of archive job ' + that.archiveJob + ': ' + response.data['Progress'] + '%'); var state = response.data['State']; if (state == 'Success') { that.creatingArchive = false; var uri = that.globalConfiguration.OrthancApiRoot + '/jobs/' + that.archiveJob + '/archive'; - - /** - * The use of "window.open()" below might be blocked - * (depending on the browser criteria to block popup). - * As a consequence, we prefer to set "window.location". - * https://www.nngroup.com/articles/the-top-ten-web-design-mistakes-of-1999/ - **/ - // window.open(uri, '_blank'); - window.location = uri; + TriggerDownloadFromUri(uri, that.archiveJob + '.zip', 'application/zip'); } else if (state == 'Running') { setTimeout(that.CheckIsDownloadComplete, 1000); @@ -1108,7 +1250,9 @@ console.log('Creating archive for study: ' + studyInstanceUid); var that = this; - axios.post(this.globalConfiguration.OrthancApiRoot + '/tools/lookup', studyInstanceUid) + axios.post(this.globalConfiguration.OrthancApiRoot + '/tools/lookup', studyInstanceUid, { + headers: axiosHeaders + }) .then(function(response) { if (response.data.length != 1) { throw(''); @@ -1127,12 +1271,13 @@ // ZIP streaming is available (this is Orthanc >= // 1.9.4): Simply give the hand to Orthanc event.preventDefault(); - window.location.href = uri; - + TriggerDownloadFromUri(uri, orthancId + '.zip', 'application/zip'); } else { // ZIP streaming is not available: Create a job to create the archive axios.post(uri, { 'Asynchronous' : true + }, { + headers: axiosHeaders }) .then(function(response) { that.creatingArchive = true; @@ -1145,12 +1290,74 @@ .catch(function (error) { alert('Cannot find the study in Orthanc'); }); - + }, + + ApplyDeepLearning: function() { + stone.ApplyDeepLearningModel(this.GetActiveCanvas()); }, - ApplyDeepLearning: function() - { - stone.ApplyDeepLearningModel(this.GetActiveCanvas()); + ChangeActiveSeries: function(offset) { + var seriesTags = this.GetActiveViewportSeriesTags(); + if (seriesTags !== null) { + var studyIndex = LookupIndexOfResource(this.studies, STUDY_INSTANCE_UID, seriesTags[STUDY_INSTANCE_UID]); + if (studyIndex != -1) { + var virtualSeriesId = this.GetActiveVirtualSeries(); + if (virtualSeriesId.length > 0) { + virtualSeriesId = virtualSeriesId[0]; + } else { + virtualSeriesId = ''; + } + + var seriesInStudyIndices = this.studies[studyIndex].series; + for (var i = 0; i < seriesInStudyIndices.length; i++) { + var series = this.series[seriesInStudyIndices[i]]; + if (this.series[seriesInStudyIndices[i]].tags[SERIES_INSTANCE_UID] == seriesTags[SERIES_INSTANCE_UID]) { + if (series.virtualSeries !== null) { + for (var j = 0; j < series.virtualSeries.length; j++) { + if (series.virtualSeries[j].ID == virtualSeriesId) { + var next = j + offset; + if (next >= 0 && + next < series.virtualSeries.length) { + this.SetViewportVirtualSeries(this.activeViewport, seriesTags[SERIES_INSTANCE_UID], series.virtualSeries[next].ID); + } + return; + } + } + } + else { + var next = i + offset; + if (next >= 0 && + next < seriesInStudyIndices.length) { + this.SetViewportSeriesInstanceUid(this.activeViewport, this.series[seriesInStudyIndices[next]].tags[SERIES_INSTANCE_UID]); + } + return; + } + } + } + } + } + }, + + ChangeActiveStudy: function(offset) { + var seriesTags = this.GetActiveViewportSeriesTags(); + if (seriesTags !== null) { + var studyIndex = LookupIndexOfResource(this.studies, STUDY_INSTANCE_UID, seriesTags[STUDY_INSTANCE_UID]); + if (studyIndex != -1) { + var next = studyIndex + offset; + if (next >= 0 && + next < this.studies.length) { + var nextStudy = this.studies[next]; + if (nextStudy.series.length > 0) { + var seriesIndex = nextStudy.series[0]; + if (this.series[seriesIndex].virtualSeries !== null) { + this.ClickVirtualSeries(seriesIndex, this.series[seriesIndex].virtualSeries[0].ID); + } else { + this.ClickSeries(seriesIndex); + } + } + } + } + } } }, @@ -1187,6 +1394,12 @@ var studyInstanceUid = args.detail.studyInstanceUid; var seriesInstanceUid = args.detail.seriesInstanceUid; that.UpdateIsSeriesComplete(studyInstanceUid, seriesInstanceUid); + + // Automatically open the first series to be loaded (new in Stone Web viewer 2.5) + if (that.isFirstSeries) { + that.SetViewportSeriesInstanceUid(1, seriesInstanceUid); + that.isFirstSeries = false; + } }); window.addEventListener('StoneAnnotationAdded', function() { @@ -1205,6 +1418,49 @@ args.detail.labelX, args.detail.labelY); } }); + + window.addEventListener('keydown', function(event) { + var canvas = that.GetActiveCanvas(); + if (canvas != '') { + switch (event.key) { + case 'Left': + case 'ArrowLeft': + stone.DecrementFrame(canvas, false); + break; + + case 'Right': + case 'ArrowRight': + stone.IncrementFrame(canvas, false); + break; + + case 'Up': + case 'ArrowUp': + that.ChangeActiveSeries(-1); + break + + case 'Down': + case 'ArrowDown': + that.ChangeActiveSeries(1); + break; + + case 'PageUp': + that.ChangeActiveStudy(-1); + break; + + case 'PageDown': + that.ChangeActiveStudy(1); + break; + + case ' ': + case 'Space': + dispatchEvent(new CustomEvent('KeyCineSwitch', { })); + break; + + default: + break; + } + } + }); } }); @@ -1235,9 +1491,20 @@ // Bearer token is new in Stone Web viewer 2.0 var token = getParameterFromUrl('token'); if (token !== undefined) { + hasAuthorizationToken = true; stone.AddHttpHeader('Authorization', 'Bearer ' + token); + axiosHeaders['Authorization'] = 'Bearer ' + token; } + if (app.globalConfiguration.OrthancApiRoot) { + axios.get(app.globalConfiguration.OrthancApiRoot + '/system', { + headers: axiosHeaders + }) + .then(function (response) { + app.orthancSystem = response.data; + }); + } + /** * Calls to "stone.XXX()" can be reordered after this point. @@ -1310,6 +1577,21 @@ alert('No study, nor patient was provided in the URL!'); } } + + // New in Stone Web viewer 2.5 + var menu = getParameterFromUrl('menu'); + if (menu !== undefined) { + if (menu == 'hidden') { + app.leftVisible = false; + } else if (menu == 'small' || + menu == 'grid' || + menu == 'full') { + app.leftVisible = true; + app.leftMode = menu; + } else { + alert('Bad value for the "menu" option in the URL (can be "hidden", "small", "grid", or "full"): ' + menu); + } + } }); @@ -1393,13 +1675,6 @@ .catch(function (error) { alert('Cannot load the WebAssembly framework'); }); - - if (app.globalConfiguration.OrthancApiRoot) { - axios.get(app.globalConfiguration.OrthancApiRoot + '/system') - .then(function (response) { - app.orthancSystem = response.data; - }); - } }) .catch(function (error) { alert('Cannot load the configuration file'); diff -r 37d6805b80ee -r 04148de691a7 Applications/StoneWebViewer/WebApplication/index.html --- a/Applications/StoneWebViewer/WebApplication/index.html Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebApplication/index.html Mon Dec 05 08:29:49 2022 +0100 @@ -36,6 +36,7 @@

Versions

Stone Web viewer: {{ stoneWebViewerVersion }}
+ Orthanc: {{ orthancSystem.Version }}
Emscripten compiler: {{ emscriptenVersion }}

@@ -74,6 +75,7 @@

Versions

Stone Web viewer: {{ stoneWebViewerVersion }}
+ Orthanc: {{ orthancSystem.Version }}
Emscripten compiler: {{ emscriptenVersion }}

User preferences

@@ -217,7 +219,7 @@
  • @@ -348,9 +350,9 @@
    @@ -385,9 +387,9 @@
    @@ -540,77 +542,94 @@
    -
    - -
    - -
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    + +
    + +
    -
    - -
    +
    + +
    + +
    + +
    -
    - +
    + +
    + +
    + +
    + +
    + +
    +
    +
    -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    diff -r 37d6805b80ee -r 04148de691a7 Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp --- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Nov 18 00:37:00 2022 +0100 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Mon Dec 05 08:29:49 2022 +0100 @@ -3530,6 +3530,23 @@ Redraw(); } } + + + void SignalSynchronizedBrowsing() + { + if (synchronizationEnabled_ && + frames_.get() != NULL && + cursor_.get() != NULL) + { + const size_t currentCursorIndex = cursor_->GetCurrentIndex(); + + const OrthancStone::CoordinateSystem3D current = + frames_->GetFrameGeometry(currentCursorIndex); + + observer_->SignalSynchronizedBrowsing( + *this, current.GetOrigin() + synchronizationOffset_, current.GetNormal()); + } + } }; @@ -4492,7 +4509,9 @@ { try { - return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus, isCircular) ? 1 : 0; + bool changed = GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus, isCircular); + GetViewport(canvas)->SignalSynchronizedBrowsing(); + return changed ? 1 : 0; } EXTERN_CATCH_EXCEPTIONS; return 0; @@ -4505,7 +4524,9 @@ { try { - return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus, isCircular) ? 1 : 0; + bool changed = GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus, isCircular); + GetViewport(canvas)->SignalSynchronizedBrowsing(); + return changed ? 1 : 0; } EXTERN_CATCH_EXCEPTIONS; return 0; @@ -4521,6 +4542,7 @@ if (frameNumber >= 0) { GetViewport(canvas)->SetFrame(static_cast(frameNumber)); + GetViewport(canvas)->SignalSynchronizedBrowsing(); } } EXTERN_CATCH_EXCEPTIONS; @@ -4533,6 +4555,7 @@ try { GetViewport(canvas)->GoToFirstFrame(); + GetViewport(canvas)->SignalSynchronizedBrowsing(); } EXTERN_CATCH_EXCEPTIONS; } @@ -4544,6 +4567,7 @@ try { GetViewport(canvas)->GoToLastFrame(); + GetViewport(canvas)->SignalSynchronizedBrowsing(); } EXTERN_CATCH_EXCEPTIONS; } diff -r 37d6805b80ee -r 04148de691a7 CITATION.cff --- a/CITATION.cff Fri Nov 18 00:37:00 2022 +0100 +++ b/CITATION.cff Mon Dec 05 08:29:49 2022 +0100 @@ -17,5 +17,5 @@ repository-code: 'https://hg.orthanc-server.com/orthanc-stone/' url: 'https://www.orthanc-server.com/' license: AGPL-3.0-or-later -version: 2.4 -date-released: 2022-11-02 +version: 2.5 +date-released: 2022-12-05 diff -r 37d6805b80ee -r 04148de691a7 OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp --- a/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp Fri Nov 18 00:37:00 2022 +0100 +++ b/OrthancStone/Sources/Scene2D/AnnotationsSceneLayer.cpp Mon Dec 05 08:29:49 2022 +0100 @@ -653,21 +653,34 @@ const double yc = middle_.GetY(); const double x2 = end_.GetX(); const double y2 = end_.GetY(); - - startAngle = atan2(y1 - yc, x1 - xc); - endAngle = atan2(y2 - yc, x2 - xc); - - fullAngle = endAngle - startAngle; - - while (fullAngle < -PI) + + double referenceAngle = atan2(y1 - yc, x1 - xc); + double secondAngle = atan2(y2 - yc, x2 - xc); + + secondAngle -= referenceAngle; + + while (secondAngle >= PI) + { + secondAngle -= 2.0 * PI; + } + + while (secondAngle <= -PI) { - fullAngle += 2.0 * PI; + secondAngle += 2.0 * PI; + } + + if (secondAngle < 0) + { + startAngle = referenceAngle + secondAngle; + endAngle = referenceAngle; } - - while (fullAngle >= PI) + else { - fullAngle -= 2.0 * PI; + startAngle = referenceAngle; + endAngle = referenceAngle + secondAngle; } + + fullAngle = endAngle - startAngle; } public: diff -r 37d6805b80ee -r 04148de691a7 TODO --- a/TODO Fri Nov 18 00:37:00 2022 +0100 +++ b/TODO Mon Dec 05 08:29:49 2022 +0100 @@ -37,6 +37,9 @@ up/down arrow keys (prev/next series). https://groups.google.com/g/orthanc-users/c/u_lH9aqKsdw/m/KQ7U9CkiAAAJ. +* Minor: Rotate the anchors of the text after rotation of the display. + + ------------ Known issues ------------