Browse Source

Merge branch 'develop' into feat/Tabs

Matthias Uiberacker 5 years ago
parent
commit
1d63a05ffd
54 changed files with 2538 additions and 949 deletions
  1. 1 1
      .eslintrc.yml
  2. 4 0
      .gitignore
  3. 2 3
      .travis.yml
  4. 15 1
      CHANGELOG.md
  5. 67 13
      demo/index.html
  6. 295 50
      demo/index.js
  7. 7 2
      karma.conf.js
  8. 39 21
      package.json
  9. 16 1
      src/MusicalScore/Graphical/BoundingBox.ts
  10. 84 14
      src/MusicalScore/Graphical/EngravingRules.ts
  11. 9 2
      src/MusicalScore/Graphical/GraphicalMeasure.ts
  12. 32 20
      src/MusicalScore/Graphical/GraphicalMusicPage.ts
  13. 5 0
      src/MusicalScore/Graphical/GraphicalNote.ts
  14. 2 121
      src/MusicalScore/Graphical/GraphicalVoiceEntry.ts
  15. 156 300
      src/MusicalScore/Graphical/MusicSheetCalculator.ts
  16. 2 3
      src/MusicalScore/Graphical/MusicSheetDrawer.ts
  17. 69 67
      src/MusicalScore/Graphical/MusicSystem.ts
  18. 198 82
      src/MusicalScore/Graphical/MusicSystemBuilder.ts
  19. 2 2
      src/MusicalScore/Graphical/SkyBottomLineCalculator.ts
  20. 11 7
      src/MusicalScore/Graphical/StaffLine.ts
  21. 6 6
      src/MusicalScore/Graphical/VexFlow/AlignmentManager.ts
  22. 29 4
      src/MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend.ts
  23. 22 4
      src/MusicalScore/Graphical/VexFlow/SvgVexFlowBackend.ts
  24. 38 2
      src/MusicalScore/Graphical/VexFlow/VexFlowBackend.ts
  25. 22 0
      src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts
  26. 2 3
      src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalSymbolFactory.ts
  27. 1 7
      src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts
  28. 27 107
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts
  29. 61 23
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts
  30. 2 3
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSystem.ts
  31. 6 2
      src/MusicalScore/Graphical/VexFlow/VexFlowStaffEntry.ts
  32. 7 0
      src/MusicalScore/Graphical/VexFlow/VexFlowStaffLine.ts
  33. 1 1
      src/MusicalScore/Graphical/VexFlow/VexFlowTabMeasure.ts
  34. 126 0
      src/MusicalScore/Graphical/VexFlow/VexFlowVoiceEntry.ts
  35. 1 1
      src/MusicalScore/Graphical/index.ts
  36. 1 2
      src/MusicalScore/Interfaces/IGraphicalSymbolFactory.ts
  37. 1 4
      src/MusicalScore/MusicSheet.ts
  38. 6 0
      src/MusicalScore/ScoreIO/InstrumentReader.ts
  39. 1 1
      src/MusicalScore/ScoreIO/MusicSymbolModules/RepetitionInstructionReader.ts
  40. 5 5
      src/MusicalScore/ScoreIO/VoiceGenerator.ts
  41. 13 2
      src/MusicalScore/VoiceData/Arpeggio.ts
  42. 9 1
      src/MusicalScore/VoiceData/Note.ts
  43. 1 0
      src/MusicalScore/VoiceData/Staff.ts
  44. 8 6
      src/OpenSheetMusicDisplay/Cursor.ts
  45. 29 3
      src/OpenSheetMusicDisplay/OSMDOptions.ts
  46. 259 47
      src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts
  47. 4 2
      test/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer_Test.ts
  48. 63 0
      test/Util/DiffImages_Test_Experimental.ts
  49. 243 0
      test/Util/generateDiffImagesPuppeteerLocalhost.js
  50. 272 0
      test/Util/generateImages_browserless.js
  51. 243 0
      test/Util/visual_regression.sh
  52. 1 1
      test/data/OSMD_function_test_Ornaments.xml
  53. 1 1
      tsconfig.json
  54. 11 1
      webpack.dev.js

+ 1 - 1
.eslintrc.yml

@@ -1,5 +1,5 @@
 parserOptions: {
-        ecmaVersion: 7, // the ecmaVersion isn't set in stone, for now added to satisfy eslint 
+        ecmaVersion: 8, // 8 = ECMA2017 necessary for promises/async. The ecmaVersion isn't set in stone, for now mostly adapted for eslint.
     }
 extends: standard
 rules:

+ 4 - 0
.gitignore

@@ -14,6 +14,10 @@ pids
 *.pid
 *.seed
 
+# optional npm script-generated data
+visual_regression/
+export/
+
 # Documentation
 docs
 

+ 2 - 3
.travis.yml

@@ -2,9 +2,8 @@ sudo: required
 dist: trusty
 language: node_js
 node_js:
-# - '6'
-# - '8'
-- '10'
+# - '10' # fails on Travis since upgrading to Vexflow 1.2.90 (still passes on AppVeyor), for Mxl_Test. Node 12 works.
+- '12'
 env:
   - timeout=10000
 notifications:

+ 15 - 1
CHANGELOG.md

@@ -1,3 +1,17 @@
+## [0.7.3](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/compare/0.7.2...0.7.3) (2020-01-15)
+
+
+### Bug Fixes
+
+* **Arpeggio:** fix up/down direction (wrong in Vexflow), remove Vexflow dependency ([450b2d9](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/450b2d9)), closes [#645](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/645)
+* **Dynamics, drawing range:** fix crescendo crashing when partially out of drawing range ([#644](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/644)) ([8105270](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/8105270))
+
+### Etc
+* **Imports:** Remove many Vexflow dependencies in core OSMD classes (Arpeggio, MusicSheetCalculator, other /Graphical/ classes) ([450b2d9](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/450b2d91bb4d52a60aeb6fa3425865e58efffebc), [90d93b9](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/90d93b907315b8b1d93586d4849d96c41fb60661))
+* **Cursor:** Improve Follow Cursor performance (thanks to @praisethemoon) ([#639](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/pull/639))
+
+
+
 ## [0.7.2](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/compare/0.7.1...0.7.2) (2019-12-13)
 
 
@@ -9,7 +23,7 @@
 * **autoBeamOption:** don't beam notes of type quarter or longer in tuplets ([c3b3b5a](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/c3b3b5a))
 * **barline:** don't automatically end piece with final barline if not specified ([#569](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/569)) ([8ae7938](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/8ae7938))
 * **barlines:** fix left barline added to end barline ([#588](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/588)) ([6608f17](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/6608f17))
-* **ChordSymbols:** save all chords on single note, show first instead of last for now ([#599](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/599)) ([2d7e265](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/2d7e265))
+* **ChordSymbols:** save and show all chords on single note ([#599](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/599)) ([2d7e265](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/2d7e265))
 * **credits placement:** fix title and composer label placement, now in relation to Staffline width ([b7af9b8](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/b7af9b8)), closes [#578](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/578)
 * **Cursor:** starts and ends at selected range of measures to draw ([#566](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/566)) ([3fe770e](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/3fe770e))
 * **demo:** set and reset options for specific test samples correctly ([b28b5dc](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/b28b5dc))

+ 67 - 13
demo/index.html

@@ -14,18 +14,18 @@
     <link rel="icon" href="./favicon.ico?" type="image/x-icon"/>
 </head>
 <body>
-<h1 class="ui centered header" id="header">
+<h1 class="ui centered header" id="header" style="opacity: 0.0">
     <img src="./favicon.ico?" class="ui image">
     <%= htmlWebpackPlugin.options.title %>
 </h1>
-<div class="ui four column grid container" id="divControls">
+<div class="ui four column grid container" id="divControls" style="visibility: hidden; opacity: 0.0">
     <div class="column">
         <h3 class="ui header">Select a sample:</h3>
-        <select class="ui selection dropdown" id="selectSample" style="width:320px; height:38%"></select>
+        <select class="ui selection dropdown" id="selectSample" style="width:320px; height:38%; visibility: hidden"></select>
     </div>
-    <div class="column">
+    <div class="column" id="backend-select-div" style="visibility: hidden">
         <h3 class="ui header">Render backend:</h3>
-        <select class="ui selection dropdown" id="backend-select" value="svg" style="height:38%">
+        <select class="ui selection dropdown" id="backend-select" value="svg" style="height:38%; visibility: hidden;">
             <option value="svg">SVG</option>
             <option value="canvas">Canvas</option>>
         </select>
@@ -83,7 +83,7 @@
     </div>
     <div class="column">
         <h3 class="ui header">Show bounding box for:</h3>
-        <select class="ui selection dropdown" id="selectBounding">
+        <select class="ui selection dropdown" id="selectBounding" style="visibility: hidden;">
             <option value="none">None</option>
             <option value="all">All</option>
             <option value="VexFlowMeasure">Measures</option>
@@ -126,13 +126,67 @@
             </div>
         </div>
     </div>
+    <div class="column">
+        <h3 class="ui header">Page size:</h3>
+        <select class="ui selection dropdown" id="selectPageSize"  style="visibility: hidden;">
+            <option value="endless">endless</option>
+            <option value="A3 P">A3 Portrait</option>
+            <option value="A3 L">A3 Landscape</option>
+            <option value="A4 P">A4 Portrait</option>
+            <option value="A4 L">A4 Landscape</option>
+            <option value="A5 P">A5 Portrait</option>
+            <option value="A5 L">A5 Landscape</option>
+            <option value="Letter P">Letter Portrait</option>
+            <option value="Letter L">Letter Landscape</option>
+        </select>
+        <div class="ui button" id="print-pdf-btn">Print to Pdf</div>
+    </div>
+</div>
+<div id="optionalControls" style="opacity: 0.0; width: 95%; display: block">
+    <div class="ui three column grid container" style="padding: 10px; margin-right: auto; margin-left: auto" id="optionalControlsColumnContainer">
+        <div class="column" id="zoomControlsButtons-optional-column" style="min-width: 30%; opacity: 0.0">
+            <div class="ui buttons" id="zoomControlsButtons-optional">
+                <div class="ui button" id="zoom-in-btn-optional">
+                    <i class="search plus icon"></i>
+                </div>
+                <div class="ui button" id="zoom-out-btn-optional">
+                    <i class="search minus icon"></i>
+                </div>
+            </div>
+            <h4 id="zoom-str-optional">???</h4>
+        </div>
+        <div class="column" id="print-pdf-btn-optional-column" style="opacity: 0.0; max-width: 25%;">
+            <div class="ui button" id="print-pdf-btn-optional">Print to Pdf</div>
+        </div>
+        <div class="column" id="selectPageSize-optional-column" style="opacity: 0.0; min-width: 35%">
+            <div class="ui two column grid container">
+            <div class="column" style="margin-top: 8px">
+            <h3>Format:</h3>
+            </div>
+            <div class="column">
+            <select class="ui selection dropdown" id="selectPageSize-optional">
+                <option value="endless">endless</option>
+                <option value="A3 P">A3 Portrait</option>
+                <option value="A3 L">A3 Landscape</option>
+                <option value="A4 P">A4 Portrait</option>
+                <option value="A4 L">A4 Landscape</option>
+                <option value="A5 P">A5 Portrait</option>
+                <option value="A5 L">A5 Landscape</option>
+                <option value="Letter P">Letter Portrait</option>
+                <option value="Letter L">Letter Landscape</option>
+            </select>
+            </div>
+            </div>
+        </div>
+    </div>
+</div>
+<div>
+    <table cellspacing="0" style="max-width:700px;">
+        <tr id="error-tr">
+            <td></td>
+            <td id="error-td"></td>
+        </tr>
+    </table>
 </div>
-<div id="optionalControls"></div>
-<table cellspacing="0" style="max-width:700px;">
-    <tr id="error-tr">
-        <td></td>
-        <td id="error-td"></td>
-    </tr>
-</table>
 </body>
 </html>

+ 295 - 50
demo/index.js

@@ -59,9 +59,9 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         selectBounding,
         skylineDebug,
         bottomlineDebug,
-        zoomIn,
-        zoomOut,
-        zoomDiv,
+        zoomIns,
+        zoomOuts,
+        zoomDivs,
         custom,
         nextCursorBtn,
         resetCursorBtn,
@@ -69,9 +69,12 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         showCursorBtn,
         hideCursorBtn,
         backendSelect,
+        backendSelectDiv,
         debugReRenderBtn,
-        debugClearBtn;
-
+        debugClearBtn,
+        selectPageSizes,
+        printPdfBtns;
+    
     // manage option setting and resetting for specific samples, e.g. in the autobeam sample autobeam is set to true, otherwise reset to previous state
     // TODO design a more elegant option state saving & restoring system, though that requires saving the options state in OSMD
     var minMeasureToDrawStashed = 1;
@@ -86,8 +89,16 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
     var drawPartNamesOptionNeedsReset = false;
 
     var showControls = true;
+    var showExportPdfControl = false;
+    var showPageFormatControl = false;
     var showZoomControl = true;
     var showHeader = true;
+    var showDebugControls = false;
+
+    if (process.env.OSMD_DEMO_TITLE) {
+        document.title = process.env.OSMD_DEMO_TITLE;
+    }
+
     // Initialization code
     function init() {
         var name, option;
@@ -95,44 +106,102 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         // Handle window parameter
         var paramEmbedded = findGetParameter('embedded');
         var paramShowControls = findGetParameter('showControls');
+        var paramShowPageFormatControl = findGetParameter('showPageFormatControl');
+        var paramShowExportPdfControl = findGetParameter('showExportPdfControl');
         var paramShowZoomControl = findGetParameter('showZoomControl');
         var paramShowHeader = findGetParameter('showHeader');
         var paramZoom = findGetParameter('zoom');
         var paramOverflow = findGetParameter('overflow');
         var paramOpenUrl = findGetParameter('openUrl');
-
+        var paramDebugControls = findGetParameter('debugControls');
+
+        var paramCompactMode = findGetParameter('compactMode');
+        var paramMeasureRangeStart = findGetParameter('measureRangeStart');
+        var paramMeasureRangeEnd = findGetParameter('measureRangeEnd');
+        var paramPageFormat = findGetParameter('pageFormat');
+        var paramPageBackgroundColor = findGetParameter('pageBackgroundColor');
+        var paramBackendType = findGetParameter('backendType');
+        var paramPageWidth = findGetParameter('pageWidth');
+        var paramPageHeight = findGetParameter('pageHeight');
+
+        var paramHorizontalScrolling = findGetParameter('horizontalScrolling');
+        var paramSingleHorizontalStaffline = findGetParameter('singleHorizontalStaffline');
+
+        showHeader = (paramShowHeader !== '0');
+        showControls = false;
         if (paramEmbedded !== undefined) {
-            showControls = (paramShowControls === '1');
-            showZoomControl = (paramShowZoomControl === '1');
-            showHeader = (paramShowHeader === '1');
+            showControls = paramShowControls !== '0';
+            showZoomControl = paramShowZoomControl !== '0';
+            showPageFormatControl = paramShowPageFormatControl !== '0';
+            showExportPdfControl = paramShowExportPdfControl !== '0';
         }
+
         if (paramZoom !== undefined) {
             if (paramZoom > 0.1 && paramZoom < 5.0) {
                 zoom = paramZoom;
-                console.log('Set zoom to ' + zoom);
             }
         }
-
         if (paramOverflow !== undefined && typeof paramOverflow === 'string') {
             if (paramOverflow === 'hidden' || paramOverflow === 'auto' || paramOverflow === 'scroll' || paramOverflow === 'visible') {
                 document.body.style.overflow = paramOverflow;
             }
         }
+        
+        var compactMode = paramCompactMode && paramCompactMode !== '0';
+        var measureRangeStart = paramMeasureRangeStart ? Number.parseInt(paramMeasureRangeStart) : 0;
+        var measureRangeEnd = paramMeasureRangeEnd ? Number.parseInt(paramMeasureRangeEnd) : Number.MAX_SAFE_INTEGER;
+        if (measureRangeStart && measureRangeEnd && measureRangeEnd < measureRangeStart) {
+            console.log("[OSMD] warning: measure range end parameter should not be smaller than measure range start. We've set start measure = end measure now.")
+            measureRangeStart = measureRangeEnd;
+        }
+        let pageFormat = paramPageFormat ? paramPageFormat : "Endless";
+        if (paramPageHeight && paramPageWidth) {
+            pageFormat = `${paramPageWidth}x${paramPageHeight}`
+        }
+        var pageBackgroundColor = paramPageBackgroundColor ? "#" + paramPageBackgroundColor : undefined; // vexflow format, see OSMDOptions. can't use # in parameters.
+        //console.log("demo: osmd pagebgcolor: " + pageBackgroundColor);
+        var backendType = (paramBackendType && paramBackendType.toLowerCase) ? paramBackendType : "svg";
+
+        var horizontalScrolling = paramHorizontalScrolling === '1';
+        var singleHorizontalStaffline = paramSingleHorizontalStaffline === '1';
+        
+        // set the backendSelect debug controls dropdown menu selected item
+        //console.log("true: " + backendSelect && backendType.toLowerCase && backendType.toLowerCase() === "canvas");
+        // TODO somehow backendSelect becomes undefined here:
+        /*if (backendSelect && backendType.toLowerCase && backendType.toLowerCase() === "canvas") {
+            console.log("here1");
+            for (var i=0; i<backendSelect.options.length; i++) {
+                if (backendSelect.options[i].value.toLowerCase() === "canvas") {
+                    backendSelect.selectedIndex = i;
+                }
+            }
+            backendSelect.value = "Canvas";
+        }*/
 
         divControls = document.getElementById('divControls');
         zoomControls = document.getElementById('zoomControls');
         header = document.getElementById('header');
         err = document.getElementById("error-td");
         error_tr = document.getElementById("error-tr");
-        zoomDiv = document.getElementById("zoom-str");
+        zoomDivs = [];
+        zoomDivs.push(document.getElementById("zoom-str"));
+        zoomDivs.push(document.getElementById("zoom-str-optional"));
         custom = document.createElement("option");
         selectSample = document.getElementById("selectSample");
         selectBounding = document.getElementById("selectBounding");
         skylineDebug = document.getElementById("skylineDebug");
         bottomlineDebug = document.getElementById("bottomlineDebug");
-        zoomIn = document.getElementById("zoom-in-btn");
-        zoomOut = document.getElementById("zoom-out-btn");
+        zoomIns = [];
+        zoomIns.push(document.getElementById("zoom-in-btn"));
+        zoomIns.push(document.getElementById("zoom-in-btn-optional"));
+        zoomOuts = [];
+        zoomOuts.push(document.getElementById("zoom-out-btn"));
+        zoomOuts.push(document.getElementById("zoom-out-btn-optional"));
         canvas = document.createElement("div");
+        if (horizontalScrolling) {
+            canvas.style.overflowX = 'auto'; // enable horizontal scrolling
+        }
+        //canvas.id = 'osmdCanvasDiv';
         //canvas.style.overflowX = 'auto'; // enable horizontal scrolling
         nextCursorBtn = document.getElementById("next-cursor-btn");
         resetCursorBtn = document.getElementById("reset-cursor-btn");
@@ -140,31 +209,111 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         showCursorBtn = document.getElementById("show-cursor-btn");
         hideCursorBtn = document.getElementById("hide-cursor-btn");
         backendSelect = document.getElementById("backend-select");
+        backendSelectDiv = document.getElementById("backend-select-div");
         debugReRenderBtn = document.getElementById("debug-re-render-btn");
         debugClearBtn = document.getElementById("debug-clear-btn");
+        selectPageSizes = [];
+        selectPageSizes.push(document.getElementById("selectPageSize"));
+        selectPageSizes.push(document.getElementById("selectPageSize-optional"));
+        printPdfBtns = [];
+        printPdfBtns.push(document.getElementById("print-pdf-btn"));
+        printPdfBtns.push(document.getElementById("print-pdf-btn-optional"));
+
+        //var defaultDisplayVisibleValue = "block"; // TODO in some browsers flow could be the better/default value
+        var defaultVisibilityValue = "visible";
+        var devDemoRunning = process.env.OSMD_DEBUG_CONTROLS;
+        showDebugControls = paramDebugControls === '1' || (devDemoRunning && paramDebugControls !== '0')
+        if (showDebugControls) {
+            var elementsToEnable = [
+                selectSample, selectBounding, selectPageSize, backendSelect, backendSelectDiv, divControls
+            ];
+            for (var i=0; i<elementsToEnable.length; i++) {
+                if (elementsToEnable[i]) { // make sure this element is not null/exists in the index.html, e.g. github.io demo has different index.html
+                    if (elementsToEnable[i].style) {
+                        elementsToEnable[i].style.visibility = defaultVisibilityValue;
+                        elementsToEnable[i].style.opacity = 1.0;
+                    }
+                }
+            }
+        } else {
+            if (divControls) {
+                divControls.style.display = "none";
+            }
+        }
 
-        if (!showControls) {
-            divControls.style.display = 'none';
+        const optionalControls = document.getElementById('optionalControls');
+        if (optionalControls) {
+            if (showControls) {
+                optionalControls.style.visibility = defaultVisibilityValue;
+                optionalControls.style.opacity = 0.8;
+            } else {
+                optionalControls.style.display = 'none';
+            }
         }
+
         if (!showHeader) {
-            header.style.display = 'none';
+            if (header) {
+                header.style.display = 'none';
+            }
+        } else {
+            if (header) {
+                header.style.opacity = 1.0;
+            }
         }
         // Hide error
         error();
 
-        if (!showControls && showZoomControl) {
-            const zoomControlsButtons = document.getElementById('zoomControlsButtons');
-            const zoomControlsString = document.getElementById('zoom-str');
+        if (showControls) {
             const optionalControls = document.getElementById('optionalControls');
-            optionalControls.appendChild(zoomControlsButtons);
-            optionalControls.appendChild(zoomControlsString);
-            optionalControls.style.position = 'absolute';
-            optionalControls.style.zIndex = '10';
-            optionalControls.style.right = '10px';
-            optionalControls.style.padding = '10px';
+            if (optionalControls) {
+                optionalControls.style.opacity = 1.0;
+                // optionalControls.appendChild(zoomControlsButtons);
+                // optionalControls.appendChild(zoomControlsString);
+                optionalControls.style.position = 'absolute';
+                optionalControls.style.zIndex = '10';
+                optionalControls.style.right = '10px';
+                // optionalControls.style.padding = '10px';
+            }
+
+            if (showZoomControl) {
+                const zoomControlsButtonsColumn = document.getElementById('zoomControlsButtons-optional-column');
+                zoomControlsButtonsColumn.style.opacity = 1.0;
+                // const zoomControlsButtons = document.getElementById('zoomControlsButtons-optional');
+                // zoomControlsButtons.style.opacity = 1.0;
+                const zoomControlsString = document.getElementById('zoom-str-optional'); // actually === zoomDivs[1] above
+
+                if (zoomControlsString) {
+                    zoomControlsString.innerHTML = Math.floor(zoom * 100.0) + "%";
+                    zoomControlsString.style.display = 'inline';
+                    // zoomControlsString.style.padding = '10px';
+                }
+            }
 
-            zoomControlsString.style.display = 'inline';
-            zoomControlsString.style.padding = '10px';
+            if (showExportPdfControl) {
+                const exportPdfButtonColumn = document.getElementById('print-pdf-btn-optional-column');
+                if (exportPdfButtonColumn) {
+                    exportPdfButtonColumn.style.opacity = 1.0;
+                }
+            }
+
+            const pageFormatControlColumn = document.getElementById("selectPageSize-optional-column");
+            if (pageFormatControlColumn) {
+                if (showPageFormatControl) {
+                    pageFormatControlColumn.style.opacity = 1.0;
+                } else {
+                    // showPageFormatControlColumn.innerHTML = "";
+                    // pageFormatControlColumn.style.minWidth = 0;
+                    // pageFormatControlColumn.style.width = 0;
+                    pageFormatControlColumn.style.display = 'none'; // squeezes buttons/columns
+                    // pageFormatControlColumn.style.visibility = 'hidden';
+
+                    // const optionalControlsColumnContainer = document.getElementById("optionalControlsColumnContainer");
+                    // optionalControlsColumnContainer.removeChild(pageFormatControlColumn);
+                    // optionalControlsColumnContainer.width *= 0.66;
+                    // optionalControls.witdh *= 0.66;
+                    // optionalControls.focus();
+                }
+            }
         }
 
         // Create select
@@ -174,26 +323,56 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
                 option.value = samples[name];
                 option.textContent = name;
             }
-            selectSample.appendChild(option);
+            if (selectSample) {
+                selectSample.appendChild(option);
+            }
+        }
+        if (selectSample) {
+            selectSample.onchange = selectSampleOnChange;
         }
-        selectSample.onchange = selectSampleOnChange;
         if (selectBounding) {
             selectBounding.onchange = selectBoundingOnChange;
         }
 
+        for (const selectPageSize of selectPageSizes) {
+            if (selectPageSize) {
+                selectPageSize.onchange = function (evt) {
+                    var value = evt.target.value;
+                    openSheetMusicDisplay.setPageFormat(value);
+                    openSheetMusicDisplay.render();
+                };
+            }
+        }
+
+        for (const printPdfBtn of printPdfBtns) {
+            if (printPdfBtn) {
+                printPdfBtn.onclick = function () {
+                    openSheetMusicDisplay.createPdf();
+                }
+            }
+        }
+
         // Pre-select default music piece
 
         custom.appendChild(document.createTextNode("Custom"));
 
         // Create zoom controls
-        zoomIn.onclick = function () {
-            zoom *= 1.2;
-            scale();
-        };
-        zoomOut.onclick = function () {
-            zoom /= 1.2;
-            scale();
-        };
+        for (const zoomIn of zoomIns) {
+            if (zoomIn) {
+                zoomIn.onclick = function () {
+                    zoom *= 1.2;
+                    scale();
+                };
+            }
+        }
+        for (const zoomOut of zoomOuts) {
+            if (zoomOut) {
+                zoomOut.onclick = function () {
+                    zoom /= 1.2;
+                    scale();
+                };
+            }
+        }
 
         if (skylineDebug) {
             skylineDebug.onclick = function () {
@@ -222,19 +401,20 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         // Create OSMD object and canvas
         openSheetMusicDisplay = new OpenSheetMusicDisplay(canvas, {
             autoResize: true,
-            backend: backendSelect.value,
+            backend: backendType,
+            //backend: "canvas",
             disableCursor: false,
-            drawingParameters: "default", // try compact (instead of default)
+            drawingParameters: compactMode ? "compact" : "default", // try compact (instead of default)
             drawPartNames: true, // try false
             // drawTitle: false,
             // drawSubtitle: false,
-            //drawFromMeasureNumber: 4,
-            //drawUpToMeasureNumber: 8,
             drawFingerings: true,
-            fingeringPosition: "auto", // left is default. try right. experimental: auto, above, below.
+            fingeringPosition: "left", // left is default. try right. experimental: auto, above, below.
             // fingeringInsideStafflines: "true", // default: false. true draws fingerings directly above/below notes
             setWantedStemDirectionByXml: true, // try false, which was previously the default behavior
             // drawUpToMeasureNumber: 3, // draws only up to measure 3, meaning it draws measure 1 to 3 of the piece.
+            drawFromMeasureNumber : measureRangeStart,
+            drawUpToMeasureNumber : measureRangeEnd,
 
             //drawMeasureNumbers: false, // disable drawing measure numbers
             //measureNumberInterval: 4, // draw measure numbers only every 4 bars (and at the beginning of a new system)
@@ -252,6 +432,10 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
                 maintain_stem_directions: false
             },
 
+            pageFormat: pageFormat,
+            pageBackgroundColor: pageBackgroundColor,
+            renderSingleHorizontalStaffline: singleHorizontalStaffline
+
             // tupletsBracketed: true, // creates brackets for all tuplets except triplets, even when not set by xml
             // tripletsBracketed: true,
             // tupletsRatioed: true, // unconventional; renders ratios for tuplets (3:2 instead of 3 for triplets)
@@ -277,7 +461,11 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             }
         }
         hideCursorBtn.addEventListener("click", function () {
-            openSheetMusicDisplay.cursor.hide();
+            if (openSheetMusicDisplay.cursor) {
+                openSheetMusicDisplay.cursor.hide();
+            } else {
+                console.info("Can't hide cursor, as it was disabled (e.g. by drawingParameters).");
+            }
         });
         showCursorBtn.addEventListener("click", function () {
             if (openSheetMusicDisplay.cursor) {
@@ -302,13 +490,16 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
             }
             console.log("[OSMD] selectSampleOnChange addEventListener change");
             // selectSampleOnChange();
-
         });
 
         if (paramOpenUrl !== undefined) {
             if (openSheetMusicDisplay.getLogLevel() < 2) { // debug or trace
                 console.log("[OSMD] selectSampleOnChange with " + paramOpenUrl);
             }
+            // DEBUG: cause an error for a certain sample, for testing
+            // if (paramOpenUrl.startsWith("Beethoven")) {
+            //     paramOpenUrl.causeError();
+            // }
             selectSampleOnChange(paramOpenUrl);
         } else {
             if (openSheetMusicDisplay.getLogLevel() < 2) { // debug or trace
@@ -319,6 +510,30 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
     }
 
     function findGetParameter(parameterName) {
+        // special treatment for the openUrl parameter, because different systems attach different arguments to an URL.
+        // because of CORS (cross-origin safety restrictions), you can only load an xml file from the same origin (server).
+
+        // test parameter: ?openUrl=https://opensheetmusiceducation.org/index.php?gf-download=2020%2F01%2FJohannSebastianBach_PraeludiumInCDur_BWV846_1.xml&endUrl&form-id=1&field-id=4&hash=c4ba271ef08204a26cbd4cd2d751c53b78f238c25ddbb1f343e1172f2ce2aa53
+        //   (enable the console.log at the end of this method for testing)
+        // working test parameter in local demo: ?openUrl=OSMD_function_test_all.xml&endUrl
+    
+        if (parameterName === 'openUrl') {
+            let startParameterName = 'openUrl=';
+            let endParameterName = '&endUrl';
+            let openUrlIndex = location.search.indexOf(startParameterName);
+            if (openUrlIndex < 0) {
+                return undefined;
+            }
+            let endIndex = location.search.indexOf(endParameterName) + endParameterName.length;
+            if (endIndex < 0) {
+                console.log("[OSMD] If using openUrl as a parameter, you have to end it with '&endUrl'. openUrl parameter omitted.");
+                return undefined;
+            }
+            let urlString = location.search.substring(openUrlIndex + startParameterName.length, endIndex - endParameterName.length);
+            //console.log("openUrl: " + urlString);
+            return urlString;
+        }
+
         let result = undefined;
         let tmp = [];
         location.search
@@ -328,7 +543,7 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
                 tmp = item.split('=');
                 if (tmp[0] === parameterName) {
                     result = decodeURIComponent(tmp[1]);
-                    console.log('Found param:' + parameterName + ' = ' + result);
+                    //console.log('Found param:' + parameterName + ' = ' + result);
                 }
             });
         return result;
@@ -344,7 +559,15 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         disable();
         var isCustom = typeof str === "string";
         if (!isCustom) {
-            str = sampleFolder + selectSample.value;
+            if (selectSample) {
+                str = sampleFolder + selectSample.value;
+            } else {
+                if (samples && samples.length > 0) {
+                    str = sampleFolder + samples[0];
+                } else {
+                    return; // no sample to load right now
+                }
+            }
         }
         // zoom = 1.0;
 
@@ -460,7 +683,11 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
     }
 
     function logCanvasSize() {
-        zoomDiv.innerHTML = Math.floor(zoom * 100.0) + "%";
+        for (const zoomDiv of zoomDivs) {
+            if (zoomDiv) {
+                zoomDiv.innerHTML = Math.floor(zoom * 100.0) + "%";
+            }
+        }
     }
 
     function scale() {
@@ -489,6 +716,7 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
         if (!errString) {
             error_tr.style.display = "none";
         } else {
+            console.log("[OSMD demo] error: " + errString)
             err.textContent = errString;
             error_tr.style.display = "";
             canvas.width = canvas.height = 0;
@@ -499,14 +727,31 @@ import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMus
     // Enable/Disable Controls
     function disable() {
         document.body.style.opacity = 0.3;
-        selectSample.disabled = zoomIn.disabled = zoomOut.disabled = "disabled";
+        setDisabledForControls("disabled");
     }
+
     function enable() {
         document.body.style.opacity = 1;
-        selectSample.disabled = zoomIn.disabled = zoomOut.disabled = "";
+        setDisabledForControls("");
         logCanvasSize();
     }
 
+    function setDisabledForControls(disabledValue) {
+        if (selectSample) {
+            selectSample.disabled = disabledValue;
+        }
+        for (const zoomIn of zoomIns) {
+            if (zoomIn) {
+                zoomIn.disabled = disabledValue;
+            }
+        }
+        for (const zoomOut of zoomOuts) {
+            if (zoomOut) {
+                zoomOut.disabled = disabledValue;
+            }
+        }
+    }
+
     // Register events: load, drag&drop
     window.addEventListener("load", function () {
         init();

+ 7 - 2
karma.conf.js

@@ -15,6 +15,10 @@ module.exports = function (config) {
 
         files: [
             {
+                pattern: 'test/Util/*.ts',
+                included: false
+            },
+            {
                 pattern: 'test/**/*.ts',
                 included: true
             }, {
@@ -28,7 +32,8 @@ module.exports = function (config) {
                 included: false,
                 watched: false,
                 served: true
-            }],
+            }
+        ],
 
         // preprocess matching files before serving them to the browser
         // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
@@ -53,7 +58,7 @@ module.exports = function (config) {
             resolve: common.resolve
         },
 
-        // Required for Firefox and Chorme to work
+        // Required for Firefox and Chrome to work
         // see https://github.com/webpack-contrib/karma-webpack/issues/188
         mime: {
             'text/x-typescript': ['ts']

+ 39 - 21
package.json

@@ -1,14 +1,14 @@
 {
   "name": "opensheetmusicdisplay",
-  "version": "0.7.2",
+  "version": "0.7.3",
   "description": "An open source JavaScript engine for displaying MusicXML based on VexFlow.",
   "main": "build/opensheetmusicdisplay.min.js",
   "typings": "build/dist/src/",
   "scripts": {
-    "docs": "typedoc --out ./build/docs --name OpenSheetMusicDisplay --module commonjs --target ES5 --ignoreCompilerErrors --mode file ./src",
+    "docs": "typedoc --out ./build/docs --name OpenSheetMusicDisplay --module commonjs --target ES2017 --ignoreCompilerErrors --mode file ./src",
     "eslint": "eslint .",
     "tslint": "tslint --project tsconfig.json \"src/**/*.ts\" \"test/**/*.ts\"",
-    "lint": "npm-run-all eslint tslint",
+    "lint": "npm-run-all tslint eslint",
     "test": "karma start --single-run --no-auto-watch",
     "test:watch": "karma start --no-single-run --auto-watch --browsers ChromeNoSecurity",
     "prepare": "npm run build",
@@ -18,6 +18,19 @@
     "build:webpack-dev": "webpack --progress --colors --config webpack.dev.js",
     "build:webpack-sourcemap": "webpack --progress --colors --config webpack.sourcemap.js",
     "start": "webpack-dev-server --progress --colors --config webpack.dev.js",
+    "generatePNG": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 1000 1600 allSmall --debug",
+    "generatePNG:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 0 0 ^Beethoven",
+    "generatePNG:puppeteer": "node ./test/Util/generateDiffImagesPuppeteerLocalhost.js ./test/data ./export 0 0 all",
+    "generatePNG:puppeteer:single": "node ./test/Util/generateDiffImagesPuppeteerLocalhost.js ./test/data ./export 0 0 ^Beethoven",
+    "generatePNG:paged": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 210 297 allSmall",
+    "generatePNG:paged:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 210 297 all --debug 5000",
+    "generatePNG:paged:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 0 0 ^Beethoven",
+    "generate:current": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 allSmall",
+    "generate:current:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 allSmall --debug",
+    "generate:current:singletest": "node test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 .*function_test_all.*",
+    "generate:blessed": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/blessed 0 0 allSmall",
+    "test:visual": "sh ./test/Util/visual_regression.sh ./visual_regression",
+    "test:visual:singletest": "sh ./test/Util/visual_regression.sh ./visual_regression OSMD_function_test_all",
     "fix-memory-limit": "cross-env NODE_OPTIONS=--max_old_space_size=4096"
   },
   "pre-commit": [
@@ -42,35 +55,40 @@
     "musicxml"
   ],
   "author": "PhonicScore",
-  "license": "MIT",
+  "license": "BSD-3-Clause",
   "bugs": {
     "url": "https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues"
   },
   "homepage": "http://opensheetmusicdisplay.org",
   "dependencies": {
+    "@types/vexflow": "^1.2.33",
     "es6-promise": "^4.2.5",
+    "jspdf-yworks": "^2.1.1",
     "jszip": "^3.0.0",
     "loglevel": "^1.5.0",
-    "typescript-collections": "^1.1.2",
-    "@types/vexflow": "^1.2.33",
-    "vexflow": "^1.2.89"
+    "svg2pdf.js": "^1.5.0",
+    "typescript-collections": "^1.3.3",
+    "vexflow": "1.2.90"
   },
   "devDependencies": {
-    "@types/chai": "^4.0.3",
-    "@types/mocha": "^5.2.6",
-    "@types/node": "^12.7.2",
+    "@types/chai": "^4.2.10",
+    "@types/mocha": "^7.0.2",
+    "@types/node": "^13.11.0",
+    "canvas": "^2.6.1",
     "chai": "^4.1.0",
     "clean-webpack-plugin": "^1.0.1",
-    "cross-env": "^6.0.2",
+    "cross-blob": "^1.2.0",
+    "cross-env": "^7.0.2",
     "cz-conventional-changelog": "^3.0.0",
-    "eslint": "^6.2.2",
+    "eslint": "^6.8.0",
     "eslint-config-standard": "^14.1.0",
-    "eslint-plugin-import": "^2.16.0",
-    "eslint-plugin-node": "^10.0.0",
-    "eslint-plugin-promise": "^4.0.1",
+    "eslint-plugin-import": "^2.20.2",
+    "eslint-plugin-node": "^11.0.0",
+    "eslint-plugin-promise": "^4.2.1",
     "eslint-plugin-standard": "^4.0.0",
-    "html-webpack-plugin": "^3.2.0",
+    "html-webpack-plugin": "^4.0.0",
     "jquery": "^3.4.1",
+    "jsdom": "^16.2.0",
     "karma": "^4.1.0",
     "karma-base64-to-js-preprocessor": "^0.0.1",
     "karma-chai": "^0.1.0",
@@ -80,16 +98,16 @@
     "karma-mocha-reporter": "^2.0.4",
     "karma-webpack": "^4.0.2",
     "karma-xml2js-preprocessor": "^0.0.3",
-    "mocha": "^6.2.0",
+    "mocha": "^7.0.1",
     "npm-run-all": "^4.1.2",
     "pre-commit": "^1.2.2",
     "ts-loader": "^4.1.0",
     "tslint": "^5.14.0",
-    "tslint-loader": "^3.5.3",
-    "typedoc": "^0.15.0",
-    "typescript": "^2.6.1",
+    "tslint-loader": "^3.5.4",
+    "typedoc": "^0.17.3",
+    "typescript": "^3.7.4",
     "webpack": "^4.39.3",
-    "webpack-cli": "^3.3.2",
+    "webpack-cli": "^3.3.11",
     "webpack-dev-server": "^3.8.0",
     "webpack-merge": "^4.1.2",
     "webpack-visualizer-plugin": "^0.1.11"

+ 16 - 1
src/MusicalScore/Graphical/BoundingBox.ts

@@ -3,6 +3,8 @@ import {ArgumentOutOfRangeException} from "../Exceptions";
 import {PointF2D} from "../../Common/DataObjects/PointF2D";
 import {SizeF2D} from "../../Common/DataObjects/SizeF2D";
 import {RectangleF2D} from "../../Common/DataObjects/RectangleF2D";
+import { StaffLineActivitySymbol } from "./StaffLineActivitySymbol";
+import { EngravingRules } from "./EngravingRules";
 
 /**
  * A bounding box delimits an area on the 2D plane.
@@ -201,7 +203,15 @@ export class BoundingBox {
     }
 
     public set Parent(value: BoundingBox) {
+        if (this.parent !== undefined) {
+            // remove from old parent
+            const index: number = this.parent.ChildElements.indexOf(this, 0);
+            if (index > -1) {
+                this.parent.ChildElements.splice(index, 1);
+            }
+        }
         this.parent = value;
+        // add to new parent
         if (this.parent.ChildElements.indexOf(this) > -1) {
             log.error("BoundingBox of " + (this.dataObject.constructor as any).name +
             " already in children list of " + (this.parent.dataObject.constructor as any).name + "'s BoundingBox");
@@ -367,7 +377,12 @@ export class BoundingBox {
         for (let idx: number = 0, len: number = this.ChildElements.length; idx < len; ++idx) {
             const childElement: BoundingBox = this.ChildElements[idx];
             minTop = Math.min(minTop, childElement.relativePosition.y + childElement.borderTop);
-            maxBottom = Math.max(maxBottom, childElement.relativePosition.y + childElement.borderBottom);
+            if (!EngravingRules.Rules.FixStafflineBoundingBox || !(childElement.dataObject instanceof StaffLineActivitySymbol)) {
+                maxBottom = Math.max(maxBottom, childElement.relativePosition.y + childElement.borderBottom);
+                // TODO there's a problem with the bottom bounding box of many stafflines, often caused by StaffLineActivitySymbol,
+                // often leading to the page SVG canvas being unnecessarily long in y-direction. This seems to be remedied by this workaround.
+                // see #643
+            }
             minMarginTop = Math.min(minMarginTop, childElement.relativePosition.y + childElement.borderMarginTop);
             maxMarginBottom = Math.max(maxMarginBottom, childElement.relativePosition.y + childElement.borderMarginBottom);
         }

+ 84 - 14
src/MusicalScore/Graphical/EngravingRules.ts

@@ -30,18 +30,18 @@ export class EngravingRules {
     private pageRightMargin: number;
     private titleTopDistance: number;
     private titleBottomDistance: number;
-    private systemDistance: number;
     private systemLeftMargin: number;
     private systemRightMargin: number;
     private firstSystemMargin: number;
     private systemLabelsRightMargin: number;
     private systemComposerDistance: number;
     private instrumentLabelTextHeight: number;
-    private minimumAllowedDistanceBetweenSystems: number;
+    private minimumDistanceBetweenSystems: number;
     private lastSystemMaxScalingFactor: number;
     private staffDistance: number;
     private betweenStaffDistance: number;
     private staffHeight: number;
+    private tabStaffHeight: number;
     private betweenStaffLinesDistance: number;
     /** Whether to automatically beam notes that don't already have beams in XML. */
     private autoBeamNotes: boolean;
@@ -214,6 +214,11 @@ export class EngravingRules {
     /** Position of fingering label in relation to corresponding note (left, right supported, above, below experimental) */
     private fingeringPosition: PlacementEnum;
     private fingeringInsideStafflines: boolean;
+    private pageFormat: PageFormat;
+    private pageBackgroundColor: string; // vexflow-color-string (#FFFFFF). Default undefined/transparent.
+    private renderSingleHorizontalStaffline: boolean;
+
+    private fixStafflineBoundingBox: boolean; // TODO temporary workaround
 
     constructor() {
         // global variables
@@ -242,15 +247,15 @@ export class EngravingRules {
 
         // System Sizing and Label Variables
         this.staffHeight = 4.0;
+        this.tabStaffHeight = 6.67;
         this.betweenStaffLinesDistance = EngravingRules.unit;
-        this.systemDistance = 10.0;
         this.systemLeftMargin = 0.0;
         this.systemRightMargin = 0.0;
         this.firstSystemMargin = 15.0;
         this.systemLabelsRightMargin = 2.0;
         this.systemComposerDistance = 2.0;
         this.instrumentLabelTextHeight = 2;
-        this.minimumAllowedDistanceBetweenSystems = 3.0;
+        this.minimumDistanceBetweenSystems = 4.0;
         this.lastSystemMaxScalingFactor = 1.4;
 
         // autoBeam options
@@ -438,6 +443,12 @@ export class EngravingRules {
         this.fingeringPosition = PlacementEnum.Left; // easier to get bounding box, and safer for vertical layout
         this.fingeringInsideStafflines = false;
 
+        this.fixStafflineBoundingBox = false; // TODO temporary workaround
+
+        this.pageFormat = PageFormat.UndefinedPageFormat; // default: undefined / 'infinite' height page, using the canvas'/container's width and height
+        this.pageBackgroundColor = undefined; // default: transparent. half-transparent white: #FFFFFF88"
+        this.renderSingleHorizontalStaffline = false;
+
         this.populateDictionaries();
         try {
             this.maxInstructionsConstValue = this.ClefLeftMargin + this.ClefRightMargin + this.KeyRightMargin + this.RhythmRightMargin + 11;
@@ -559,12 +570,6 @@ export class EngravingRules {
     public set InstrumentLabelTextHeight(value: number) {
         this.instrumentLabelTextHeight = value;
     }
-    public get SystemDistance(): number {
-        return this.systemDistance;
-    }
-    public set SystemDistance(value: number) {
-        this.systemDistance = value;
-    }
     public get SystemLeftMargin(): number {
         return this.systemLeftMargin;
     }
@@ -589,11 +594,11 @@ export class EngravingRules {
     public set SystemLabelsRightMargin(value: number) {
         this.systemLabelsRightMargin = value;
     }
-    public get MinimumAllowedDistanceBetweenSystems(): number {
-        return this.minimumAllowedDistanceBetweenSystems;
+    public get MinimumDistanceBetweenSystems(): number {
+        return this.minimumDistanceBetweenSystems;
     }
-    public set MinimumAllowedDistanceBetweenSystems(value: number) {
-        this.minimumAllowedDistanceBetweenSystems = value;
+    public set MinimumDistanceBetweenSystems(value: number) {
+        this.minimumDistanceBetweenSystems = value;
     }
     public get LastSystemMaxScalingFactor(): number {
         return this.lastSystemMaxScalingFactor;
@@ -619,6 +624,12 @@ export class EngravingRules {
     public set StaffHeight(value: number) {
         this.staffHeight = value;
     }
+    public get TabStaffHeight(): number {
+        return this.tabStaffHeight;
+    }
+    public set TabStaffHeight(value: number) {
+        this.tabStaffHeight = value;
+    }
     public get BetweenStaffLinesDistance(): number {
         return this.betweenStaffLinesDistance;
     }
@@ -1547,6 +1558,31 @@ export class EngravingRules {
     public set FingeringInsideStafflines(value: boolean) {
         this.fingeringInsideStafflines = value;
     }
+    public set FixStafflineBoundingBox(value: boolean) { // TODO temporary workaround
+        this.fixStafflineBoundingBox = value;
+    }
+    public get FixStafflineBoundingBox(): boolean {
+        return this.fixStafflineBoundingBox;
+    }
+
+    public get PageFormat(): PageFormat {
+        return this.pageFormat;
+    }
+    public set PageFormat(value: PageFormat) {
+        this.pageFormat = value;
+    }
+    public get PageBackgroundColor(): string {
+        return this.pageBackgroundColor;
+    }
+    public set PageBackgroundColor(value: string) {
+        this.pageBackgroundColor = value;
+    }
+    public get RenderSingleHorizontalStaffline(): boolean {
+        return this.renderSingleHorizontalStaffline;
+    }
+    public set RenderSingleHorizontalStaffline(value: boolean) {
+        this.renderSingleHorizontalStaffline = value;
+    }
 
     /**
      * This method maps NoteDurations to Distances and DistancesScalingFactors.
@@ -1609,3 +1645,37 @@ export class EngravingRules {
         }
     }
 }
+
+// TODO maybe this should be moved to OSMDOptions. Also see OpenSheetMusicDisplay.PageFormatStandards
+export class PageFormat {
+    constructor(width: number, height: number, idString: string = "noIdStringGiven") {
+        this.width = width;
+        this.height = height;
+        this.idString = idString;
+    }
+    public width: number;
+    public height: number;
+    public idString: string;
+    public get aspectRatio(): number {
+        if (!this.IsUndefined) {
+            return this.width / this.height;
+        } else {
+            return 0; // infinite page height
+        }
+    }
+    /** Undefined page format: use default page format. */
+    public get IsUndefined(): boolean {
+        return this.width === undefined || this.height === undefined || this.height === 0 || this.width === 0;
+    }
+
+    public static get UndefinedPageFormat(): PageFormat {
+        return new PageFormat(0, 0);
+    }
+
+    public Equals(otherPageFormat: PageFormat): boolean {
+        if (!otherPageFormat) {
+            return false;
+        }
+        return otherPageFormat.width === this.width && otherPageFormat.height === this.height;
+    }
+}

+ 9 - 2
src/MusicalScore/Graphical/GraphicalMeasure.ts

@@ -42,8 +42,6 @@ export abstract class GraphicalMeasure extends GraphicalObject {
 
     public parentSourceMeasure: SourceMeasure;
     public staffEntries: GraphicalStaffEntry[];
-    public parentMusicSystem: MusicSystem;
-    public tabMeasure: GraphicalMeasure = undefined;
     /**
      * The x-width of possibly existing: repetition start line, clef, key, rhythm.
      */
@@ -63,6 +61,7 @@ export abstract class GraphicalMeasure extends GraphicalObject {
     public hasError: boolean;
 
     private parentStaff: Staff;
+    private parentMusicSystem: MusicSystem;
     private measureNumber: number = -1;
     private parentStaffLine: StaffLine;
 
@@ -70,6 +69,14 @@ export abstract class GraphicalMeasure extends GraphicalObject {
         return this.parentStaff;
     }
 
+    public get ParentMusicSystem(): MusicSystem {
+        return this.parentMusicSystem;
+    }
+
+    public set ParentMusicSystem(value: MusicSystem) {
+        this.parentMusicSystem = value;
+    }
+
     public get MeasureNumber(): number {
         return this.measureNumber;
     }

+ 32 - 20
src/MusicalScore/Graphical/GraphicalMusicPage.ts

@@ -10,6 +10,7 @@ export class GraphicalMusicPage extends GraphicalObject {
     private musicSystems: MusicSystem[] = [];
     private labels: GraphicalLabel[] = [];
     private parent: GraphicalMusicSheet;
+    private pageNumber: number;
 
     constructor(parent: GraphicalMusicSheet) {
         super();
@@ -41,6 +42,14 @@ export class GraphicalMusicPage extends GraphicalObject {
         this.parent = value;
     }
 
+    public get PageNumber(): number {
+        return this.pageNumber;
+    }
+
+    public set PageNumber(value: number) {
+        this.pageNumber = value;
+    }
+
     /**
      * This method calculates the absolute Position of each GraphicalMusicPage according to a given placement
      * @param pageIndex
@@ -48,26 +57,29 @@ export class GraphicalMusicPage extends GraphicalObject {
      * @returns {PointF2D}
      */
     public setMusicPageAbsolutePosition(pageIndex: number, rules: EngravingRules): PointF2D {
-        if (rules.PagePlacement === PagePlacementEnum.Down) {
-            return new PointF2D(0.0, pageIndex * rules.PageHeight);
-        } else if (rules.PagePlacement === PagePlacementEnum.Right) {
-            return new PointF2D(pageIndex * this.parent.ParentMusicSheet.pageWidth, 0.0);
-        } else {
-            // placement RightDown
-            if (pageIndex % 2 === 0) {
-                if (pageIndex === 0) {
-                    return new PointF2D(0.0, pageIndex * rules.PageHeight);
-                } else {
-                    return new PointF2D(0.0, (pageIndex - 1) * rules.PageHeight);
-                }
-            } else {
-                if (pageIndex === 1) {
-                    return new PointF2D(this.parent.ParentMusicSheet.pageWidth, (pageIndex - 1) * rules.PageHeight);
-                } else {
-                    return new PointF2D(this.parent.ParentMusicSheet.pageWidth, (pageIndex - 2) * rules.PageHeight);
-                }
-            }
-        }
+        return new PointF2D(0.0, 0.0);
+
+        // use this code if pages are rendered on only one canvas:
+        // if (rules.PagePlacement === PagePlacementEnum.Down) {
+        //     return new PointF2D(0.0, pageIndex * rules.PageHeight);
+        // } else if (rules.PagePlacement === PagePlacementEnum.Right) {
+        //     return new PointF2D(pageIndex * this.parent.ParentMusicSheet.pageWidth, 0.0);
+        // } else {
+        //     // placement RightDown
+        //     if (pageIndex % 2 === 0) {
+        //         if (pageIndex === 0) {
+        //             return new PointF2D(0.0, pageIndex * rules.PageHeight);
+        //         } else {
+        //             return new PointF2D(0.0, (pageIndex - 1) * rules.PageHeight);
+        //         }
+        //     } else {
+        //         if (pageIndex === 1) {
+        //             return new PointF2D(this.parent.ParentMusicSheet.pageWidth, (pageIndex - 1) * rules.PageHeight);
+        //         } else {
+        //             return new PointF2D(this.parent.ParentMusicSheet.pageWidth, (pageIndex - 2) * rules.PageHeight);
+        //         }
+        //     }
+        // }
     }
 }
 

+ 5 - 0
src/MusicalScore/Graphical/GraphicalNote.ts

@@ -8,6 +8,7 @@ import {GraphicalObject} from "./GraphicalObject";
 import {MusicSheetCalculator} from "./MusicSheetCalculator";
 import {BoundingBox} from "./BoundingBox";
 import {GraphicalVoiceEntry} from "./GraphicalVoiceEntry";
+import {GraphicalMusicPage} from "./GraphicalMusicPage";
 
 /**
  * The graphical counterpart of a [[Note]]
@@ -57,4 +58,8 @@ export class GraphicalNote extends GraphicalObject {
       }
       return Math.min(3, num - 1);
     }
+
+    public get ParentMusicPage(): GraphicalMusicPage {
+      return this.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentMusicSystem.Parent;
+    }
 }

+ 2 - 121
src/MusicalScore/Graphical/GraphicalVoiceEntry.ts

@@ -4,11 +4,6 @@ import { BoundingBox } from "./BoundingBox";
 import { GraphicalNote } from "./GraphicalNote";
 import { GraphicalStaffEntry } from "./GraphicalStaffEntry";
 import { OctaveEnum } from "../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
-import { VexFlowVoiceEntry } from "./VexFlow/VexFlowVoiceEntry";
-import { EngravingRules } from "./EngravingRules";
-import { ColoringModes } from "./DrawingParameters";
-import { NoteEnum } from "../../Common/DataObjects/Pitch";
-import { Note } from "..";
 
 /**
  * The graphical counterpart of a [[VoiceEntry]].
@@ -38,123 +33,9 @@ export class GraphicalVoiceEntry extends GraphicalObject {
         });
     }
 
-    /** (Re-)color notes and stems by setting their Vexflow styles.
-     * Could be made redundant by a Vexflow PR, but Vexflow needs more solid and permanent color methods/variables for that
-     * See VexFlowConverter.StaveNote()
+    /** (Re-)color notes and stems
      */
     public color(): void {
-        const defaultColorNotehead: string = EngravingRules.Rules.DefaultColorNotehead;
-        const defaultColorRest: string = EngravingRules.Rules.DefaultColorRest;
-        const defaultColorStem: string = EngravingRules.Rules.DefaultColorStem;
-        const transparentColor: string = "#00000000"; // transparent color in vexflow
-        let noteheadColor: string; // if null: no noteheadcolor to set (stays black)
-
-        const vfStaveNote: any = (<VexFlowVoiceEntry>(this as any)).vfStaveNote;
-        for (let i: number = 0; i < this.notes.length; i++) {
-            const note: GraphicalNote = this.notes[i];
-
-            noteheadColor = note.sourceNote.NoteheadColor;
-            // Switch between XML colors and automatic coloring
-            if (EngravingRules.Rules.ColoringMode === ColoringModes.AutoColoring ||
-                EngravingRules.Rules.ColoringMode === ColoringModes.CustomColorSet) {
-                if (note.sourceNote.isRest()) {
-                    noteheadColor = EngravingRules.Rules.ColoringSetCurrent.getValue(-1);
-                } else {
-                    const fundamentalNote: NoteEnum = note.sourceNote.Pitch.FundamentalNote;
-                    noteheadColor = EngravingRules.Rules.ColoringSetCurrent.getValue(fundamentalNote);
-                }
-            }
-            if (!note.sourceNote.PrintObject) {
-                noteheadColor = transparentColor; // transparent
-            } else if (!noteheadColor // revert transparency after PrintObject was set to false, then true again
-                || noteheadColor === "#000000" // questionable, because you might want to set specific notes to black,
-                                               // but unfortunately some programs export everything explicitly as black
-                ) {
-                noteheadColor = EngravingRules.Rules.DefaultColorNotehead;
-            }
-
-            // DEBUG runtime coloring test
-            /*const testColor: string = "#FF0000";
-            if (i === 2 && Math.random() < 0.1 && note.sourceNote.NoteheadColor !== testColor) {
-                const measureNumber: number = note.parentVoiceEntry.parentStaffEntry.parentMeasure.MeasureNumber;
-                noteheadColor = testColor;
-                console.log("color changed to " + noteheadColor + " of this note:\n" + note.sourceNote.Pitch.ToString() +
-                    ", in measure #" + measureNumber);
-            }*/
-
-            if (!noteheadColor) {
-                if (!note.sourceNote.isRest() && defaultColorNotehead) {
-                    noteheadColor = defaultColorNotehead;
-                } else if (note.sourceNote.isRest() && defaultColorRest) {
-                    noteheadColor = defaultColorRest;
-                }
-            }
-            if (noteheadColor && note.sourceNote.PrintObject) {
-                note.sourceNote.NoteheadColor = noteheadColor;
-            } else if (!noteheadColor) {
-                continue;
-            }
-
-            // color notebeam if all noteheads have same color and stem coloring enabled
-            if (EngravingRules.Rules.ColoringEnabled && note.sourceNote.NoteBeam && EngravingRules.Rules.ColorStemsLikeNoteheads) {
-                const beamNotes: Note[] = note.sourceNote.NoteBeam.Notes;
-                let colorBeam: boolean = true;
-                for (let j: number = 0; j < beamNotes.length; j++) {
-                    if (beamNotes[j].NoteheadColor !== noteheadColor) {
-                        colorBeam = false;
-                    }
-                }
-                if (colorBeam) {
-                    if (vfStaveNote.beam !== null && vfStaveNote.beam.setStyle) {
-                        vfStaveNote.beam.setStyle({ fillStyle: noteheadColor, strokeStyle: noteheadColor});
-                    }
-                }
-            }
-
-            if (vfStaveNote) {
-                if (vfStaveNote.note_heads) { // see VexFlowConverter, needs Vexflow PR
-                    const notehead: any = vfStaveNote.note_heads[i];
-                    if (notehead) {
-                        notehead.setStyle({ fillStyle: noteheadColor, strokeStyle: noteheadColor });
-                    }
-                }
-            }
-        }
-
-        // color stems
-        let stemColor: string = EngravingRules.Rules.DefaultColorStem; // reset to black/default when coloring was disabled. maybe needed elsewhere too
-        if (EngravingRules.Rules.ColoringEnabled) {
-            stemColor = this.parentVoiceEntry.StemColor; // TODO: once coloringSetCustom gets stem color, respect it
-            if (!stemColor || EngravingRules.Rules.ColorStemsLikeNoteheads
-                || stemColor === "#000000") { // see above, noteheadColor === "#000000"
-                // condition could be even more fine-grained by only recoloring if there was no custom StemColor set. will be more complex though
-                if (noteheadColor) {
-                    stemColor = noteheadColor;
-                } else if (defaultColorStem) {
-                    stemColor = defaultColorStem;
-                }
-            }
-        }
-        let stemTransparent: boolean = true;
-        for (const note of this.parentVoiceEntry.Notes) {
-            if (note.PrintObject) {
-                stemTransparent = false;
-                break;
-            }
-        }
-        if (stemTransparent) {
-            stemColor = transparentColor;
-        }
-        const stemStyle: Object = { fillStyle: stemColor, strokeStyle: stemColor };
-
-        if (vfStaveNote && vfStaveNote.setStemStyle) {
-            if (!stemTransparent) {
-                this.parentVoiceEntry.StemColor = stemColor;
-            }
-            vfStaveNote.setStemStyle(stemStyle);
-            if (vfStaveNote.flag && vfStaveNote.setFlagStyle && EngravingRules.Rules.ColorFlags) {
-                vfStaveNote.setFlagStyle(stemStyle);
-            }
-        }
+        // override
     }
 }

+ 156 - 300
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -85,7 +85,7 @@ export abstract class MusicSheetCalculator {
 
     protected graphicalMusicSheet: GraphicalMusicSheet;
     protected rules: EngravingRules;
-    //protected symbolFactory: IGraphicalSymbolFactory;
+    protected musicSystems: MusicSystem[];
 
     public static get TextMeasurer(): ITextMeasurer {
         return MusicSheetCalculator.textMeasurer;
@@ -172,6 +172,8 @@ export abstract class MusicSheetCalculator {
      * The main method for the Calculator.
      */
     public calculate(): void {
+        this.musicSystems = [];
+
         this.clearSystemsAndMeasures();
 
         // delete graphicalObjects (currently: ties) that will be recalculated, newly create GraphicalObjects streching over a single StaffEntry
@@ -190,7 +192,7 @@ export abstract class MusicSheetCalculator {
         this.calculateMusicSystems();
 
         // Add some white space at the end of the piece:
-        this.graphicalMusicSheet.MusicPages[0].PositionAndShape.BorderMarginBottom += 9;
+        //this.graphicalMusicSheet.MusicPages[0].PositionAndShape.BorderMarginBottom += 9;
 
         // transform Relative to Absolute Positions
         GraphicalMusicSheet.transformRelativeToAbsolutePosition(this.graphicalMusicSheet);
@@ -228,55 +230,6 @@ export abstract class MusicSheetCalculator {
         throw new Error("abstract, not implemented");
     }
 
-    /** Calculates the relative Positions of all MusicSystems.
-     *
-     */
-    protected calculateMusicSystemsRelativePositions(graphicalMusicPage: GraphicalMusicPage): void {
-        // xPosition is always fixed
-        let relativePosition: PointF2D = new PointF2D(this.rules.PageLeftMargin + this.rules.SystemLeftMargin, 0);
-
-        if (EngravingRules.Rules.CompactMode) {
-            relativePosition.y += EngravingRules.Rules.PageTopMarginNarrow;
-        } else {
-            relativePosition.y += EngravingRules.Rules.PageTopMargin;
-        }
-
-        // first System is handled extra
-        const firstMusicSystem: MusicSystem = graphicalMusicPage.MusicSystems[0];
-        if (graphicalMusicPage === graphicalMusicPage.Parent.MusicPages[0]) {
-            if (EngravingRules.Rules.RenderTitle) {
-                relativePosition.y += this.rules.TitleTopDistance + this.rules.SheetTitleHeight +
-                    this.rules.TitleBottomDistance;
-            }
-        } else {
-            if (EngravingRules.Rules.RenderTitle) {
-                relativePosition.y += this.rules.PageTopMargin + this.rules.TitleTopDistance;
-            } else {
-                relativePosition.y = this.rules.PageTopMargin;
-            }
-        }
-
-        firstMusicSystem.PositionAndShape.RelativePosition = relativePosition;
-
-        for (let i: number = 1; i < graphicalMusicPage.MusicSystems.length; i++) {
-            const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[i];
-            relativePosition = new PointF2D(this.rules.PageLeftMargin + this.rules.SystemLeftMargin, 0);
-
-            // find optimum distance between Systems
-            const previousSystem: MusicSystem = graphicalMusicPage.MusicSystems[i - 1];
-            const lastPreviousStaffLine: StaffLine = previousSystem.StaffLines[previousSystem.StaffLines.length - 1];
-            const distance: number = (lastPreviousStaffLine.SkyBottomLineCalculator.getBottomLineMax() - this.rules.StaffHeight) +
-                Math.abs(musicSystem.StaffLines[0].SkyBottomLineCalculator.getSkyLineMin()) +
-                this.rules.MinimumAllowedDistanceBetweenSystems;
-
-            relativePosition.y = previousSystem.PositionAndShape.RelativePosition.y +
-                lastPreviousStaffLine.PositionAndShape.RelativePosition.y +
-                this.rules.StaffHeight + Math.max(this.rules.SystemDistance, distance);
-
-            musicSystem.PositionAndShape.RelativePosition = relativePosition;
-        }
-    }
-
     /**
      * Calculates the x layout of the staff entries within the staff measures belonging to one source measure.
      * All staff entries are x-aligned throughout all the measures.
@@ -287,47 +240,6 @@ export abstract class MusicSheetCalculator {
     }
 
     /**
-     * This method checks the distances between two System's StaffLines and if needed, shifts the lower down.
-     * @param musicSystem
-     */
-    protected optimizeDistanceBetweenStaffLines(musicSystem: MusicSystem): void {
-        musicSystem.PositionAndShape.calculateAbsolutePositionsRecursive(0, 0);
-
-        // don't perform any y-spacing in case of a StaffEntryLink (in both StaffLines)
-        if (!musicSystem.checkStaffEntriesForStaffEntryLink()) {
-            for (let i: number = 0; i < musicSystem.StaffLines.length - 1; i++) {
-                const upperBottomLine: number = musicSystem.StaffLines[i].SkyBottomLineCalculator.getBottomLineMax();
-                // TODO: Lower skyline should add to offset when there are items above the line. Currently no test
-                // file available
-                // const lowerSkyLine: number = Math.min(...musicSystem.StaffLines[i + 1].SkyLine);
-                if (Math.abs(upperBottomLine) > this.rules.MinimumStaffLineDistance) {
-                    // Remove staffheight from offset. As it results in huge distances
-                    const offset: number = Math.abs(upperBottomLine) + this.rules.MinimumStaffLineDistance - this.rules.StaffHeight;
-                    this.updateStaffLinesRelativePosition(musicSystem, i + 1, offset);
-                }
-            }
-        }
-    }
-
-    /**
-     * This method updates the System's StaffLine's RelativePosition (starting from the given index).
-     * @param musicSystem
-     * @param index
-     * @param value
-     */
-    protected updateStaffLinesRelativePosition(musicSystem: MusicSystem, index: number, value: number): void {
-        for (let i: number = index; i < musicSystem.StaffLines.length; i++) {
-            musicSystem.StaffLines[i].PositionAndShape.RelativePosition.y += value;
-        }
-
-        musicSystem.PositionAndShape.BorderBottom += value;
-    }
-
-    protected calculateSystemYLayout(): void {
-        throw new Error("abstract, not implemented");
-    }
-
-    /**
      * Called for every source measure when generating the list of staff measures for it.
      */
     protected initGraphicalMeasuresCreation(): void {
@@ -456,7 +368,7 @@ export abstract class MusicSheetCalculator {
         let end: number = relativeX - graphicalLabel.PositionAndShape.BorderLeft + graphicalLabel.PositionAndShape.BorderMarginRight;
 
         // take into account the InstrumentNameLabel's at the beginning of the first MusicSystem
-        if (staffLine === musicSystem.StaffLines[0] && musicSystem === musicSystem.Parent.MusicSystems[0]) {
+        if (staffLine === musicSystem.StaffLines[0] && musicSystem === this.musicSystems[0]) {
             start -= staffLine.PositionAndShape.RelativePosition.x;
             end -= staffLine.PositionAndShape.RelativePosition.x;
         }
@@ -684,19 +596,7 @@ export abstract class MusicSheetCalculator {
                 const graphicalMeasure: GraphicalMeasure = allMeasures[idx][idx2];
 
                 if (graphicalMeasure.isVisible()) {
-                    // if a Tab Measure exists:
-                    if (graphicalMeasure.tabMeasure !== undefined) {
-                        // if there is no linked measure with "normal notes" given for the Tabs,
-                        // add the current measure to show the normal notes:
-                        if (visiblegraphicalMeasures.length === 0) {
-                            visiblegraphicalMeasures.push(graphicalMeasure);
-                        }
-                        // add the Tab measure:
-                        visiblegraphicalMeasures.push(graphicalMeasure.tabMeasure);
-                    } else {
-                        // default case: normal measure
-                        visiblegraphicalMeasures.push(graphicalMeasure);
-                    }
+                    visiblegraphicalMeasures.push(graphicalMeasure);
 
                     if (EngravingRules.Rules.ColoringEnabled) {
                         // (re-)color notes
@@ -705,15 +605,6 @@ export abstract class MusicSheetCalculator {
                                 gve.color();
                             }
                         }
-
-                        if (graphicalMeasure.tabMeasure !== undefined) {
-                            // (re-)color tab notes
-                            for (const staffEntry of graphicalMeasure.tabMeasure.staffEntries) {
-                                for (const gve of staffEntry.graphicalVoiceEntries) {
-                                    gve.color();
-                                }
-                            }
-                        }
                     }
                 }
             }
@@ -737,7 +628,7 @@ export abstract class MusicSheetCalculator {
         // build the MusicSystems
         const musicSystemBuilder: MusicSystemBuilder = new MusicSystemBuilder();
         musicSystemBuilder.initialize(this.graphicalMusicSheet, visibleMeasureList, numberOfStaffLines);
-        musicSystemBuilder.buildMusicSystems();
+        this.musicSystems = musicSystemBuilder.buildMusicSystems();
 
         this.formatMeasures();
 
@@ -763,12 +654,9 @@ export abstract class MusicSheetCalculator {
 
         // calculate MeasureNumbers
         if (EngravingRules.Rules.RenderMeasureNumbers) {
-            for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-                const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-                for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                    const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-                    this.calculateMeasureNumberPlacement(musicSystem);
-                }
+            for (let idx: number = 0, len: number = this.musicSystems.length; idx < len; ++idx) {
+                const musicSystem: MusicSystem = this.musicSystems[idx];
+                this.calculateMeasureNumberPlacement(musicSystem);
             }
         }
         // calculate Slurs
@@ -806,19 +694,16 @@ export abstract class MusicSheetCalculator {
 
         // update all StaffLine's Borders
         // create temporary Object, just to call the methods (in order to avoid declaring them static)
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    this.updateStaffLineBorders(staffLine);
-                }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const musicSystem: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
+                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
+                this.updateStaffLineBorders(staffLine);
             }
         }
 
-        // Y-spacing
-        this.calculateSystemYLayout();
+        // calculate Y-spacing -> MusicPages are created here
+        musicSystemBuilder.calculateSystemYLayout();
         // calculate Comments for each Staffline
         this.calculateComments();
         // calculate marked Areas for Systems
@@ -830,16 +715,17 @@ export abstract class MusicSheetCalculator {
         for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
             const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
             for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
+                const isFirstSystem: boolean = idx === 0 && idx2 === 0;
                 const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
                 musicSystem.setMusicSystemLabelsYPosition();
                 if (!this.leadSheet) {
                     musicSystem.setYPositionsToVerticalLineObjectsAndCreateLines(this.rules);
-                    musicSystem.createSystemLeftLine(this.rules.SystemThinLineWidth, this.rules.SystemLabelsRightMargin);
+                    musicSystem.createSystemLeftLine(this.rules.SystemThinLineWidth, this.rules.SystemLabelsRightMargin, isFirstSystem);
                     musicSystem.createInstrumentBrackets(this.graphicalMusicSheet.ParentMusicSheet.Instruments, this.rules.StaffHeight);
                     musicSystem.createGroupBrackets(this.graphicalMusicSheet.ParentMusicSheet.InstrumentalGroups, this.rules.StaffHeight, 0);
                     musicSystem.alignBeginInstructions();
                 } else if (musicSystem === musicSystem.Parent.MusicSystems[0]) {
-                    musicSystem.createSystemLeftLine(this.rules.SystemThinLineWidth, this.rules.SystemLabelsRightMargin);
+                    musicSystem.createSystemLeftLine(this.rules.SystemThinLineWidth, this.rules.SystemLabelsRightMargin, isFirstSystem);
                 }
                 musicSystem.calculateBorders(this.rules);
             }
@@ -884,22 +770,20 @@ export abstract class MusicSheetCalculator {
     }
 
     protected calculateChordSymbols(): void {
-        for (const musicPage of this.graphicalMusicSheet.MusicPages) {
-            for (const musicSystem of musicPage.MusicSystems) {
-                for (const staffLine of musicSystem.StaffLines) {
-                    const sbc: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
-                    for (const measure of staffLine.Measures) {
-                        for (const staffEntry of measure.staffEntries) {
-                            if (!staffEntry.graphicalChordContainers || staffEntry.graphicalChordContainers.length === 0) {
-                                continue;
-                            }
-                            for (const graphicalChordContainer of staffEntry.graphicalChordContainers) {
-                                const sps: BoundingBox = staffEntry.PositionAndShape;
-                                const gps: BoundingBox = graphicalChordContainer.PositionAndShape;
-                                const start: number = gps.BorderMarginLeft + sps.AbsolutePosition.x;
-                                const end: number = gps.BorderMarginRight + sps.AbsolutePosition.x;
-                                sbc.updateSkyLineInRange(start, end, sps.BorderMarginTop);
-                            }
+        for (const musicSystem of this.musicSystems) {
+            for (const staffLine of musicSystem.StaffLines) {
+                const sbc: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
+                for (const measure of staffLine.Measures) {
+                    for (const staffEntry of measure.staffEntries) {
+                        if (!staffEntry.graphicalChordContainers || staffEntry.graphicalChordContainers.length === 0) {
+                            continue;
+                        }
+                        for (const graphicalChordContainer of staffEntry.graphicalChordContainers) {
+                            const sps: BoundingBox = staffEntry.PositionAndShape;
+                            const gps: BoundingBox = graphicalChordContainer.PositionAndShape;
+                            const start: number = gps.BorderMarginLeft + sps.AbsolutePosition.x;
+                            const end: number = gps.BorderMarginRight + sps.AbsolutePosition.x;
+                            sbc.updateSkyLineInRange(start, end, sps.BorderMarginTop);
                         }
                     }
                 }
@@ -998,13 +882,24 @@ export abstract class MusicSheetCalculator {
         }
 
         graphicalContinuousDynamic.EndMeasure = endMeasure;
+        const staffLine: StaffLine = graphicalContinuousDynamic.ParentStaffLine;
         const endStaffLine: StaffLine = endMeasure.ParentStaffLine;
+
+        // check if Expression spreads over the same StaffLine or not
+        const sameStaffLine: boolean = endStaffLine !== undefined && staffLine === endStaffLine;
+
+        let isPartOfMultiStaffInstrument: boolean = false;
+        if (endStaffLine) { // unfortunately we can't do something like (endStaffLine?.check() || staffLine?.check()) in this typescript version
+            isPartOfMultiStaffInstrument = endStaffLine.isPartOfMultiStaffInstrument();
+        } else if (staffLine) {
+            isPartOfMultiStaffInstrument = staffLine.isPartOfMultiStaffInstrument();
+        }
+
         const endAbsoluteTimestamp: Fraction = Fraction.createFromFraction(graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.AbsoluteTimestamp);
 
         const endPosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
-            endAbsoluteTimestamp, staffIndex, endStaffLine, endStaffLine.isPartOfMultiStaffInstrument(), 0);
+            endAbsoluteTimestamp, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0);
 
-        const staffLine: StaffLine = graphicalContinuousDynamic.ParentStaffLine;
         //currentMusicSystem and currentStaffLine
         const musicSystem: MusicSystem = staffLine.ParentMusicSystem;
         const currentStaffLineIndex: number = musicSystem.StaffLines.indexOf(staffLine);
@@ -1017,9 +912,6 @@ export abstract class MusicSheetCalculator {
         // if ContinuousDynamicExpression is given from wedge
         let secondGraphicalContinuousDynamic: GraphicalContinuousDynamicExpression = undefined;
 
-        // check if Expression spreads over the same StaffLine or not
-        const sameStaffLine: boolean = endStaffLine !== undefined && staffLine === endStaffLine;
-
         // last length check
         if (sameStaffLine && endPosInStaffLine.x - startPosInStaffline.x < this.rules.WedgeMinLength) {
             endPosInStaffLine.x = startPosInStaffline.x + this.rules.WedgeMinLength;
@@ -1028,6 +920,7 @@ export abstract class MusicSheetCalculator {
         // Upper staff wedge always starts at the given position and the lower staff wedge always starts at the begin of measure
         const upperStartX: number = startPosInStaffline.x;
         const lowerStartX: number = endStaffLine.Measures[0].beginInstructionsWidth - this.rules.WedgeHorizontalMargin - 2;
+        //TODO fix this when a range of measures to draw is given that doesn't include all the dynamic's measures (e.g. for crescendo)
         let upperEndX: number = 0;
         let lowerEndX: number = 0;
 
@@ -1433,7 +1326,7 @@ export abstract class MusicSheetCalculator {
                 // check if MusicSystem is first MusicSystem
                 if (staffLine.Measures[0].staffEntries.length > 0 &&
                     Math.abs(relative.x - staffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x) === 0 &&
-                    staffLine.ParentMusicSystem === staffLine.ParentMusicSystem.Parent.MusicSystems[0]) {
+                    staffLine.ParentMusicSystem === this.musicSystems[0]) {
                     const firstInstructionEntry: GraphicalStaffEntry = staffLine.Measures[0].FirstInstructionStaffEntry;
                     if (firstInstructionEntry) {
                         const lastInstruction: AbstractGraphicalInstruction = firstInstructionEntry.GraphicalInstructions.last();
@@ -1716,21 +1609,18 @@ export abstract class MusicSheetCalculator {
     }
 
     protected checkMeasuresForWholeRestNotes(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const musicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = musicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = musicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    for (let idx4: number = 0, len4: number = staffLine.Measures.length; idx4 < len4; ++idx4) {
-                        const measure: GraphicalMeasure = staffLine.Measures[idx4];
-                        if (measure.staffEntries.length === 1) {
-                            const gse: GraphicalStaffEntry = measure.staffEntries[0];
-                            if (gse.graphicalVoiceEntries.length > 0 && gse.graphicalVoiceEntries[0].notes.length === 1) {
-                                const graphicalNote: GraphicalNote = gse.graphicalVoiceEntries[0].notes[0];
-                                if (graphicalNote.sourceNote.Pitch === undefined && (new Fraction(1, 2)).lt(graphicalNote.sourceNote.Length)) {
-                                    this.layoutMeasureWithWholeRest(graphicalNote, gse, measure);
-                                }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const musicSystem: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
+                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
+                for (let idx4: number = 0, len4: number = staffLine.Measures.length; idx4 < len4; ++idx4) {
+                    const measure: GraphicalMeasure = staffLine.Measures[idx4];
+                    if (measure.staffEntries.length === 1) {
+                        const gse: GraphicalStaffEntry = measure.staffEntries[0];
+                        if (gse.graphicalVoiceEntries.length > 0 && gse.graphicalVoiceEntries[0].notes.length === 1) {
+                            const graphicalNote: GraphicalNote = gse.graphicalVoiceEntries[0].notes[0];
+                            if (graphicalNote.sourceNote.Pitch === undefined && (new Fraction(1, 2)).lt(graphicalNote.sourceNote.Length)) {
+                                this.layoutMeasureWithWholeRest(graphicalNote, gse, measure);
                             }
                         }
                     }
@@ -1835,7 +1725,11 @@ export abstract class MusicSheetCalculator {
     }
 
     protected calculatePageLabels(page: GraphicalMusicPage): void {
-
+        if (EngravingRules.Rules.RenderSingleHorizontalStaffline) {
+            page.PositionAndShape.BorderRight = page.PositionAndShape.Size.width;
+            page.PositionAndShape.calculateBoundingBox();
+            this.graphicalMusicSheet.ParentMusicSheet.pageWidth = page.PositionAndShape.Size.width;
+        }
         // The PositionAndShape child elements of page need to be manually connected to the lyricist, composer, subtitle, etc.
         // because the page is only available now
         let firstSystemAbsoluteTopMargin: number = 10;
@@ -2026,9 +1920,12 @@ export abstract class MusicSheetCalculator {
                                    openOctaveShifts: OctaveShiftParams[], openLyricWords: LyricWord[], staffIndex: number,
                                    staffEntryLinks: StaffEntryLink[]): GraphicalMeasure {
         const staff: Staff = this.graphicalMusicSheet.ParentMusicSheet.getStaffFromIndex(staffIndex);
-        const measure: GraphicalMeasure = MusicSheetCalculator.symbolFactory.createGraphicalMeasure(sourceMeasure, staff);
+        let measure: GraphicalMeasure = undefined;
         if (activeClefs[staffIndex].ClefType === ClefEnum.TAB) {
-            measure.tabMeasure = MusicSheetCalculator.symbolFactory.createTabStaffMeasure(sourceMeasure, staff);
+            staff.isTab = true;
+            measure = MusicSheetCalculator.symbolFactory.createTabStaffMeasure(sourceMeasure, staff);
+        } else {
+            measure = MusicSheetCalculator.symbolFactory.createGraphicalMeasure(sourceMeasure, staff);
         }
         measure.hasError = sourceMeasure.getErrorInMeasure(staffIndex);
         // check for key instruction changes
@@ -2081,18 +1978,6 @@ export abstract class MusicSheetCalculator {
                 } else {
                     measure.addGraphicalStaffEntry(graphicalStaffEntry);
                 }
-                // if there is a Tab measure
-                if (measure.tabMeasure !== undefined) {
-                    // create new Tab-GraphicalStaffEntry in Tab-Measure
-                    const tabStaffEntry: GraphicalStaffEntry = MusicSheetCalculator.symbolFactory.createStaffEntry(sourceStaffEntry, measure.tabMeasure);
-                    graphicalStaffEntry.tabStaffEntry = tabStaffEntry;
-                    if (entryIndex < measure.tabMeasure.staffEntries.length) {
-                        // a GraphicalStaffEntry has been inserted already at this Index (from Tie)
-                        measure.tabMeasure.addGraphicalStaffEntryAtTimestamp(tabStaffEntry);
-                    } else {
-                        measure.tabMeasure.addGraphicalStaffEntry(tabStaffEntry);
-                    }
-                }
 
                 const linkedNotes: Note[] = [];
                 if (sourceStaffEntry.Link !== undefined) {
@@ -2231,42 +2116,30 @@ export abstract class MusicSheetCalculator {
     }
 
     private calculateSkyBottomLines(): void {
-        for (const graphicalMusicPage of this.graphicalMusicSheet.MusicPages) {
-            for (const musicSystem of graphicalMusicPage.MusicSystems) {
-                for (const staffLine of musicSystem.StaffLines) {
-                    staffLine.SkyBottomLineCalculator.calculateLines();
-                }
+        for (const musicSystem of this.musicSystems) {
+            for (const staffLine of musicSystem.StaffLines) {
+                staffLine.SkyBottomLineCalculator.calculateLines();
             }
         }
     }
 
     /**
-     * Re-adjust the x positioning of expressions. Update the skyline afterwards
+     * Re-adjust the x positioning of expressions.
      */
-    private calculateExpressionAlignements(): void {
-        for (const graphicalMusicPage of this.graphicalMusicSheet.MusicPages) {
-            for (const musicSystem of graphicalMusicPage.MusicSystems) {
-                for (const staffLine of musicSystem.StaffLines) {
-                    staffLine.AlignmentManager.alignDynamicExpressions();
-                    staffLine.AbstractExpressions.forEach(ae => ae.updateSkyBottomLine());
-                }
-            }
-        }
+    protected calculateExpressionAlignements(): void {
+        // override
     }
 
     private calculateBeams(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const musicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = musicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = musicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    for (let idx4: number = 0, len4: number = staffLine.Measures.length; idx4 < len4; ++idx4) {
-                        const measure: GraphicalMeasure = staffLine.Measures[idx4];
-                        for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
-                            const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
-                            this.layoutBeams(staffEntry);
-                        }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const musicSystem: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
+                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
+                for (let idx4: number = 0, len4: number = staffLine.Measures.length; idx4 < len4; ++idx4) {
+                    const measure: GraphicalMeasure = staffLine.Measures[idx4];
+                    for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
+                        const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
+                        this.layoutBeams(staffEntry);
                     }
                 }
             }
@@ -2274,21 +2147,18 @@ export abstract class MusicSheetCalculator {
     }
 
     private calculateStaffEntryArticulationMarks(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const page: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = page.MusicSystems.length; idx2 < len2; ++idx2) {
-                const system: MusicSystem = page.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
-                    const line: StaffLine = system.StaffLines[idx3];
-                    for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
-                        const measure: GraphicalMeasure = line.Measures[idx4];
-                        for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
-                            const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
-                            for (let idx6: number = 0, len6: number = graphicalStaffEntry.sourceStaffEntry.VoiceEntries.length; idx6 < len6; ++idx6) {
-                                const voiceEntry: VoiceEntry = graphicalStaffEntry.sourceStaffEntry.VoiceEntries[idx6];
-                                if (voiceEntry.Articulations.length > 0) {
-                                    this.layoutArticulationMarks(voiceEntry.Articulations, voiceEntry, graphicalStaffEntry);
-                                }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const system: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
+                const line: StaffLine = system.StaffLines[idx3];
+                for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
+                    const measure: GraphicalMeasure = line.Measures[idx4];
+                    for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
+                        const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
+                        for (let idx6: number = 0, len6: number = graphicalStaffEntry.sourceStaffEntry.VoiceEntries.length; idx6 < len6; ++idx6) {
+                            const voiceEntry: VoiceEntry = graphicalStaffEntry.sourceStaffEntry.VoiceEntries[idx6];
+                            if (voiceEntry.Articulations.length > 0) {
+                                this.layoutArticulationMarks(voiceEntry.Articulations, voiceEntry, graphicalStaffEntry);
                             }
                         }
                     }
@@ -2298,26 +2168,23 @@ export abstract class MusicSheetCalculator {
     }
 
     private calculateOrnaments(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const page: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = page.MusicSystems.length; idx2 < len2; ++idx2) {
-                const system: MusicSystem = page.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
-                    const line: StaffLine = system.StaffLines[idx3];
-                    for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
-                        const measure: GraphicalMeasure = line.Measures[idx4];
-                        for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
-                            const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
-                            for (let idx6: number = 0, len6: number = graphicalStaffEntry.sourceStaffEntry.VoiceEntries.length; idx6 < len6; ++idx6) {
-                                const voiceEntry: VoiceEntry = graphicalStaffEntry.sourceStaffEntry.VoiceEntries[idx6];
-                                if (voiceEntry.OrnamentContainer !== undefined) {
-                                    if (voiceEntry.hasTie() && !graphicalStaffEntry.relInMeasureTimestamp.Equals(voiceEntry.Timestamp)) {
-                                        continue;
-                                    }
-                                    this.layoutOrnament(voiceEntry.OrnamentContainer, voiceEntry, graphicalStaffEntry);
-                                    if (!(this.staffEntriesWithOrnaments.indexOf(graphicalStaffEntry) !== -1)) {
-                                        this.staffEntriesWithOrnaments.push(graphicalStaffEntry);
-                                    }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const system: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
+                const line: StaffLine = system.StaffLines[idx3];
+                for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
+                    const measure: GraphicalMeasure = line.Measures[idx4];
+                    for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
+                        const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
+                        for (let idx6: number = 0, len6: number = graphicalStaffEntry.sourceStaffEntry.VoiceEntries.length; idx6 < len6; ++idx6) {
+                            const voiceEntry: VoiceEntry = graphicalStaffEntry.sourceStaffEntry.VoiceEntries[idx6];
+                            if (voiceEntry.OrnamentContainer !== undefined) {
+                                if (voiceEntry.hasTie() && !graphicalStaffEntry.relInMeasureTimestamp.Equals(voiceEntry.Timestamp)) {
+                                    continue;
+                                }
+                                this.layoutOrnament(voiceEntry.OrnamentContainer, voiceEntry, graphicalStaffEntry);
+                                if (!(this.staffEntriesWithOrnaments.indexOf(graphicalStaffEntry) !== -1)) {
+                                    this.staffEntriesWithOrnaments.push(graphicalStaffEntry);
                                 }
                             }
                         }
@@ -2328,18 +2195,15 @@ export abstract class MusicSheetCalculator {
     }
 
     private optimizeRestPlacement(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const page: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = page.MusicSystems.length; idx2 < len2; ++idx2) {
-                const system: MusicSystem = page.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
-                    const line: StaffLine = system.StaffLines[idx3];
-                    for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
-                        const measure: GraphicalMeasure = line.Measures[idx4];
-                        for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
-                            const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
-                            this.optimizeRestNotePlacement(graphicalStaffEntry, measure);
-                        }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const system: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
+                const line: StaffLine = system.StaffLines[idx3];
+                for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
+                    const measure: GraphicalMeasure = line.Measures[idx4];
+                    for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
+                        const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
+                        this.optimizeRestNotePlacement(graphicalStaffEntry, measure);
                     }
                 }
             }
@@ -2406,26 +2270,23 @@ export abstract class MusicSheetCalculator {
     }
 
     private calculateTieCurves(): void {
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    for (let idx4: number = 0, len5: number = staffLine.Measures.length; idx4 < len5; ++idx4) {
-                        const measure: GraphicalMeasure = staffLine.Measures[idx4];
-                        for (let idx6: number = 0, len6: number = measure.staffEntries.length; idx6 < len6; ++idx6) {
-                            const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx6];
-                            const graphicalTies: GraphicalTie[] = staffEntry.GraphicalTies;
-                            for (let idx7: number = 0, len7: number = graphicalTies.length; idx7 < len7; ++idx7) {
-                                const graphicalTie: GraphicalTie = graphicalTies[idx7];
-                                if (graphicalTie.StartNote !== undefined && graphicalTie.StartNote.parentVoiceEntry.parentStaffEntry === staffEntry) {
-                                    const tieIsAtSystemBreak: boolean = (
-                                        graphicalTie.StartNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine !==
-                                        graphicalTie.EndNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine
-                                    );
-                                    this.layoutGraphicalTie(graphicalTie, tieIsAtSystemBreak);
-                                }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const musicSystem: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
+                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
+                for (let idx4: number = 0, len5: number = staffLine.Measures.length; idx4 < len5; ++idx4) {
+                    const measure: GraphicalMeasure = staffLine.Measures[idx4];
+                    for (let idx6: number = 0, len6: number = measure.staffEntries.length; idx6 < len6; ++idx6) {
+                        const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx6];
+                        const graphicalTies: GraphicalTie[] = staffEntry.GraphicalTies;
+                        for (let idx7: number = 0, len7: number = graphicalTies.length; idx7 < len7; ++idx7) {
+                            const graphicalTie: GraphicalTie = graphicalTies[idx7];
+                            if (graphicalTie.StartNote !== undefined && graphicalTie.StartNote.parentVoiceEntry.parentStaffEntry === staffEntry) {
+                                const tieIsAtSystemBreak: boolean = (
+                                    graphicalTie.StartNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine !==
+                                    graphicalTie.EndNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine
+                                );
+                                this.layoutGraphicalTie(graphicalTie, tieIsAtSystemBreak);
                             }
                         }
                     }
@@ -2444,28 +2305,22 @@ export abstract class MusicSheetCalculator {
             }
         }
         // first calc lyrics text positions
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    const lyricsStaffEntries: GraphicalStaffEntry[] =
-                        this.calculateSingleStaffLineLyricsPosition(staffLine, staffLine.ParentStaff.ParentInstrument.LyricVersesNumbers);
-                    lyricStaffEntriesDict.setValue(staffLine, lyricsStaffEntries);
-                    this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
-                }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const musicSystem: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
+                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
+                const lyricsStaffEntries: GraphicalStaffEntry[] =
+                    this.calculateSingleStaffLineLyricsPosition(staffLine, staffLine.ParentStaff.ParentInstrument.LyricVersesNumbers);
+                lyricStaffEntriesDict.setValue(staffLine, lyricsStaffEntries);
+                this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
             }
         }
         // then fill in the lyric word dashes and lyrics extends/underscores
-        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
-            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
-            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
-                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
-                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
-                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
-                }
+        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
+            const musicSystem: MusicSystem = this.musicSystems[idx2];
+            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
+                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
+                this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
             }
         }
     }
@@ -2532,7 +2387,8 @@ export abstract class MusicSheetCalculator {
             this.calculateDashes(startStaffLine, startX, endX, y);
 
             // calculate Dashes for the second StaffLine (only if endStaffEntry isn't the first StaffEntry of the StaffLine)
-            if (nextStaffLine &&
+            if (nextStaffLine && // check for undefined objects e.g. when drawingRange given
+                nextStaffLine.Measures[0] &&
                 endStaffentry.parentMeasure.ParentStaffLine &&
                 !(endStaffentry === endStaffentry.parentMeasure.staffEntries[0] &&
                 endStaffentry.parentMeasure === endStaffentry.parentMeasure.ParentStaffLine.Measures[0])) {

+ 2 - 3
src/MusicalScore/Graphical/MusicSheetDrawer.ts

@@ -396,7 +396,7 @@ export abstract class MusicSheetDrawer {
         this.drawLine(graphicalLine.Start, graphicalLine.End, colorOrStyle, lineWidth);
     }
 
-    public drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number): void {
+    protected drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number): void {
         // implemented by subclass (VexFlowMusicSheetDrawer)
     }
 
@@ -464,7 +464,7 @@ export abstract class MusicSheetDrawer {
         this.graphicalMusicSheet.LeadSheet = value;
     }
 
-    private drawPage(page: GraphicalMusicPage): void {
+    protected drawPage(page: GraphicalMusicPage): void {
         if (!this.isVisible(page.PositionAndShape)) {
             return;
         }
@@ -484,7 +484,6 @@ export abstract class MusicSheetDrawer {
         if (this.drawableBoundingBoxElement) {
             this.drawBoundingBoxes(page.PositionAndShape, 0, this.drawableBoundingBoxElement);
         }
-
     }
 
     /**

+ 69 - 67
src/MusicalScore/Graphical/MusicSystem.ts

@@ -43,15 +43,12 @@ export abstract class MusicSystem extends GraphicalObject {
     protected graphicalMarkedAreas: GraphicalMarkedArea[] = [];
     protected graphicalComments: GraphicalComment[] = [];
     protected systemLines: SystemLine[] = [];
-    protected rules: EngravingRules;
 
-    constructor(parent: GraphicalMusicPage, id: number) {
+    constructor(id: number) {
         super();
-        this.parent = parent;
         this.id = id;
-        this.boundingBox = new BoundingBox(this, parent.PositionAndShape);
+        this.boundingBox = new BoundingBox(this);
         this.maxLabelLength = 0.0;
-        this.rules = this.parent.Parent.ParentMusicSheet.Rules;
     }
 
     public get Parent(): GraphicalMusicPage {
@@ -59,7 +56,16 @@ export abstract class MusicSystem extends GraphicalObject {
     }
 
     public set Parent(value: GraphicalMusicPage) {
+        // remove from old page
+        if (this.parent !== undefined) {
+            const index: number = this.parent.MusicSystems.indexOf(this, 0);
+            if (index > -1) {
+                this.parent.MusicSystems.splice(index, 1);
+            }
+        }
+
         this.parent = value;
+        this.boundingBox.Parent = value.PositionAndShape;
     }
 
     public get NextSystem(): MusicSystem {
@@ -116,9 +122,9 @@ export abstract class MusicSystem extends GraphicalObject {
      * @param lineWidth
      * @param systemLabelsRightMargin
      */
-    public createSystemLeftLine(lineWidth: number, systemLabelsRightMargin: number): void {
+    public createSystemLeftLine(lineWidth: number, systemLabelsRightMargin: number, isFirstSystem: boolean): void {
         let xPosition: number = -lineWidth / 2;
-        if (this === this.parent.MusicSystems[0] && this.parent === this.parent.Parent.MusicPages[0]) {
+        if (isFirstSystem) {
             xPosition = this.maxLabelLength + systemLabelsRightMargin - lineWidth / 2;
         }
         const top: GraphicalMeasure = this.staffLines[0].Measures[0];
@@ -195,7 +201,7 @@ export abstract class MusicSystem extends GraphicalObject {
     public AddGraphicalMeasures(graphicalMeasures: GraphicalMeasure[]): void {
         for (let idx: number = 0, len: number = graphicalMeasures.length; idx < len; ++idx) {
             const graphicalMeasure: GraphicalMeasure = graphicalMeasures[idx];
-            graphicalMeasure.parentMusicSystem = this;
+            graphicalMeasure.ParentMusicSystem = this;
         }
         this.graphicalMeasures.push(graphicalMeasures);
     }
@@ -279,79 +285,75 @@ export abstract class MusicSystem extends GraphicalObject {
      * @param systemLabelsRightMargin
      * @param labelMarginBorderFactor
      */
-    public createMusicSystemLabel(instrumentLabelTextHeight: number, systemLabelsRightMargin: number, labelMarginBorderFactor: number): void {
-        if (this.parent === this.parent.Parent.MusicPages[0]) {
-            const instruments: Instrument[] = this.parent.Parent.ParentMusicSheet.getVisibleInstruments();
-            for (let idx: number = 0, len: number = instruments.length; idx < len; ++idx) {
-                const instrument: Instrument = instruments[idx];
-                let instrNameLabel: Label;
-                if (this !== this.parent.MusicSystems[0]) {
-                    if (!EngravingRules.Rules.RenderPartAbbreviations
-                        // don't render part abbreviations if there's only one instrument/part (could be an option in the future)
-                        || this.Parent.Parent.ParentMusicSheet.Instruments.length === 1
-                        || !instrument.PartAbbreviation
-                        || instrument.PartAbbreviation === "") {
-                        return;
-                    }
-                    const labelText: string = instrument.PartAbbreviation;
-                    // const labelText: string = instrument.NameLabel.text[0] + ".";
-                    instrNameLabel = new Label(labelText, instrument.NameLabel.textAlignment, instrument.NameLabel.font);
-                } else {
-                    instrNameLabel = instrument.NameLabel;
-                    if (!EngravingRules.Rules.RenderPartNames) {
-                        instrNameLabel = new Label("", instrument.NameLabel.textAlignment, instrument.NameLabel.font);
-                        systemLabelsRightMargin = 0; // might affect lyricist/tempo placement. but without this there's still some extra x-spacing.
-                    }
+    public createMusicSystemLabel(  instrumentLabelTextHeight: number, systemLabelsRightMargin: number,
+                                    labelMarginBorderFactor: number, isFirstSystem: boolean = false): void {
+        for (let idx: number = 0, len: number = this.staffLines.length; idx < len; ++idx) {
+            const instrument: Instrument = this.staffLines[idx].ParentStaff.ParentInstrument;
+            let instrNameLabel: Label;
+            if (isFirstSystem) {
+                instrNameLabel = instrument.NameLabel;
+                if (!EngravingRules.Rules.RenderPartNames) {
+                    instrNameLabel = new Label("", instrument.NameLabel.textAlignment, instrument.NameLabel.font);
+                    systemLabelsRightMargin = 0; // might affect lyricist/tempo placement. but without this there's still some extra x-spacing.
+                }
+            } else {
+                if (!EngravingRules.Rules.RenderPartAbbreviations
+                    // don't render part abbreviations if there's only one instrument/part (could be an option in the future)
+                    || this.staffLines.length === 1
+                    || !instrument.PartAbbreviation
+                    || instrument.PartAbbreviation === "") {
+                    return;
                 }
-                const graphicalLabel: GraphicalLabel = new GraphicalLabel(
-                    instrNameLabel, instrumentLabelTextHeight, TextAlignmentEnum.LeftCenter, this.boundingBox
-                );
-                graphicalLabel.setLabelPositionAndShapeBorders();
-                this.labels.setValue(instrument, graphicalLabel);
-                // X-Position will be 0 (Label starts at the same PointF_2D with MusicSystem)
-                // Y-Position will be calculated after the y-Spacing
-                // graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(0.0, 0.0);
+                const labelText: string = instrument.PartAbbreviation;
+                // const labelText: string = instrument.NameLabel.text[0] + ".";
+                instrNameLabel = new Label(labelText, instrument.NameLabel.textAlignment, instrument.NameLabel.font);
             }
+            const graphicalLabel: GraphicalLabel = new GraphicalLabel(
+                instrNameLabel, instrumentLabelTextHeight, TextAlignmentEnum.LeftCenter, this.boundingBox
+            );
+            graphicalLabel.setLabelPositionAndShapeBorders();
+            this.labels.setValue(instrument, graphicalLabel);
+            // X-Position will be 0 (Label starts at the same PointF_2D with MusicSystem)
+            // Y-Position will be calculated after the y-Spacing
+            // graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(0.0, 0.0);
+        }
 
-            // calculate maxLabelLength (needed for X-Spacing)
-            this.maxLabelLength = 0.0;
-            const labels: GraphicalLabel[] = this.labels.values();
-            for (let idx: number = 0, len: number = labels.length; idx < len; ++idx) {
-                const label: GraphicalLabel = labels[idx];
-                if (label.PositionAndShape.Size.width > this.maxLabelLength) {
-                    this.maxLabelLength = label.PositionAndShape.Size.width;
-                }
+        // calculate maxLabelLength (needed for X-Spacing)
+        this.maxLabelLength = 0.0;
+        const labels: GraphicalLabel[] = this.labels.values();
+        for (let idx: number = 0, len: number = labels.length; idx < len; ++idx) {
+            const label: GraphicalLabel = labels[idx];
+            if (label.PositionAndShape.Size.width > this.maxLabelLength) {
+                this.maxLabelLength = label.PositionAndShape.Size.width;
             }
-            this.updateMusicSystemStaffLineXPosition(systemLabelsRightMargin);
         }
+        this.updateMusicSystemStaffLineXPosition(systemLabelsRightMargin);
     }
 
     /**
      * Set the Y-Positions for the MusicSystem's Labels.
      */
     public setMusicSystemLabelsYPosition(): void {
-        if (this.parent === this.parent.Parent.MusicPages[0]) {
-            this.labels.forEach((key: Instrument, value: GraphicalLabel): void => {
-                let ypositionSum: number = 0;
-                let staffCounter: number = 0;
-                for (let i: number = 0; i < this.staffLines.length; i++) {
-                    if (this.staffLines[i].ParentStaff.ParentInstrument === key) {
-                        for (let j: number = i; j < this.staffLines.length; j++) {
-                            const staffLine: StaffLine = this.staffLines[j];
-                            if (staffLine.ParentStaff.ParentInstrument !== key) {
-                                break;
-                            }
-                            ypositionSum += staffLine.PositionAndShape.RelativePosition.y;
-                            staffCounter++;
+        this.labels.forEach((key: Instrument, value: GraphicalLabel): void => {
+            let ypositionSum: number = 0;
+            let staffCounter: number = 0;
+            for (let i: number = 0; i < this.staffLines.length; i++) {
+                if (this.staffLines[i].ParentStaff.ParentInstrument === key) {
+                    for (let j: number = i; j < this.staffLines.length; j++) {
+                        const staffLine: StaffLine = this.staffLines[j];
+                        if (staffLine.ParentStaff.ParentInstrument !== key) {
+                            break;
                         }
-                        break;
+                        ypositionSum += staffLine.PositionAndShape.RelativePosition.y;
+                        staffCounter++;
                     }
+                    break;
                 }
-                if (staffCounter > 0) {
-                    value.PositionAndShape.RelativePosition = new PointF2D(0.0, ypositionSum / staffCounter + 2.0);
-                }
-            });
-        }
+            }
+            if (staffCounter > 0) {
+                value.PositionAndShape.RelativePosition = new PointF2D(0.0, ypositionSum / staffCounter + 2.0);
+            }
+        });
     }
 
     /**

+ 198 - 82
src/MusicalScore/Graphical/MusicSystemBuilder.ts

@@ -3,11 +3,12 @@ import {GraphicalMusicPage} from "./GraphicalMusicPage";
 import {EngravingRules} from "./EngravingRules";
 import {RhythmInstruction} from "../VoiceData/Instructions/RhythmInstruction";
 import {KeyInstruction} from "../VoiceData/Instructions/KeyInstruction";
-import {ClefInstruction, ClefEnum} from "../VoiceData/Instructions/ClefInstruction";
+import {ClefInstruction} from "../VoiceData/Instructions/ClefInstruction";
 import {SourceMeasure} from "../VoiceData/SourceMeasure";
 import {MusicSystem} from "./MusicSystem";
 import {BoundingBox} from "./BoundingBox";
 import {Staff} from "../VoiceData/Staff";
+import {Instrument} from "../Instrument";
 import {PointF2D} from "../../Common/DataObjects/PointF2D";
 import {StaffLine} from "./StaffLine";
 import {GraphicalLine} from "./GraphicalLine";
@@ -23,12 +24,11 @@ import {SystemLinePosition} from "./SystemLinePosition";
 export class MusicSystemBuilder {
     private measureList: GraphicalMeasure[][];
     private graphicalMusicSheet: GraphicalMusicSheet;
-    private currentMusicPage: GraphicalMusicPage;
-    private currentPageHeight: number;
     private currentSystemParams: SystemBuildParameters;
     private numberOfVisibleStaffLines: number;
     private rules: EngravingRules;
     private measureListIndex: number;
+    private musicSystems: MusicSystem[] = [];
 
     /**
      * Does the mapping from the currently visible staves to the global staff-list of the music sheet.
@@ -46,8 +46,6 @@ export class MusicSystemBuilder {
         this.graphicalMusicSheet = graphicalMusicSheet;
         this.rules = this.graphicalMusicSheet.ParentMusicSheet.rules;
         this.measureList = measureList;
-        this.currentMusicPage = this.createMusicPage();
-        this.currentPageHeight = 0.0;
         this.numberOfVisibleStaffLines = numberOfStaffLines;
         this.activeRhythm = new Array(this.numberOfVisibleStaffLines);
         this.activeKeys = new Array(this.numberOfVisibleStaffLines);
@@ -55,15 +53,14 @@ export class MusicSystemBuilder {
         this.initializeActiveInstructions(this.measureList[0]);
     }
 
-    public buildMusicSystems(): void {
+    public buildMusicSystems(): MusicSystem[] {
         let previousMeasureEndsSystem: boolean = false;
         const systemMaxWidth: number = this.getFullPageSystemWidth();
         this.measureListIndex = 0;
+        this.currentSystemParams = new SystemBuildParameters();
 
         // the first System - create also its Labels
-        this.initMusicSystem(this.measureList[0]);
-        this.addSystemLabels();
-        this.currentPageHeight += this.currentSystemParams.currentSystem.PositionAndShape.RelativePosition.y;
+        this.currentSystemParams.currentSystem = this.initMusicSystem();
 
         let numberOfMeasures: number = 0;
         for (let idx: number = 0, len: number = this.measureList.length; idx < len; ++idx) {
@@ -116,6 +113,7 @@ export class MusicSystemBuilder {
             }
             const totalMeasureWidth: number = currentMeasureBeginInstructionsWidth + currentMeasureEndInstructionsWidth + currentMeasureVarWidth;
             const measureFitsInSystem: boolean = this.currentSystemParams.currentWidth + totalMeasureWidth + nextMeasureBeginInstructionWidth < systemMaxWidth;
+            //if (true) // prevent line break at all costs, squeezes measures and breaks lyrics spacing
             if (isSystemStartMeasure || measureFitsInSystem) {
                 this.addMeasureToSystem(
                     graphicalMeasures, measureStartLine, measureEndLine, totalMeasureWidth,
@@ -131,6 +129,20 @@ export class MusicSystemBuilder {
             previousMeasureEndsSystem = sourceMeasureEndsSystem;
         }
         this.finalizeCurrentAndCreateNewSystem(this.measureList[this.measureList.length - 1], true);
+        return this.musicSystems;
+    }
+
+    /**
+     * calculates the y positions of the staff lines within a system and
+     * furthermore the y positions of the systems themselves.
+     */
+    public calculateSystemYLayout(): void {
+        for (const musicSystem of this.musicSystems) {
+            this.optimizeDistanceBetweenStaffLines(musicSystem);
+        }
+
+        // set y positions of systems using the previous system and a fixed distance.
+        this.calculateMusicSystemsRelativePositions();
     }
 
     /**
@@ -165,23 +177,9 @@ export class MusicSystemBuilder {
             this.checkAndCreateExtraInstructionMeasure(measures);
         }
         this.stretchMusicSystem(isPartEndingSystem);
-        if (this.currentPageHeight + this.currentSystemParams.currentSystem.PositionAndShape.Size.height + this.rules.SystemDistance <= this.rules.PageHeight) {
-            this.currentPageHeight += this.currentSystemParams.currentSystem.PositionAndShape.Size.height + this.rules.SystemDistance;
-            if (
-                this.currentPageHeight + this.currentSystemParams.currentSystem.PositionAndShape.Size.height
-                + this.rules.SystemDistance >= this.rules.PageHeight
-            ) {
-                this.currentMusicPage = this.createMusicPage();
-                this.currentPageHeight = this.rules.PageTopMargin + this.rules.TitleTopDistance;
-            }
-        } else {
-            this.currentMusicPage = this.createMusicPage();
-            this.currentPageHeight = this.rules.PageTopMargin + this.rules.TitleTopDistance;
-        }
-
+        this.currentSystemParams = new SystemBuildParameters();
         if (this.measureListIndex < this.measureList.length) {
-            this.initMusicSystem(measures);
-            this.addSystemLabels();
+            this.currentSystemParams.currentSystem = this.initMusicSystem();
         }
     }
 
@@ -225,40 +223,20 @@ export class MusicSystemBuilder {
         this.currentSystemParams.systemMeasureIndex++;
     }
 
-    private addSystemLabels(): void {
-        this.currentSystemParams.currentSystem.createMusicSystemLabel(
-            this.rules.InstrumentLabelTextHeight,
-            this.rules.SystemLabelsRightMargin,
-            this.rules.LabelMarginBorderFactor
-        );
-    }
-
-    /**
-     * Create a new [[GraphicalMusicPage]]
-     * (for now only one long page is used per music sheet, as we scroll down and have no page flips)
-     * @returns {GraphicalMusicPage}
-     */
-    private createMusicPage(): GraphicalMusicPage {
-        const page: GraphicalMusicPage = new GraphicalMusicPage(this.graphicalMusicSheet);
-        this.graphicalMusicSheet.MusicPages.push(page);
-        page.PositionAndShape.BorderLeft = 0.0;
-        page.PositionAndShape.BorderRight = this.graphicalMusicSheet.ParentMusicSheet.pageWidth;
-        page.PositionAndShape.BorderTop = 0.0;
-        page.PositionAndShape.BorderBottom = this.rules.PageHeight;
-        page.PositionAndShape.RelativePosition = new PointF2D(0.0, 0.0);
-        return page;
-    }
-
     /**
      * Initialize a new [[MusicSystem]].
      * @returns {MusicSystem}
      */
-    private initMusicSystem(measures: GraphicalMeasure[]): MusicSystem {
-        this.currentSystemParams = new SystemBuildParameters();
-        const musicSystem: MusicSystem = MusicSheetCalculator.symbolFactory.createMusicSystem(this.currentMusicPage, this.globalSystemIndex++);
-        this.currentSystemParams.currentSystem = musicSystem;
-        this.layoutSystemStaves(measures);
-        this.currentMusicPage.MusicSystems.push(musicSystem);
+    private initMusicSystem(): MusicSystem {
+        const musicSystem: MusicSystem = MusicSheetCalculator.symbolFactory.createMusicSystem(this.globalSystemIndex++);
+        this.musicSystems.push(musicSystem);
+        this.layoutSystemStaves(musicSystem);
+        musicSystem.createMusicSystemLabel(
+            this.rules.InstrumentLabelTextHeight,
+            this.rules.SystemLabelsRightMargin,
+            this.rules.LabelMarginBorderFactor,
+            this.musicSystems.length === 1
+        );
         return musicSystem;
     }
 
@@ -267,19 +245,29 @@ export class MusicSystemBuilder {
      * @returns {number}
      */
     private getFullPageSystemWidth(): number {
-        return this.currentMusicPage.PositionAndShape.Size.width - this.rules.PageLeftMargin
+        return this.graphicalMusicSheet.ParentMusicSheet.pageWidth - this.rules.PageLeftMargin
             - this.rules.PageRightMargin - this.rules.SystemLeftMargin - this.rules.SystemRightMargin;
     }
 
-    private layoutSystemStaves(measures: GraphicalMeasure[]): void {
+    private layoutSystemStaves(musicSystem: MusicSystem): void {
         const systemWidth: number = this.getFullPageSystemWidth();
-        const musicSystem: MusicSystem = this.currentSystemParams.currentSystem;
         const boundingBox: BoundingBox = musicSystem.PositionAndShape;
         boundingBox.BorderLeft = 0.0;
         boundingBox.BorderRight = systemWidth;
         boundingBox.BorderTop = 0.0;
-
-        /* let multiLyrics: boolean = false;
+        const staffList: Staff[] = [];
+        const instruments: Instrument[] = this.graphicalMusicSheet.ParentMusicSheet.Instruments;
+        for (let idx: number = 0, len: number = instruments.length; idx < len; ++idx) {
+            const instrument: Instrument = instruments[idx];
+            if (instrument.Voices.length === 0 || !instrument.Visible) {
+                continue;
+            }
+            for (let idx2: number = 0, len2: number = instrument.Staves.length; idx2 < len2; ++idx2) {
+                const staff: Staff = instrument.Staves[idx2];
+                staffList.push(staff);
+            }
+        }
+        let multiLyrics: boolean = false;
         if (this.leadSheet) {
             for (let idx: number = 0, len: number = staffList.length; idx < len; ++idx) {
                 const staff: Staff = staffList[idx];
@@ -288,23 +276,24 @@ export class MusicSystemBuilder {
                     break;
                 }
             }
-        } */
+        }
         let yOffsetSum: number = 0;
-        for (let idx: number = 0, len: number = measures.length; idx < len; ++idx) {
-            this.addStaffLineToMusicSystem(musicSystem, yOffsetSum, measures[idx].ParentStaff);
+        for (let i: number = 0; i < staffList.length; i++) {
+            this.addStaffLineToMusicSystem(musicSystem, yOffsetSum, staffList[i]);
             yOffsetSum += this.rules.StaffHeight;
-            let yOffset: number = 0;
-            // if (this.leadSheet && !multiLyrics) {
-            //     yOffset = 2.5;
-            // } else {
-            if (idx + 1 < measures.length &&
-                measures[idx].ParentStaff.ParentInstrument === measures[idx + 1].ParentStaff.ParentInstrument) {
+            if (i + 1 < staffList.length) {
+                let yOffset: number = 0;
+                if (this.leadSheet && !multiLyrics) {
+                    yOffset = 2.5;
+                } else {
+                    if (staffList[i].ParentInstrument === staffList[i + 1].ParentInstrument) {
                         yOffset = this.rules.BetweenStaffDistance;
                     } else {
                         yOffset = this.rules.StaffDistance;
                     }
-            // }
-            yOffsetSum += yOffset;
+                }
+                yOffsetSum += yOffset;
+            }
         }
         boundingBox.BorderBottom = yOffsetSum;
     }
@@ -321,8 +310,7 @@ export class MusicSystemBuilder {
             musicSystem.StaffLines.push(staffLine);
             const boundingBox: BoundingBox = staffLine.PositionAndShape;
             const relativePosition: PointF2D = new PointF2D();
-            if (musicSystem.Parent.MusicSystems[0] === musicSystem &&
-                musicSystem.Parent === musicSystem.Parent.Parent.MusicPages[0] &&
+            if (musicSystem === this.musicSystems[0] &&
                 !EngravingRules.Rules.CompactMode) {
                 relativePosition.x = this.rules.FirstSystemMargin;
                 boundingBox.BorderRight = musicSystem.PositionAndShape.Size.width - this.rules.FirstSystemMargin;
@@ -468,12 +456,7 @@ export class MusicSystemBuilder {
         let keyAdded: boolean = false;
         let rhythmAdded: boolean = false;
         if (currentClef !== undefined) {
-            if (measure.tabMeasure) {
-                measure.tabMeasure.addClefAtBegin(currentClef);
-                measure.addClefAtBegin(new ClefInstruction(ClefEnum.G));
-            } else {
-                measure.addClefAtBegin(currentClef);
-            }
+            measure.addClefAtBegin(currentClef);
             clefAdded = true;
         } else {
             currentClef = this.activeClefs[visibleStaffIdx];
@@ -486,9 +469,6 @@ export class MusicSystemBuilder {
         }
         if (currentRhythm !== undefined && currentRhythm.PrintObject) {
             measure.addRhythmAtBegin(currentRhythm);
-            if (measure.tabMeasure) {
-                measure.tabMeasure.addRhythmAtBegin(currentRhythm);
-            }
             rhythmAdded = true;
         }
         if (clefAdded || keyAdded || rhythmAdded) {
@@ -923,7 +903,143 @@ export class MusicSystemBuilder {
         }
         currentSystem.PositionAndShape.BorderRight = width + this.currentSystemParams.maxLabelLength + this.rules.SystemLabelsRightMargin;
     }
+
+    /**
+     * This method checks the distances between any two consecutive StaffLines of a System and if needed, shifts the lower one down.
+     * @param musicSystem
+     */
+    private optimizeDistanceBetweenStaffLines(musicSystem: MusicSystem): void {
+        // don't perform any y-spacing in case of a StaffEntryLink (in both StaffLines)
+        if (!musicSystem.checkStaffEntriesForStaffEntryLink()) {
+            for (let i: number = 0; i < musicSystem.StaffLines.length - 1; i++) {
+                const upperBottomLine: number = musicSystem.StaffLines[i].SkyBottomLineCalculator.getBottomLineMax();
+                // TODO: Lower skyline should add to offset when there are items above the line. Currently no test
+                // file available
+                // const lowerSkyLine: number = Math.min(...musicSystem.StaffLines[i + 1].SkyLine);
+                if (Math.abs(upperBottomLine) > this.rules.MinimumStaffLineDistance) {
+                    // Remove staffheight from offset. As it results in huge distances
+                    const offset: number = Math.abs(upperBottomLine) + this.rules.MinimumStaffLineDistance - this.rules.StaffHeight;
+                    this.updateStaffLinesRelativePosition(musicSystem, i + 1, offset);
+                }
+            }
+        }
+        const firstStaffLine: StaffLine = musicSystem.StaffLines[0];
+        musicSystem.PositionAndShape.BorderTop = firstStaffLine.PositionAndShape.RelativePosition.y + firstStaffLine.PositionAndShape.BorderTop;
+        const lastStaffLine: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
+        musicSystem.PositionAndShape.BorderBottom = lastStaffLine.PositionAndShape.RelativePosition.y + lastStaffLine.PositionAndShape.BorderBottom;
+    }
+
+    /**
+     * This method updates the System's StaffLine's RelativePosition (starting from the given index).
+     * @param musicSystem
+     * @param index
+     * @param value
+     */
+    private updateStaffLinesRelativePosition(musicSystem: MusicSystem, index: number, value: number): void {
+        for (let i: number = index; i < musicSystem.StaffLines.length; i++) {
+            musicSystem.StaffLines[i].PositionAndShape.RelativePosition.y += value;
+        }
+
+        musicSystem.PositionAndShape.BorderBottom += value;
+    }
+
+    /**
+     * Create a new [[GraphicalMusicPage]]
+     * (for now only one long page is used per music sheet, as we scroll down and have no page flips)
+     * @returns {GraphicalMusicPage}
+     */
+    private createMusicPage(): GraphicalMusicPage {
+        const page: GraphicalMusicPage = new GraphicalMusicPage(this.graphicalMusicSheet);
+        this.graphicalMusicSheet.MusicPages.push(page);
+        page.PageNumber = this.graphicalMusicSheet.MusicPages.length; // caution: page number = page index + 1
+        page.PositionAndShape.BorderLeft = 0.0;
+        page.PositionAndShape.BorderRight = this.graphicalMusicSheet.ParentMusicSheet.pageWidth;
+        page.PositionAndShape.BorderTop = 0.0;
+        page.PositionAndShape.BorderBottom = this.rules.PageHeight;
+        page.PositionAndShape.RelativePosition = new PointF2D(0.0, 0.0);
+        return page;
+    }
+
+    private addSystemToPage(page: GraphicalMusicPage, system: MusicSystem): void {
+        page.MusicSystems.push(system);
+        system.Parent = page;
+    }
+
+    /** Calculates the relative Positions of all MusicSystems.
+     *
+     */
+    private calculateMusicSystemsRelativePositions(): void {
+        let currentPage: GraphicalMusicPage = this.createMusicPage();
+        let currentYPosition: number = 0;
+        // xPosition is always fixed
+        let currentSystem: MusicSystem = this.musicSystems[0];
+        let timesPageCouldntFitSingleSystem: number = 0;
+
+        for (let i: number = 0; i < this.musicSystems.length; i++) {
+            currentSystem = this.musicSystems[i];
+            if (currentPage.MusicSystems.length === 0) {
+                // first system on the page:
+                this.addSystemToPage(currentPage, currentSystem);
+                if (EngravingRules.Rules.CompactMode) {
+                    currentYPosition = EngravingRules.Rules.PageTopMarginNarrow;
+                } else {
+                    currentYPosition = EngravingRules.Rules.PageTopMargin;
+                }
+
+                // Handle Title for first System on the first page
+                if (this.graphicalMusicSheet.MusicPages.length === 1 &&
+                    EngravingRules.Rules.RenderTitle) {
+                    currentYPosition +=   this.rules.TitleTopDistance + this.rules.SheetTitleHeight +
+                                            this.rules.TitleBottomDistance;
+                }
+                currentYPosition += -currentSystem.PositionAndShape.BorderTop;
+                const relativePosition: PointF2D = new PointF2D(this.rules.PageLeftMargin + this.rules.SystemLeftMargin,
+                                                                currentYPosition);
+                currentSystem.PositionAndShape.RelativePosition = relativePosition;
+                currentYPosition += currentSystem.PositionAndShape.BorderBottom;
+                if (currentYPosition > this.rules.PageHeight - this.rules.PageBottomMargin) { // can't fit single system on page, maybe PageFormat too small
+                    timesPageCouldntFitSingleSystem++;
+                    if (timesPageCouldntFitSingleSystem <= 4) { // only warn once with detailed info
+                        console.log(`warning: could not fit a single system on page ${currentPage.PageNumber}` +
+                            ` and measure number ${currentSystem.GraphicalMeasures[0][0].MeasureNumber}.
+                            The PageFormat may be too small for this sheet."
+                            Will not give further warnings for all pages, only total.`
+                        );
+                    }
+                }
+            } else {
+                // if this is not the first system on the page:
+                // find optimum distance between Systems
+                const previousSystem: MusicSystem = this.musicSystems[i - 1];
+                const previousStaffLineBB: BoundingBox = previousSystem.StaffLines[previousSystem.StaffLines.length - 1].PositionAndShape;
+                const currentStaffLineBB: BoundingBox = currentSystem.StaffLines[0].PositionAndShape;
+                let distance: number =  currentStaffLineBB.RelativePosition.y + previousStaffLineBB.BorderTop -
+                                        (previousStaffLineBB.RelativePosition.y + previousStaffLineBB.BorderBottom);
+                distance = Math.max(this.rules.MinimumDistanceBetweenSystems, distance);
+                const neededHeight: number = distance - currentSystem.PositionAndShape.BorderTop + currentSystem.PositionAndShape.BorderBottom;
+                if (currentYPosition + neededHeight <
+                    this.rules.PageHeight - this.rules.PageBottomMargin) {
+                    // enough space on this page:
+                    this.addSystemToPage(currentPage, currentSystem);
+                    const relativePosition: PointF2D = new PointF2D(this.rules.PageLeftMargin + this.rules.SystemLeftMargin,
+                                                                    currentYPosition + distance - currentSystem.PositionAndShape.BorderTop);
+                    currentSystem.PositionAndShape.RelativePosition = relativePosition;
+                    currentYPosition += neededHeight;
+                } else {
+                    // new page needed:
+                    currentPage = this.createMusicPage();
+                    // re-check this system again:
+                    i -= 1;
+                    continue;
+                }
+            }
+        }
+        if (timesPageCouldntFitSingleSystem > 0) {
+            console.log(`total amount of pages that couldn't fit a single music system: ${timesPageCouldntFitSingleSystem} of ${currentPage.PageNumber}`);
+        }
+    }
 }
+
 export class SystemBuildParameters {
     public currentSystem: MusicSystem;
     public systemMeasures: MeasureBuildParameters[] = [];

+ 2 - 2
src/MusicalScore/Graphical/SkyBottomLineCalculator.ts

@@ -147,7 +147,7 @@ export class SkyBottomLineCalculator {
         }
         // Remap the values from 0 to +/- height in units
         this.mSkyLine = this.mSkyLine.map(v => (v - Math.max(...this.mSkyLine)) / unitInPixels);
-        this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.mRules.StaffHeight);
+        this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.StaffLineParent.StaffHeight);
     }
 
     /**
@@ -409,7 +409,7 @@ export class SkyBottomLineCalculator {
                 const endPoint: number = Math.ceil(boundingBox.AbsolutePosition.x + boundingBox.BorderRight) ;
 
                 this.updateInRange(this.mSkyLine, startPoint, endPoint, currentTopBorder);
-            } else if (currentBottomBorder > this.mRules.StaffHeight) {
+            } else if (currentBottomBorder > this.StaffLineParent.StaffHeight) {
                 const startPoint: number = Math.floor(boundingBox.AbsolutePosition.x + boundingBox.BorderLeft);
                 const endPoint: number = Math.ceil(boundingBox.AbsolutePosition.x + boundingBox.BorderRight);
 

+ 11 - 7
src/MusicalScore/Graphical/StaffLine.ts

@@ -12,8 +12,8 @@ import {GraphicalLabel} from "./GraphicalLabel";
 import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
 import { GraphicalOctaveShift } from "./GraphicalOctaveShift";
 import { GraphicalSlur } from "./GraphicalSlur";
-import { AlignmentManager } from "./AlignmentManager";
 import { AbstractGraphicalExpression } from "./AbstractGraphicalExpression";
+import { EngravingRules } from "./EngravingRules";
 
 /**
  * A StaffLine contains the [[Measure]]s in one line of the music sheet
@@ -26,10 +26,11 @@ export abstract class StaffLine extends GraphicalObject {
     protected parentStaff: Staff;
     protected octaveShifts: GraphicalOctaveShift[] = [];
     protected skyBottomLine: SkyBottomLineCalculator;
-    protected alignmentManager: AlignmentManager;
     protected lyricLines: GraphicalLine[] = [];
     protected lyricsDashes: GraphicalLabel[] = [];
     protected abstractExpressions: AbstractGraphicalExpression[] = [];
+    /** The staff height in units */
+    private staffHeight: number;
 
     // For displaying Slurs
     protected graphicalSlurs: GraphicalSlur[] = [];
@@ -40,7 +41,10 @@ export abstract class StaffLine extends GraphicalObject {
         this.parentStaff = parentStaff;
         this.boundingBox = new BoundingBox(this, parentSystem.PositionAndShape);
         this.skyBottomLine = new SkyBottomLineCalculator(this);
-        this.alignmentManager = new AlignmentManager(this);
+        this.staffHeight = EngravingRules.Rules.StaffHeight;
+        if (this.parentStaff.isTab) {
+            this.staffHeight = EngravingRules.Rules.TabStaffHeight;
+        }
     }
 
     public get Measures(): GraphicalMeasure[] {
@@ -104,10 +108,6 @@ export abstract class StaffLine extends GraphicalObject {
         this.parentStaff = value;
     }
 
-    public get AlignmentManager(): AlignmentManager {
-        return this.alignmentManager;
-    }
-
     public get SkyBottomLineCalculator(): SkyBottomLineCalculator {
         return this.skyBottomLine;
     }
@@ -128,6 +128,10 @@ export abstract class StaffLine extends GraphicalObject {
         this.octaveShifts = value;
     }
 
+    public get StaffHeight(): number {
+        return this.staffHeight;
+    }
+
     // get all Graphical Slurs of a staffline
     public get GraphicalSlurs(): GraphicalSlur[] {
         return this.graphicalSlurs;

+ 6 - 6
src/MusicalScore/Graphical/AlignmentManager.ts → src/MusicalScore/Graphical/VexFlow/AlignmentManager.ts

@@ -1,9 +1,9 @@
-import { StaffLine } from "./StaffLine";
-import { BoundingBox } from "./BoundingBox";
-import { VexFlowContinuousDynamicExpression } from "./VexFlow/VexFlowContinuousDynamicExpression";
-import { AbstractGraphicalExpression } from "./AbstractGraphicalExpression";
-import { PointF2D } from "../../Common/DataObjects/PointF2D";
-import { EngravingRules } from "./EngravingRules";
+import { StaffLine } from "../StaffLine";
+import { BoundingBox } from "../BoundingBox";
+import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression";
+import { AbstractGraphicalExpression } from "../AbstractGraphicalExpression";
+import { PointF2D } from "../../../Common/DataObjects/PointF2D";
+import { EngravingRules } from "../EngravingRules";
 
 export class AlignmentManager {
     private parentStaffline: StaffLine;

+ 29 - 4
src/MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend.ts

@@ -6,21 +6,32 @@ import {Fonts} from "../../../Common/Enums/Fonts";
 import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
 import {PointF2D} from "../../../Common/DataObjects/PointF2D";
 import {VexFlowConverter} from "./VexFlowConverter";
+import {BackendType} from "../../../OpenSheetMusicDisplay";
+import {EngravingRules} from "../EngravingRules";
+import {GraphicalMusicPage} from "../GraphicalMusicPage";
 
 export class CanvasVexFlowBackend extends VexFlowBackend {
-
-    public getBackendType(): number {
+    public getVexflowBackendType(): Vex.Flow.Renderer.Backends {
         return Vex.Flow.Renderer.Backends.CANVAS;
     }
 
+    public getOSMDBackendType(): BackendType {
+        return BackendType.Canvas;
+    }
+
     public initialize(container: HTMLElement): void {
         this.canvas = document.createElement("canvas");
+        if (!this.graphicalMusicPage) {
+            this.graphicalMusicPage = new GraphicalMusicPage(undefined);
+            this.graphicalMusicPage.PageNumber = 1;
+        }
+        this.canvas.id = "osmdCanvasVexFlowBackendCanvas" + this.graphicalMusicPage.PageNumber; // needed to extract image buffer from js
         this.inner = document.createElement("div");
         this.inner.style.position = "relative";
         this.canvas.style.zIndex = "0";
         this.inner.appendChild(this.canvas);
         container.appendChild(this.inner);
-        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getBackendType());
+        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getVexflowBackendType());
         this.ctx = <Vex.Flow.CanvasContext>this.renderer.getContext();
     }
 
@@ -30,10 +41,15 @@ export class CanvasVexFlowBackend extends VexFlowBackend {
      * @param height Height of the canvas
      */
     public initializeHeadless(width: number = 300, height: number = 300): void {
+        if (!this.graphicalMusicPage) {
+            // not needed here yet, but just for future safety, make sure the page isn't undefined
+            this.graphicalMusicPage = new GraphicalMusicPage(undefined);
+            this.graphicalMusicPage.PageNumber = 1;
+        }
         this.canvas = document.createElement("canvas");
         (this.canvas as any).width = width;
         (this.canvas as any).height = height;
-        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getBackendType());
+        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getVexflowBackendType());
         this.ctx = <Vex.Flow.CanvasContext>this.renderer.getContext();
     }
 
@@ -43,6 +59,15 @@ export class CanvasVexFlowBackend extends VexFlowBackend {
 
     public clear(): void {
         (<any>this.ctx).clearRect(0, 0, (<any>this.canvas).width, (<any>this.canvas).height);
+
+        // set background color if not transparent
+        if (EngravingRules.Rules.PageBackgroundColor !== undefined) {
+            this.ctx.save();
+            // note that this will hide the cursor
+            this.ctx.setFillStyle(EngravingRules.Rules.PageBackgroundColor);
+            this.ctx.fillRect(0, 0, (this.canvas as any).width, (this.canvas as any).height);
+            this.ctx.restore();
+        }
     }
 
     public scale(k: number): void {

+ 22 - 4
src/MusicalScore/Graphical/VexFlow/SvgVexFlowBackend.ts

@@ -6,31 +6,39 @@ import {FontStyles} from "../../../Common/Enums/FontStyles";
 import {Fonts} from "../../../Common/Enums/Fonts";
 import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
 import {PointF2D} from "../../../Common/DataObjects/PointF2D";
-import { EngravingRules } from "..";
+import {EngravingRules} from "..";
+import {BackendType} from "../../../OpenSheetMusicDisplay";
 
 export class SvgVexFlowBackend extends VexFlowBackend {
 
     private ctx: Vex.Flow.SVGContext;
 
-    public getBackendType(): number {
+    public getVexflowBackendType(): Vex.Flow.Renderer.Backends {
         return Vex.Flow.Renderer.Backends.SVG;
     }
 
+    public getOSMDBackendType(): BackendType {
+        return BackendType.SVG;
+    }
+
     public initialize(container: HTMLElement): void {
         this.canvas = document.createElement("div");
         this.inner = this.canvas;
         this.inner.style.position = "relative";
         this.canvas.style.zIndex = "0";
         container.appendChild(this.inner);
-        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getBackendType());
+        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getVexflowBackendType());
         this.ctx = <Vex.Flow.SVGContext>this.renderer.getContext();
-
     }
 
     public getContext(): Vex.Flow.SVGContext {
         return this.ctx;
     }
 
+    public getSvgElement(): SVGElement {
+        return this.ctx.svg;
+    }
+
     public clear(): void {
         if (!this.ctx) {
             return;
@@ -42,6 +50,16 @@ export class SvgVexFlowBackend extends VexFlowBackend {
         while (svg.lastChild) {
             svg.removeChild(svg.lastChild);
         }
+
+        // set background color if not transparent
+        if (EngravingRules.Rules.PageBackgroundColor !== undefined) {
+            this.ctx.save();
+            // note that this will hide the cursor
+            this.ctx.setFillStyle(EngravingRules.Rules.PageBackgroundColor);
+
+            this.ctx.fillRect(0, 0, this.canvas.offsetWidth, this.canvas.offsetHeight);
+            this.ctx.restore();
+        }
     }
 
     public scale(k: number): void {

+ 38 - 2
src/MusicalScore/Graphical/VexFlow/VexFlowBackend.ts

@@ -3,6 +3,8 @@ import {FontStyles} from "../../../Common/Enums/FontStyles";
 import {Fonts} from "../../../Common/Enums/Fonts";
 import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
 import {PointF2D} from "../../../Common/DataObjects/PointF2D";
+import {GraphicalMusicPage} from "..";
+import {BackendType} from "../../../OpenSheetMusicDisplay";
 
 export class VexFlowBackends {
   public static CANVAS: 0;
@@ -14,6 +16,9 @@ export class VexFlowBackends {
 
 export abstract class VexFlowBackend {
 
+  /** The GraphicalMusicPage the backend is drawing from. Each backend only renders one GraphicalMusicPage, to which the coordinates are relative. */
+  public graphicalMusicPage: GraphicalMusicPage;
+
   public abstract initialize(container: HTMLElement): void;
 
   public getInnerElement(): HTMLElement {
@@ -28,7 +33,33 @@ export abstract class VexFlowBackend {
     return this.renderer;
   }
 
-  public abstract getContext(): Vex.IRenderContext;
+  public removeAllChildrenFromContainer(container: HTMLElement): void {
+    while (container.children.length !== 0) {
+      container.removeChild(container.children.item(0));
+    }
+  }
+
+  // note: removing single children to remove all is error-prone, because sometimes a random SVG-child remains.
+  public removeFromContainer(container: HTMLElement): void {
+    //console.log("backend type: " + this.getVexflowBackendType());
+    let htmlElementToRemove: HTMLElement = this.canvas; // for SVGBackend
+    if (this.getVexflowBackendType() === Vex.Flow.Renderer.Backends.CANVAS) {
+      htmlElementToRemove = this.inner;
+      // for SVG, this.canvas === this.inner, but for Canvas, removing this.canvas causes an error because it's not a child of container,
+      // so we have to remove this.inner instead.
+    }
+
+    // only remove child if the container has this child, otherwise it will throw an error.
+    for (let i: number = 0; i < container.children.length; i++) {
+      if (container.children.item(i) === htmlElementToRemove) {
+        container.removeChild(htmlElementToRemove);
+        break;
+      }
+    }
+    // there is unfortunately no built-in container.hasChild(child) method.
+  }
+
+public abstract getContext(): Vex.IRenderContext;
 
   // public abstract setWidth(width: number): void;
   // public abstract setHeight(height: number): void;
@@ -58,7 +89,12 @@ export abstract class VexFlowBackend {
 
   public abstract renderCurve(points: PointF2D[]): void;
 
-  public abstract getBackendType(): number;
+  public abstract getVexflowBackendType(): Vex.Flow.Renderer.Backends;
+
+  /** The general type of backend: Canvas or SVG.
+   * This is not used for now (only VexflowBackendType used), but it may be useful when we don't want to use a Vexflow class.
+   */
+  public abstract getOSMDBackendType(): BackendType;
 
   protected renderer: Vex.Flow.Renderer;
   protected inner: HTMLElement;

+ 22 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -25,6 +25,7 @@ import { unitInPixels } from "./VexFlowMusicSheetDrawer";
 import { EngravingRules } from "../EngravingRules";
 import { Note } from "../..";
 import StaveNote = Vex.Flow.StaveNote;
+import { ArpeggioType } from "../../VoiceData";
 import { TabNote } from "../../VoiceData/TabNote";
 
 /**
@@ -465,6 +466,27 @@ export class VexFlowConverter {
         }
     }
 
+    public static StrokeTypeFromArpeggioType(arpeggioType: ArpeggioType): Vex.Flow.Stroke.Type {
+        switch (arpeggioType) {
+            case ArpeggioType.ARPEGGIO_DIRECTIONLESS:
+                return Vex.Flow.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
+            case ArpeggioType.BRUSH_DOWN:
+                return Vex.Flow.Stroke.Type.BRUSH_UP; // TODO somehow up and down are mixed up in Vexflow right now
+            case ArpeggioType.BRUSH_UP:
+                return Vex.Flow.Stroke.Type.BRUSH_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
+            case ArpeggioType.RASQUEDO_DOWN:
+                return Vex.Flow.Stroke.Type.RASQUEDO_UP;
+            case ArpeggioType.RASQUEDO_UP:
+                return Vex.Flow.Stroke.Type.RASQUEDO_DOWN;
+            case ArpeggioType.ROLL_DOWN:
+                return Vex.Flow.Stroke.Type.ROLL_UP; // TODO somehow up and down are mixed up in Vexflow right now
+            case ArpeggioType.ROLL_UP:
+                return Vex.Flow.Stroke.Type.ROLL_DOWN; // TODO somehow up and down are mixed up in Vexflow right now
+            default:
+                return Vex.Flow.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
+        }
+    }
+
     /**
      * Convert a set of GraphicalNotes to a VexFlow StaveNote
      * @param notes form a chord on the staff

+ 2 - 3
src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalSymbolFactory.ts

@@ -1,6 +1,5 @@
 import Vex = require("vexflow");
 import {IGraphicalSymbolFactory} from "../../Interfaces/IGraphicalSymbolFactory";
-import {GraphicalMusicPage} from "../GraphicalMusicPage";
 import {MusicSystem} from "../MusicSystem";
 import {VexFlowMusicSystem} from "./VexFlowMusicSystem";
 import {Staff} from "../../VoiceData/Staff";
@@ -37,8 +36,8 @@ export class VexFlowGraphicalSymbolFactory implements IGraphicalSymbolFactory {
      * @param systemIndex
      * @returns {VexFlowMusicSystem}
      */
-    public createMusicSystem(page: GraphicalMusicPage, systemIndex: number): MusicSystem {
-        return new VexFlowMusicSystem(page, systemIndex);
+    public createMusicSystem(systemIndex: number): MusicSystem {
+        return new VexFlowMusicSystem(systemIndex);
     }
 
     /**

+ 1 - 7
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -34,7 +34,6 @@ import {PlacementEnum} from "../../VoiceData/Expressions/AbstractExpression";
 import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
 import {AutoBeamOptions} from "../../../OpenSheetMusicDisplay/OSMDOptions";
 import {NoteType, Arpeggio} from "../../VoiceData";
-import { VexFlowTabMeasure } from "./VexFlowTabMeasure";
 
 export class VexFlowMeasure extends GraphicalMeasure {
     constructor(staff: Staff, sourceMeasure: SourceMeasure = undefined, staffLine: StaffLine = undefined) {
@@ -111,11 +110,6 @@ export class VexFlowMeasure extends GraphicalMeasure {
         this.connectors = [];
         // Clean up instructions
         this.resetLayout();
-
-        // clean also tab measure if present:
-        if (this.tabMeasure !== undefined) {
-            (this.tabMeasure as VexFlowTabMeasure).clean();
-        }
     }
 
     /**
@@ -953,7 +947,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
                     // TODO right now our arpeggio object has all arpeggio notes from arpeggios across all voices.
                     // see VoiceGenerator. Doesn't matter for Vexflow for now though
                     if (voiceEntry.notes && voiceEntry.notes.length > 1) {
-                        const type: Vex.Flow.Stroke.Type = arpeggio.type;
+                        const type: Vex.Flow.Stroke.Type = VexFlowConverter.StrokeTypeFromArpeggioType(arpeggio.type);
                         const stroke: Vex.Flow.Stroke = new Vex.Flow.Stroke(type, {
                             all_voices: EngravingRules.Rules.ArpeggiosGoAcrossVoices
                             // default: false. This causes arpeggios to always go across all voices, which is often unwanted.

+ 27 - 107
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -47,7 +47,7 @@ import { ContinuousDynamicExpression } from "../../VoiceData/Expressions/Continu
 import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression";
 import { InstantaneousTempoExpression } from "../../VoiceData/Expressions";
 import { AlignRestOption } from "../../../OpenSheetMusicDisplay";
-import { VexFlowTabMeasure } from "./VexFlowTabMeasure";
+import { VexFlowStaffLine } from "./VexFlowStaffLine";
 
 export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   /** space needed for a dash for lyrics spacing, calculated once */
@@ -99,21 +99,12 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
    * @returns the minimum required x width of the source measure (=list of staff measures)
    */
   protected calculateMeasureXLayout(measures: GraphicalMeasure[]): number {
-    // include tab measures:
     const visibleMeasures: GraphicalMeasure[] = [];
     for (const measure of measures) {
       visibleMeasures.push(measure);
-      if (measure.tabMeasure) {
-        visibleMeasures.push(measure.tabMeasure);
-      }
     }
     measures = visibleMeasures;
 
-    // Finalize beams
-    /*for (let measure of measures) {
-     (measure as VexFlowMeasure).finalizeBeams();
-     (measure as VexFlowMeasure).finalizeTuplets();
-     }*/
     // Format the voices
     const allVoices: Vex.Flow.Voice[] = [];
     const formatter: Vex.Flow.Formatter = new Vex.Flow.Formatter();
@@ -127,22 +118,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
           allVoices.push(mvoices[voiceID]);
         }
       }
-      if (measure.tabMeasure !== undefined) {
-        const tabVoices: { [voiceID: number]: Vex.Flow.Voice; } = (measure.tabMeasure as VexFlowTabMeasure).vfVoices;
-        const tvoices: Vex.Flow.Voice[] = [];
-        for (const voiceID in tabVoices) {
-          if (tabVoices.hasOwnProperty(voiceID)) {
-            tvoices.push(tabVoices[voiceID]);
-            allVoices.push(tabVoices[voiceID]);
-          }
-        }
-        if (tvoices.length === 0) {
-          log.info("Found a tab measure with no voices. Continuing anyway.", tabVoices);
-          continue;
-        }
-        // all voices that belong to one stave are collectively added to create a common context in VexFlow.
-        formatter.joinVoices(tvoices);
-      }
+
       if (voices.length === 0) {
         log.debug("Found a measure with no voices. Continuing anyway.", mvoices);
         // no need to log this, measures with no voices/notes are fine. see OSMDOptions.fillEmptyMeasuresWithWholeRest
@@ -395,9 +371,6 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
 
   protected graphicalMeasureCreatedCalculations(measure: GraphicalMeasure): void {
     (measure as VexFlowMeasure).graphicalMeasureCreatedCalculations();
-    if (measure.tabMeasure !== undefined) {
-      (measure.tabMeasure as VexFlowTabMeasure).graphicalMeasureCreatedCalculations();
-    }
   }
 
   /**
@@ -424,21 +397,6 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   }
 
   /**
-   * calculates the y positions of the staff lines within a system and
-   * furthermore the y positions of the systems themselves.
-   */
-  protected calculateSystemYLayout(): void {
-    for (const graphicalMusicPage of this.graphicalMusicSheet.MusicPages) {
-      for (const musicSystem of graphicalMusicPage.MusicSystems) {
-        this.optimizeDistanceBetweenStaffLines(musicSystem);
-      }
-
-      // set y positions of systems using the previous system and a fixed distance.
-      this.calculateMusicSystemsRelativePositions(graphicalMusicPage);
-    }
-  }
-
-  /**
    * Is called at the begin of the method for creating the vertically aligned staff measures belonging to one source measure.
    */
   protected initGraphicalMeasuresCreation(): void {
@@ -548,7 +506,13 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
       graphicalContinuousDynamic.StartMeasure = startMeasure;
 
       if (!graphicalContinuousDynamic.IsVerbal && continuousDynamic.EndMultiExpression) {
+        try {
         this.calculateGraphicalContinuousDynamic(graphicalContinuousDynamic, dynamicStartPosition);
+        } catch (e) {
+          // TODO this sometimes fails when the measure range to draw doesn't include all the dynamic's measures, method needs to be adjusted
+          //   see calculateGraphicalContinuousDynamic(), also in MusicSheetCalculator.
+
+        }
       } else if (graphicalContinuousDynamic.IsVerbal) {
         this.calculateGraphicalVerbalContinuousDynamic(graphicalContinuousDynamic, dynamicStartPosition);
       } else {
@@ -715,6 +679,23 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
   }
 
   /**
+   * Re-adjust the x positioning of expressions. Update the skyline afterwards
+   */
+  protected calculateExpressionAlignements(): void {
+    for (const musicSystem of this.musicSystems) {
+            for (const staffLine of musicSystem.StaffLines) {
+              try {
+                (<VexFlowStaffLine>staffLine).AlignmentManager.alignDynamicExpressions();
+                staffLine.AbstractExpressions.forEach(ae => ae.updateSkyBottomLine());
+              } catch (e) {
+                // TODO still necessary when calculation of expression fails, see calculateDynamicExpressionsForMultiExpression()
+                //   see calculateGraphicalContinuousDynamic(), also in MusicSheetCalculator.
+        }
+    }
+  }
+  }
+
+  /**
    * Check if the tied graphical note belongs to any beams or tuplets and react accordingly.
    * @param tiedGraphicalNote
    * @param beams
@@ -865,8 +846,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
     */
 
-    for (const gmPage of this.graphicalMusicSheet.MusicPages) {
-      for (const musicSystem of gmPage.MusicSystems) {
+    for (const musicSystem of this.musicSystems) {
         for (const staffLine of musicSystem.StaffLines) {
           // if a graphical slur reaches out of the last musicsystem, we have to create another graphical slur reaching into this musicsystem
           // (one slur needs 2 graphical slurs)
@@ -893,34 +873,6 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
           // add reference of slur array to the VexFlowStaffline class
           for (const graphicalMeasure of staffLine.Measures) {
             for (const graphicalStaffEntry of graphicalMeasure.staffEntries) {
-              // for (var idx5: number = 0, len5 = graphicalStaffEntry.GraceStaffEntriesBefore.Count; idx5 < len5; ++idx5) {
-              //     var graceStaffEntry: GraphicalStaffEntry = graphicalStaffEntry.GraceStaffEntriesBefore[idx5];
-              //     if (graceStaffEntry.Notes[0][0].SourceNote.NoteSlurs.Count > 0) {
-              //         var graceNote: Note = graceStaffEntry.Notes[0][0].SourceNote;
-              //         graceStaffEntry.RelInMeasureTimestamp = Fraction.createFromFraction(graphicalStaffEntry.RelInMeasureTimestamp);
-              //         for (var idx6: number = 0, len6 = graceNote.NoteSlurs.Count; idx6 < len6; ++idx6) {
-              //             var graceNoteSlur: Slur = graceNote.NoteSlurs[idx6];
-              //             if (graceNoteSlur.StartNote == graceNote) {
-              //                 var vfSlur: VexFlowSlur = new VexFlowSlur(graceNoteSlur);
-              //                 vfSlur.GraceStart = true;
-              //                 staffLine.GraphicalSlurs.Add(vfSlur);
-              //                 openGraphicalSlurs[i].Add(vfSlur);
-              //                 for (var j: number = graphicalStaffEntry.GraceStaffEntriesBefore.IndexOf(graceStaffEntry);
-              //                     j < graphicalStaffEntry.GraceStaffEntriesBefore.Count; j++)
-              //                        vfSlur.StaffEntries.Add(<PsStaffEntry>graphicalStaffEntry.GraceStaffEntriesBefore[j]);
-              //             }
-              //             if (graceNote == graceNoteSlur.EndNote) {
-              //                 var vfSlur: VexFlowSlur = findGraphicalSlurFromSlur(openGraphicalSlurs[i], graceNoteSlur);
-              //                 if (vfSlur != null) {
-              //                     vfSlur.GraceEnd = true;
-              //                     openGraphicalSlurs[i].Remove(vfSlur);
-              //                     for (var j: number = 0; j <= graphicalStaffEntry.GraceStaffEntriesBefore.IndexOf(graceStaffEntry); j++)
-              //                         vfSlur.StaffEntries.Add(<PsStaffEntry>graphicalStaffEntry.GraceStaffEntriesBefore[j]);
-              //                 }
-              //             }
-              //         }
-              //     }
-              // }
               // loop over "normal" notes (= no gracenotes)
               for (const graphicalVoiceEntry of graphicalStaffEntry.graphicalVoiceEntries) {
                 for (const graphicalNote of graphicalVoiceEntry.notes) {
@@ -977,35 +929,6 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
                   }
                 }
               }
-              // for (var idx5: number = 0, len5 = graphicalStaffEntry.GraceStaffEntriesAfter.Count; idx5 < len5; ++idx5) {
-              //     var graceStaffEntry: GraphicalStaffEntry = graphicalStaffEntry.GraceStaffEntriesAfter[idx5];
-              //     if (graceStaffEntry.Notes[0][0].SourceNote.NoteSlurs.Count > 0) {
-              //         var graceNote: Note = graceStaffEntry.Notes[0][0].SourceNote;
-              //         graceStaffEntry.RelInMeasureTimestamp = Fraction.createFromFraction(graphicalStaffEntry.RelInMeasureTimestamp);
-              //         for (var idx6: number = 0, len6 = graceNote.NoteSlurs.Count; idx6 < len6; ++idx6) {
-              //             var graceNoteSlur: Slur = graceNote.NoteSlurs[idx6];
-              //             if (graceNoteSlur.StartNote == graceNote) {
-              //                 var vfSlur: VexFlowSlur = new VexFlowSlur(graceNoteSlur);
-              //                 vfSlur.GraceStart = true;
-              //                 staffLine.GraphicalSlurs.Add(vfSlur);
-              //                 openGraphicalSlurs[i].Add(vfSlur);
-              //                 for (var j: number = graphicalStaffEntry.GraceStaffEntriesAfter.IndexOf(graceStaffEntry);
-              //                      j < graphicalStaffEntry.GraceStaffEntriesAfter.Count; j++)
-              //                        vfSlur.StaffEntries.Add(<PsStaffEntry>graphicalStaffEntry.GraceStaffEntriesAfter[j]);
-              //             }
-              //             if (graceNote == graceNoteSlur.EndNote) {
-              //                 var vfSlur: VexFlowSlur = findGraphicalSlurFromSlur(openGraphicalSlurs[i], graceNoteSlur);
-              //                 if (vfSlur != null) {
-              //                     vfSlur.GraceEnd = true;
-              //                     openGraphicalSlurs[i].Remove(vfSlur);
-              //                     vfSlur.StaffEntries.Add(<PsStaffEntry>graphicalStaffEntry);
-              //                     for (var j: number = 0; j <= graphicalStaffEntry.GraceStaffEntriesAfter.IndexOf(graceStaffEntry); j++)
-              //                         vfSlur.StaffEntries.Add(<PsStaffEntry>graphicalStaffEntry.GraceStaffEntriesAfter[j]);
-              //                 }
-              //             }
-              //         }
-              //     }
-              // }
 
               //add the present Staffentry to all open slurs that don't contain this Staffentry already
               for (const gSlur of openGraphicalSlurs) {
@@ -1020,11 +943,9 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         // Attach vfSlur array to the vfStaffline to be drawn
         //vfStaffLine.SlursInVFStaffLine = vfSlurs;
       } // loop over MusicSystems
-    } // loop over MusicPages
 
     // order slurs that were saved to the Staffline
-    for (const graphicalMusicPage of this.graphicalMusicSheet.MusicPages) {
-        for (const musicSystem of graphicalMusicPage.MusicSystems) {
+    for (const musicSystem of this.musicSystems) {
           for (const staffLine of musicSystem.StaffLines) {
             // Sort all gSlurs in the staffline using the Compare function in class GraphicalSlurSorter
             const sortedGSlurs: GraphicalSlur[] = staffLine.GraphicalSlurs.sort(GraphicalSlur.Compare);
@@ -1039,4 +960,3 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         }
       }
   }
-}

+ 61 - 23
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -26,6 +26,8 @@ import log = require("loglevel");
 import { GraphicalContinuousDynamicExpression } from "../GraphicalContinuousDynamicExpression";
 import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression";
 import { DrawingParameters } from "../DrawingParameters";
+import { GraphicalMusicPage } from "../GraphicalMusicPage";
+import { GraphicalMusicSheet } from "../GraphicalMusicSheet";
 
 /**
  * This is a global constant which denotes the height in pixels of the space between two lines of the stave
@@ -36,39 +38,49 @@ export const unitInPixels: number = 10;
 
 export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
     private backend: VexFlowBackend;
+    private backends: VexFlowBackend[] = [];
     private zoom: number = 1.0;
+    private pageIdx: number = 0; // this is a bad solution, should use MusicPage.PageNumber instead.
 
-    constructor(element: HTMLElement,
-                backend: VexFlowBackend,
-                drawingParameters: DrawingParameters = new DrawingParameters()) {
+    constructor(drawingParameters: DrawingParameters = new DrawingParameters()) {
         super(new VexFlowTextMeasurer(), drawingParameters);
-        this.backend = backend;
     }
 
-    public clear(): void {
-        this.backend.clear();
+    public get Backends(): VexFlowBackend[] {
+        return this.backends;
     }
 
-    /**
-     * Zoom the rendering areas
-     * @param k is the zoom factor
-     */
-    public scale(k: number): void {
-        this.zoom = k;
-        this.backend.scale(this.zoom);
+    public drawSheet(graphicalMusicSheet: GraphicalMusicSheet): void {
+        this.pageIdx = 0;
+        for (const graphicalMusicPage of graphicalMusicSheet.MusicPages) {
+            const backend: VexFlowBackend = this.backends[this.pageIdx];
+            backend.graphicalMusicPage = graphicalMusicPage;
+            backend.scale(this.zoom);
+            //backend.resize(graphicalMusicSheet.ParentMusicSheet.pageWidth * unitInPixels * this.zoom,
+            //               EngravingRules.Rules.PageHeight * unitInPixels * this.zoom);
+            this.pageIdx += 1;
+        }
+
+        this.pageIdx = 0;
+        this.backend = this.backends[0];
+        super.drawSheet(graphicalMusicSheet);
     }
 
-    /**
-     * Resize the rendering areas
-     * @param x
-     * @param y
-     */
-    public resize(x: number, y: number): void {
-        this.backend.resize(x, y);
+    protected drawPage(page: GraphicalMusicPage): void {
+        this.backend = this.backends[page.PageNumber - 1]; // TODO we may need to set this in a couple of other places. this.pageIdx is a bad solution
+        super.drawPage(page);
+        this.pageIdx += 1;
+        this.backend = this.backends[this.pageIdx];
     }
 
-    public translate(x: number, y: number): void {
-        this.backend.translate(x, y);
+    public clear(): void {
+        for (const backend of this.backends) {
+            backend.clear();
+        }
+    }
+
+    public setZoom(zoom: number): void {
+        this.zoom = zoom;
     }
 
     /**
@@ -155,12 +167,38 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
     //     ctx.fillStyle = oldStyle;
     // }
 
-    public drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 0.2): void {
+    /** Draws a line in the current backend. Only usable while pages are drawn sequentially, because backend reference is updated in that process.
+     *  To add your own lines after rendering, use DrawOverlayLine.
+     */
+    protected drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 0.2): void {
+        // TODO maybe the backend should be given as an argument here as well, otherwise this can't be used after rendering of multiple pages is done.
         start = this.applyScreenTransformation(start);
         stop = this.applyScreenTransformation(stop);
+        /*if (!this.backend) {
+            this.backend = this.backends[0];
+        }*/
         this.backend.renderLine(start, stop, color, lineWidth * unitInPixels);
     }
 
+    /** Lets a user/developer draw an overlay line on the score. Use this instead of drawLine, which is for OSMD internally only.
+     *  The MusicPage has to be specified, because each page and Vexflow backend has its own relative coordinates.
+     *  (the AbsolutePosition of a GraphicalNote is relative to its backend)
+     *  To get a MusicPage, use GraphicalNote.ParentMusicPage.
+     */
+    public DrawOverlayLine(start: PointF2D, stop: PointF2D, musicPage: GraphicalMusicPage,
+                           color: string = "#FF0000FF", lineWidth: number = 0.2): void {
+        if (!musicPage.PageNumber || musicPage.PageNumber > this.backends.length || musicPage.PageNumber < 1) {
+            console.log("VexFlowMusicSheetDrawer.drawOverlayLine: invalid page number / music page number doesn't correspond to an existing backend.");
+            return;
+        }
+        const musicPageIndex: number = musicPage.PageNumber - 1;
+        const backendToUse: VexFlowBackend = this.backends[musicPageIndex];
+
+        start = this.applyScreenTransformation(start);
+        stop = this.applyScreenTransformation(stop);
+        backendToUse.renderLine(start, stop, color, lineWidth * unitInPixels);
+    }
+
     protected drawSkyLine(staffline: StaffLine): void {
         const startPosition: PointF2D = staffline.PositionAndShape.AbsolutePosition;
         const width: number = staffline.PositionAndShape.Size.width;

+ 2 - 3
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSystem.ts

@@ -1,5 +1,4 @@
 import {MusicSystem} from "../MusicSystem";
-import {GraphicalMusicPage} from "../GraphicalMusicPage";
 import {SystemLinesEnum} from "../SystemLinesEnum";
 import {SystemLinePosition} from "../SystemLinePosition";
 import {GraphicalMeasure} from "../GraphicalMeasure";
@@ -14,8 +13,8 @@ import { VexFlowInstrumentBrace } from "./VexFlowInstrumentBrace";
 import { SkyBottomLineCalculator } from "../SkyBottomLineCalculator";
 
 export class VexFlowMusicSystem extends MusicSystem {
-    constructor(parent: GraphicalMusicPage, id: number) {
-        super(parent, id);
+    constructor(id: number) {
+        super(id);
 
     }
 

+ 6 - 2
src/MusicalScore/Graphical/VexFlow/VexFlowStaffEntry.ts

@@ -30,11 +30,15 @@ export class VexFlowStaffEntry extends GraphicalStaffEntry {
         for (const gve of this.graphicalVoiceEntries as VexFlowVoiceEntry[]) {
             if (gve.vfStaveNote) {
                 gve.vfStaveNote.setStave(stave);
-                if (!gve.vfStaveNote.preFormatted || gve.vfStaveNote.getBoundingBox() === null) {
+                if (!gve.vfStaveNote.preFormatted) {
                     continue;
                 }
                 gve.applyBordersFromVexflow();
-                this.PositionAndShape.RelativePosition.x = gve.vfStaveNote.getBoundingBox().getX() / unitInPixels;
+                if (this.parentMeasure.ParentStaff.isTab) {
+                    this.PositionAndShape.RelativePosition.x = (gve.vfStaveNote.getAbsoluteX() + (<any>gve.vfStaveNote).glyph.getWidth()) / unitInPixels;
+                } else {
+                    this.PositionAndShape.RelativePosition.x = gve.vfStaveNote.getAbsoluteX() / unitInPixels;
+                }
                 const sourceNote: Note = gve.notes[0].sourceNote;
                 if (sourceNote.isRest() && sourceNote.Length.RealValue === this.parentMeasure.parentSourceMeasure.ActiveTimeSignature.RealValue) {
                     // whole rest: length = measure length. (4/4 in a 4/4 time signature, 3/4 in a 3/4 time signature, 1/4 in a 1/4 time signature, etc.)

+ 7 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowStaffLine.ts

@@ -2,13 +2,16 @@ import {StaffLine} from "../StaffLine";
 import {MusicSystem} from "../MusicSystem";
 import {Staff} from "../../VoiceData/Staff";
 import { VexFlowSlur } from "./VexFlowSlur";
+import { AlignmentManager } from "./AlignmentManager";
 
 export class VexFlowStaffLine extends StaffLine {
     constructor(parentSystem: MusicSystem, parentStaff: Staff) {
         super(parentSystem, parentStaff);
+        this.alignmentManager = new AlignmentManager(this);
     }
 
     protected slursInVFStaffLine: VexFlowSlur[] = [];
+    protected alignmentManager: AlignmentManager;
 
     public get SlursInVFStaffLine(): VexFlowSlur[] {
         return this.slursInVFStaffLine;
@@ -16,4 +19,8 @@ export class VexFlowStaffLine extends StaffLine {
     public addVFSlurToVFStaffline(vfSlur: VexFlowSlur): void {
         this.slursInVFStaffLine.push(vfSlur);
     }
+
+    public get AlignmentManager(): AlignmentManager {
+        return this.alignmentManager;
+    }
 }

+ 1 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowTabMeasure.ts

@@ -90,7 +90,7 @@ export class VexFlowTabMeasure extends VexFlowMeasure {
                     // TODO right now our arpeggio object has all arpeggio notes from arpeggios across all voices.
                     // see VoiceGenerator. Doesn't matter for Vexflow for now though
                     if (voiceEntry.notes && voiceEntry.notes.length > 1) {
-                        const type: Vex.Flow.Stroke.Type = arpeggio.type;
+                        const type: Vex.Flow.Stroke.Type = VexFlowConverter.StrokeTypeFromArpeggioType(arpeggio.type);
                         const stroke: Vex.Flow.Stroke = new Vex.Flow.Stroke(type, {
                             all_voices: EngravingRules.Rules.ArpeggiosGoAcrossVoices
                             // default: false. This causes arpeggios to always go across all voices, which is often unwanted.

+ 126 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowVoiceEntry.ts

@@ -3,6 +3,11 @@ import { VoiceEntry } from "../../VoiceData/VoiceEntry";
 import { GraphicalVoiceEntry } from "../GraphicalVoiceEntry";
 import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
 import { unitInPixels } from "./VexFlowMusicSheetDrawer";
+import { EngravingRules } from "../EngravingRules";
+import { GraphicalNote } from "..";
+import { NoteEnum } from "../../../Common/DataObjects/Pitch";
+import { Note } from "../../VoiceData/Note";
+import { ColoringModes } from "./../DrawingParameters";
 
 export class VexFlowVoiceEntry extends GraphicalVoiceEntry {
     private mVexFlowStaveNote: Vex.Flow.StemmableNote;
@@ -33,4 +38,125 @@ export class VexFlowVoiceEntry extends GraphicalVoiceEntry {
     public get vfStaveNote(): Vex.Flow.StemmableNote {
         return this.mVexFlowStaveNote;
     }
+
+    /** (Re-)color notes and stems by setting their Vexflow styles.
+     * Could be made redundant by a Vexflow PR, but Vexflow needs more solid and permanent color methods/variables for that
+     * See VexFlowConverter.StaveNote()
+     */
+    public color(): void {
+        const defaultColorNotehead: string = EngravingRules.Rules.DefaultColorNotehead;
+        const defaultColorRest: string = EngravingRules.Rules.DefaultColorRest;
+        const defaultColorStem: string = EngravingRules.Rules.DefaultColorStem;
+        const transparentColor: string = "#00000000"; // transparent color in vexflow
+        let noteheadColor: string; // if null: no noteheadcolor to set (stays black)
+        let sourceNoteNoteheadColor: string;
+
+        const vfStaveNote: any = (<VexFlowVoiceEntry>(this as any)).vfStaveNote;
+        for (let i: number = 0; i < this.notes.length; i++) {
+            const note: GraphicalNote = this.notes[i];
+
+            sourceNoteNoteheadColor = note.sourceNote.NoteheadColor;
+            noteheadColor = sourceNoteNoteheadColor;
+            // Switch between XML colors and automatic coloring
+            if (EngravingRules.Rules.ColoringMode === ColoringModes.AutoColoring ||
+                EngravingRules.Rules.ColoringMode === ColoringModes.CustomColorSet) {
+                if (note.sourceNote.isRest()) {
+                    noteheadColor = EngravingRules.Rules.ColoringSetCurrent.getValue(-1);
+                } else {
+                    const fundamentalNote: NoteEnum = note.sourceNote.Pitch.FundamentalNote;
+                    noteheadColor = EngravingRules.Rules.ColoringSetCurrent.getValue(fundamentalNote);
+                }
+            }
+            if (!note.sourceNote.PrintObject) {
+                noteheadColor = transparentColor; // transparent
+            } else if (!noteheadColor // revert transparency after PrintObject was set to false, then true again
+                || noteheadColor === "#000000" // questionable, because you might want to set specific notes to black,
+                                               // but unfortunately some programs export everything explicitly as black
+                ) {
+                noteheadColor = EngravingRules.Rules.DefaultColorNotehead;
+            }
+
+            // DEBUG runtime coloring test
+            /*const testColor: string = "#FF0000";
+            if (i === 2 && Math.random() < 0.1 && note.sourceNote.NoteheadColor !== testColor) {
+                const measureNumber: number = note.parentVoiceEntry.parentStaffEntry.parentMeasure.MeasureNumber;
+                noteheadColor = testColor;
+                console.log("color changed to " + noteheadColor + " of this note:\n" + note.sourceNote.Pitch.ToString() +
+                    ", in measure #" + measureNumber);
+            }*/
+
+            if (!sourceNoteNoteheadColor && EngravingRules.Rules.ColoringMode === ColoringModes.XML && note.sourceNote.PrintObject) {
+                if (!note.sourceNote.isRest() && defaultColorNotehead) {
+                    noteheadColor = defaultColorNotehead;
+                } else if (note.sourceNote.isRest() && defaultColorRest) {
+                    noteheadColor = defaultColorRest;
+                }
+            }
+            if (noteheadColor && note.sourceNote.PrintObject) {
+                note.sourceNote.NoteheadColorCurrentlyRendered = noteheadColor;
+            } else if (!noteheadColor) {
+                continue;
+            }
+
+            // color notebeam if all noteheads have same color and stem coloring enabled
+            if (EngravingRules.Rules.ColoringEnabled && note.sourceNote.NoteBeam && EngravingRules.Rules.ColorBeams) {
+                const beamNotes: Note[] = note.sourceNote.NoteBeam.Notes;
+                let colorBeam: boolean = true;
+                for (let j: number = 0; j < beamNotes.length; j++) {
+                    if (beamNotes[j].NoteheadColorCurrentlyRendered !== noteheadColor) {
+                        colorBeam = false;
+                    }
+                }
+                if (colorBeam) {
+                    if (vfStaveNote.beam !== null && vfStaveNote.beam.setStyle) {
+                        vfStaveNote.beam.setStyle({ fillStyle: noteheadColor, strokeStyle: noteheadColor});
+                    }
+                }
+            }
+
+            if (vfStaveNote) {
+                if (vfStaveNote.note_heads) { // see VexFlowConverter, needs Vexflow PR
+                    const notehead: any = vfStaveNote.note_heads[i];
+                    if (notehead) {
+                        notehead.setStyle({ fillStyle: noteheadColor, strokeStyle: noteheadColor });
+                    }
+                }
+            }
+        }
+
+        // color stems
+        let stemColor: string = defaultColorStem; // reset to black/default when coloring was disabled. maybe needed elsewhere too
+        if (EngravingRules.Rules.ColoringEnabled) {
+            stemColor = this.parentVoiceEntry.StemColor; // TODO: once coloringSetCustom gets stem color, respect it
+            if (!stemColor
+                || stemColor === "#000000") { // see above, noteheadColor === "#000000"
+                stemColor = defaultColorStem;
+            }
+            if (EngravingRules.Rules.ColorStemsLikeNoteheads && noteheadColor) {
+                // condition could be even more fine-grained by only recoloring if there was no custom StemColor set. will be more complex though
+                stemColor = noteheadColor;
+            }
+        }
+        let stemTransparent: boolean = true;
+        for (const note of this.parentVoiceEntry.Notes) {
+            if (note.PrintObject) {
+                stemTransparent = false;
+                break;
+            }
+        }
+        if (stemTransparent) {
+            stemColor = transparentColor;
+        }
+        const stemStyle: Object = { fillStyle: stemColor, strokeStyle: stemColor };
+
+        if (vfStaveNote && vfStaveNote.setStemStyle) {
+            if (!stemTransparent) {
+                this.parentVoiceEntry.StemColor = stemColor;
+            }
+            vfStaveNote.setStemStyle(stemStyle);
+            if (vfStaveNote.flag && vfStaveNote.setFlagStyle && EngravingRules.Rules.ColorFlags) {
+                vfStaveNote.setFlagStyle(stemStyle);
+            }
+        }
+    }
 }

+ 1 - 1
src/MusicalScore/Graphical/index.ts

@@ -3,7 +3,7 @@
 export * from "./AbstractGraphicalExpression";
 export * from "./AbstractGraphicalInstruction";
 export * from "./AccidentalCalculator";
-export * from "./AlignmentManager";
+export * from "./VexFlow/AlignmentManager";
 export * from "./BoundingBox";
 export * from "./Clickable";
 export * from "./DrawingEnums";

+ 1 - 2
src/MusicalScore/Interfaces/IGraphicalSymbolFactory.ts

@@ -1,6 +1,5 @@
 import {ClefInstruction} from "../VoiceData/Instructions/ClefInstruction";
 import {Fraction} from "../../Common/DataObjects/Fraction";
-import {GraphicalMusicPage} from "../Graphical/GraphicalMusicPage";
 import {GraphicalNote} from "../Graphical/GraphicalNote";
 import {GraphicalStaffEntry} from "../Graphical/GraphicalStaffEntry";
 import {MusicSystem} from "../Graphical/MusicSystem";
@@ -18,7 +17,7 @@ import { VoiceEntry } from "../VoiceData/VoiceEntry";
 
 export interface IGraphicalSymbolFactory {
 
-    createMusicSystem(page: GraphicalMusicPage, systemIndex: number): MusicSystem;
+    createMusicSystem(systemIndex: number): MusicSystem;
 
     createStaffLine(parentSystem: MusicSystem, parentStaff: Staff): StaffLine;
 

+ 1 - 4
src/MusicalScore/MusicSheet.ts

@@ -79,10 +79,7 @@ export class MusicSheet /*implements ISettableMusicSheet, IComparable<MusicSheet
     // (*) private musicSheetParameterObject: MusicSheetParameterObject = undefined;
     private engravingRules: EngravingRules;
     // (*) private musicSheetParameterChangedDelegate: MusicSheetParameterChangedDelegate;
-    /*
-     * The BPM info is present in the sheet, if it is set to false, means each measure's
-     * BPM was set to its value, 120
-     */
+    /* Whether BPM info is present in the sheet. If it is set to false, each measure's BPM was set to a default of 120. */
     private hasBPMInfo: boolean;
 
     /**

+ 6 - 0
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -79,6 +79,7 @@ export class InstrumentReader {
   private divisions: number = 0;
   private currentMeasure: SourceMeasure;
   private previousMeasure: SourceMeasure;
+  private currentClefNumber: number = 1;
   private currentXmlMeasureIndex: number = 0;
   private currentStaff: Staff;
   private currentStaffEntry: SourceStaffEntry;
@@ -801,6 +802,10 @@ export class InstrumentReader {
         if (nodeList.hasAttributes && nodeList.attributes()[0].name === "number") {
           try {
             staffNumber = parseInt(nodeList.attributes()[0].value, 10);
+            if (staffNumber > this.currentClefNumber) {
+              staffNumber = this.currentClefNumber;
+            }
+            this.currentClefNumber = staffNumber + 1;
           } catch (err) {
             errorMsg = ITextTranslation.translateText(
               "ReaderErrorMessages/ClefError",
@@ -808,6 +813,7 @@ export class InstrumentReader {
             );
             this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
             staffNumber = 1;
+            this.currentClefNumber = staffNumber + 1;
           }
         }
 

+ 1 - 1
src/MusicalScore/ScoreIO/MusicSymbolModules/RepetitionInstructionReader.ts

@@ -104,7 +104,7 @@ export class RepetitionInstructionReader {
             this.addInstruction(this.repetitionInstructions, newInstruction);
           }
         } else { // location right
-          if (type === "stop" || type === "discontinue") {
+          if (type === "stop") {
             const newInstruction: RepetitionInstruction = new RepetitionInstruction(this.currentMeasureIndex, RepetitionInstructionEnum.Ending,
                                                                                     AlignmentType.End, undefined, endingIndices);
             this.addInstruction(this.repetitionInstructions, newInstruction);

+ 5 - 5
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -26,7 +26,7 @@ import { CollectionUtil } from "../../Util/CollectionUtil";
 import { ArticulationReader } from "./MusicSymbolModules/ArticulationReader";
 import { SlurReader } from "./MusicSymbolModules/SlurReader";
 import { Notehead } from "../VoiceData/Notehead";
-import { Arpeggio } from "../VoiceData/Arpeggio";
+import { Arpeggio, ArpeggioType } from "../VoiceData/Arpeggio";
 import { NoteType } from "../VoiceData/NoteType";
 import { TabNote } from "../VoiceData/TabNote";
 
@@ -166,18 +166,18 @@ export class VoiceGenerator {
               }
             }
             if (!arpeggioAlreadyExists) {
-                let arpeggioType: Vex.Flow.Stroke.Type = Vex.Flow.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
+                let arpeggioType: ArpeggioType = ArpeggioType.ARPEGGIO_DIRECTIONLESS;
                 const directionAttr: Attr = arpeggioNode.attribute("direction");
                 if (directionAttr !== null) {
                   switch (directionAttr.value) {
                     case "up":
-                      arpeggioType = Vex.Flow.Stroke.Type.ROLL_UP;
+                      arpeggioType = ArpeggioType.ROLL_UP;
                       break;
                     case "down":
-                      arpeggioType = Vex.Flow.Stroke.Type.ROLL_DOWN;
+                      arpeggioType = ArpeggioType.ROLL_DOWN;
                       break;
                     default:
-                      arpeggioType = Vex.Flow.Stroke.Type.ARPEGGIO_DIRECTIONLESS;
+                      arpeggioType = ArpeggioType.ARPEGGIO_DIRECTIONLESS;
                   }
                 }
 

+ 13 - 2
src/MusicalScore/VoiceData/Arpeggio.ts

@@ -2,7 +2,7 @@ import { VoiceEntry } from "./VoiceEntry";
 import { Note } from "./Note";
 
 export class Arpeggio {
-    constructor(parentVoiceEntry: VoiceEntry, type: Vex.Flow.Stroke.Type = Vex.Flow.Stroke.Type.ARPEGGIO_DIRECTIONLESS) {
+    constructor(parentVoiceEntry: VoiceEntry, type: ArpeggioType = ArpeggioType.ARPEGGIO_DIRECTIONLESS) {
         this.parentVoiceEntry = parentVoiceEntry;
         this.type = type;
         this.notes = [];
@@ -10,10 +10,21 @@ export class Arpeggio {
 
     public parentVoiceEntry: VoiceEntry;
     public notes: Note[];
-    public type: Vex.Flow.Stroke.Type;
+    public type: ArpeggioType;
 
     public addNote(note: Note): void {
         this.notes.push(note);
         note.Arpeggio = this;
     }
 }
+
+/** Corresponds to Vex.Flow.Stroke.Type for now. But we don't want VexFlow as a dependency here. */
+export enum ArpeggioType {
+    BRUSH_DOWN = 1,
+    BRUSH_UP,
+    ROLL_DOWN,
+    ROLL_UP,
+    RASQUEDO_DOWN,
+    RASQUEDO_UP,
+    ARPEGGIO_DIRECTIONLESS
+}

+ 9 - 1
src/MusicalScore/VoiceData/Note.ts

@@ -77,11 +77,12 @@ export class Note {
      * because Note.Notehead is undefined for normal Noteheads to save space and time.
      */
     private noteheadColorXml: string;
-    /** Color of the notehead currently set. RGB Hexadecimal, like #00FF00.
+    /** Color of the notehead currently set/desired for next render. RGB Hexadecimal, like #00FF00.
      * Needs to be stored here and not in Note.Notehead,
      * because Note.Notehead is undefined for normal Noteheads to save space and time.
      */
     private noteheadColor: string;
+    private noteheadColorCurrentlyRendered: string;
 
     public get ParentVoiceEntry(): VoiceEntry {
         return this.voiceEntry;
@@ -200,12 +201,19 @@ export class Note {
     public set NoteheadColorXml(value: string) {
         this.noteheadColorXml = value;
     }
+    /** The desired notehead color for the next render. */
     public get NoteheadColor(): string {
         return this.noteheadColor;
     }
     public set NoteheadColor(value: string) {
         this.noteheadColor = value;
     }
+    public get NoteheadColorCurrentlyRendered(): string {
+        return this.noteheadColorCurrentlyRendered;
+    }
+    public set NoteheadColorCurrentlyRendered(value: string) {
+        this.noteheadColorCurrentlyRendered = value;
+    }
 
     public isRest(): boolean {
         return this.Pitch === undefined;

+ 1 - 0
src/MusicalScore/VoiceData/Staff.ts

@@ -13,6 +13,7 @@ export class Staff {
     public idInMusicSheet: number;
     public audible: boolean;
     public following: boolean;
+    public isTab: boolean = false;
 
     private parentInstrument: Instrument;
     private voices: Voice[] = [];

+ 8 - 6
src/OpenSheetMusicDisplay/Cursor.ts

@@ -8,7 +8,7 @@ import {GraphicalMusicSheet} from "../MusicalScore/Graphical/GraphicalMusicSheet
 import {Instrument} from "../MusicalScore/Instrument";
 import {Note} from "../MusicalScore/VoiceData/Note";
 import {EngravingRules, Fraction} from "..";
-import {SourceMeasure} from "../MusicalScore";
+import {SourceMeasure, StaffLine} from "../MusicalScore";
 
 /**
  * A cursor which can iterate through the music sheet.
@@ -74,7 +74,7 @@ export class Cursor {
     this.iterator = this.manager.getIterator();
   }
 
-  private getStaffEntriesFromVoiceEntry(voiceEntry: VoiceEntry): VexFlowStaffEntry {
+  private getStaffEntryFromVoiceEntry(voiceEntry: VoiceEntry): VexFlowStaffEntry {
     const measureIndex: number = voiceEntry.ParentSourceStaffEntry.VerticalContainerParent.ParentMeasure.measureListIndex;
     const staffIndex: number = voiceEntry.ParentSourceStaffEntry.ParentStaff.idInMusicSheet;
     return <VexFlowStaffEntry>this.graphic.findGraphicalStaffEntryFromMeasureList(staffIndex, measureIndex, voiceEntry.ParentSourceStaffEntry);
@@ -96,15 +96,16 @@ export class Cursor {
     let x: number = 0, y: number = 0, height: number = 0;
 
     // get all staff entries inside the current voice entry
-    const gseArr: VexFlowStaffEntry[] = voiceEntries.map(ve => this.getStaffEntriesFromVoiceEntry(ve));
+    const gseArr: VexFlowStaffEntry[] = voiceEntries.map(ve => this.getStaffEntryFromVoiceEntry(ve));
     // sort them by x position and take the leftmost entry
     const gse: VexFlowStaffEntry =
           gseArr.sort((a, b) => a.PositionAndShape.AbsolutePosition.x <= b.PositionAndShape.AbsolutePosition.x ? -1 : 1 )[0];
     x = gse.PositionAndShape.AbsolutePosition.x;
-    const musicSystem: MusicSystem = gse.parentMeasure.parentMusicSystem;
+    const musicSystem: MusicSystem = gse.parentMeasure.ParentMusicSystem;
     y = musicSystem.PositionAndShape.AbsolutePosition.y + musicSystem.StaffLines[0].PositionAndShape.RelativePosition.y;
+    const bottomStaffline: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
     const endY: number = musicSystem.PositionAndShape.AbsolutePosition.y +
-      musicSystem.StaffLines[musicSystem.StaffLines.length - 1].PositionAndShape.RelativePosition.y + 4.0;
+    bottomStaffline.PositionAndShape.RelativePosition.y + bottomStaffline.StaffHeight;
     height = endY - y;
 
     // The following code is not necessary (for now, but it could come useful later):
@@ -134,7 +135,8 @@ export class Cursor {
       this.updateStyle(newWidth);
     }
     if (this.openSheetMusicDisplay.FollowCursor) {
-      this.cursorElement.scrollIntoView({behavior: "smooth", block: "center"});
+      const diff: number = this.cursorElement.getBoundingClientRect().top;
+      this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
     }
     // Show cursor
     // // Old cursor: this.graphic.Cursors.push(cursor);

+ 29 - 3
src/OpenSheetMusicDisplay/OSMDOptions.ts

@@ -34,7 +34,7 @@ export interface IOSMDOptions {
      * Only considered before loading a sample, not before render.
      * To change the color after loading a sample and before render, use note(.sourceNote).NoteheadColor.
      * The format is Vexflow format, either "#rrggbb" or "#rrggbbtt" where <tt> is transparency. All hex values.
-     * E.g., a half-transparent red would be "#FF000080", invisible would be "#00000000" or "#12345600".
+     * E.g., a half-transparent red would be "#FF000080", invisible/transparent would be "#00000000" or "#12345600".
      */
     defaultColorNotehead?: string;
     /** Default color for a note stem. Default black (undefined). */
@@ -85,6 +85,8 @@ export interface IOSMDOptions {
     fingeringPosition?: string;
     /** For above/below fingerings, whether to draw them directly above/below notes (default), or above/below staffline. */
     fingeringInsideStafflines?: boolean;
+    /** Whether to draw hidden/invisible notes (print-object="no" in XML). Default false. Not yet supported. */ // TODO
+    drawHiddenNotes?: boolean;
     /** Whether to draw lyrics (and their extensions and dashes). */
     drawLyrics?: boolean;
     /** Whether to calculate extra slurs with bezier curves not covered by Vexflow slurs. Default true. */
@@ -108,8 +110,19 @@ export interface IOSMDOptions {
      * (Bracketing all triplets can be cluttering)
      */
     tripletsBracketed?: boolean;
-    /** Whether to draw hidden/invisible notes (print-object="no" in XML). Default false. Not yet supported. */ // TODO
-    drawHiddenNotes?: boolean;
+    /**  See OpenSheetMusicDisplay.PageFormatStandards for standard options like "A4 P" or "Endless". Default Endless.
+     *   Uses OpenSheetMusicDisplay.StringToPageFormat(). Unfortunately it would be error-prone to set a PageFormat type directly.
+     */
+    pageFormat?: string;
+    /** A custom page/canvas background color. Default undefined/transparent.
+     *  Example: "#FFFFFF" = white. "#12345600" = transparent.
+     *  This can be useful when you want to export an image with e.g. white background color instead of transparent,
+     *  from a CanvasBackend.
+     *  Note: Using a background color will prevent the cursor from being visible.
+     */
+    pageBackgroundColor?: string;
+    /** This makes OSMD render on one single horizontal (staff-)line. */
+    renderSingleHorizontalStaffline?: boolean;
 }
 
 export enum AlignRestOption {
@@ -124,6 +137,11 @@ export enum FillEmptyMeasuresWithWholeRests {
     YesInvisible = 2
 }
 
+export enum BackendType {
+    SVG = 0,
+    Canvas = 1
+}
+
 /** Handles [[IOSMDOptions]], e.g. returning default options with OSMDOptionsStandard() */
 export class OSMDOptions {
     /** Returns the default options for OSMD.
@@ -136,6 +154,14 @@ export class OSMDOptions {
             drawingParameters: DrawingParametersEnum.default,
         };
     }
+
+    public static BackendTypeFromString(value: string): BackendType {
+        if (value && value.toLowerCase() === "canvas") {
+            return BackendType.Canvas;
+        } else {
+            return BackendType.SVG;
+        }
+    }
 }
 
 export interface AutoBeamOptions {

+ 259 - 47
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -14,12 +14,14 @@ import { Promise } from "es6-promise";
 import { AJAX } from "./AJAX";
 import * as log from "loglevel";
 import { DrawingParametersEnum, DrawingParameters, ColoringModes } from "../MusicalScore/Graphical/DrawingParameters";
-import { IOSMDOptions, OSMDOptions, AutoBeamOptions } from "./OSMDOptions";
-import { EngravingRules } from "../MusicalScore/Graphical/EngravingRules";
+import { IOSMDOptions, OSMDOptions, AutoBeamOptions, BackendType } from "./OSMDOptions";
+import { EngravingRules, PageFormat } from "../MusicalScore/Graphical/EngravingRules";
 import { AbstractExpression } from "../MusicalScore/VoiceData/Expressions/AbstractExpression";
 import { Dictionary } from "typescript-collections";
 import { NoteEnum } from "..";
-import { AutoColorSet } from "../MusicalScore";
+import { AutoColorSet, GraphicalMusicPage } from "../MusicalScore";
+import jspdf = require("jspdf-yworks/dist/jspdf.min");
+import svg2pdf = require("svg2pdf.js/dist/svg2pdf.min");
 
 /**
  * The main class and control point of OpenSheetMusicDisplay.<br>
@@ -27,7 +29,7 @@ import { AutoColorSet } from "../MusicalScore";
  * After the constructor, use load() and render() to load and render a MusicXML file.
  */
 export class OpenSheetMusicDisplay {
-    private version: string = "0.7.2-dev"; // getter: this.Version
+    private version: string = "0.7.3-dev"; // getter: this.Version
     // at release, bump version and change to -release, afterwards to -dev again
 
     /**
@@ -56,6 +58,7 @@ export class OpenSheetMusicDisplay {
         if (options.autoResize === undefined) {
             options.autoResize = true;
         }
+        this.backendType = BackendType.SVG; // default, can be changed by options
         this.setOptions(options);
     }
 
@@ -63,11 +66,13 @@ export class OpenSheetMusicDisplay {
     public zoom: number = 1.0;
 
     private container: HTMLElement;
-    private canvas: HTMLElement;
-    private backend: VexFlowBackend;
-    private innerElement: HTMLElement;
+    private backendType: BackendType;
+    private needBackendUpdate: boolean;
     private sheet: MusicSheet;
     private drawer: VexFlowMusicSheetDrawer;
+    private drawBoundingBox: string;
+    private drawSkyLine: boolean;
+    private drawBottomLine: boolean;
     private graphic: GraphicalMusicSheet;
     private drawingParameters: DrawingParameters;
     private autoResizeEnabled: boolean;
@@ -81,10 +86,11 @@ export class OpenSheetMusicDisplay {
     public load(content: string | Document): Promise<{}> {
         // Warning! This function is asynchronous! No error handling is done here.
         this.reset();
+        //console.log("typeof content: " + typeof content);
         if (typeof content === "string") {
-
             const str: string = <string>content;
             const self: OpenSheetMusicDisplay = this;
+            // console.log("substring: " + str.substr(0, 5));
             if (str.substr(0, 4) === "\x50\x4b\x03\x04") {
                 log.debug("[OSMD] This is a zip file, unpack it first: " + str);
                 // This is a zip file, unpack it first
@@ -104,7 +110,7 @@ export class OpenSheetMusicDisplay {
                 // UTF with BOM detected, truncate first three bytes and pass along
                 return self.load(str.substr(3));
             }
-            if (str.substr(0, 5) === "<?xml") {
+            if (str.substr(0, 6).includes("<?xml")) { // first character is sometimes null, making first five characters '<?xm'.
                 log.debug("[OSMD] Finally parsing XML content, length: " + str.length);
                 // Parse the string representing an xml file
                 const parser: DOMParser = new DOMParser();
@@ -118,7 +124,7 @@ export class OpenSheetMusicDisplay {
                     (exc: Error) => { throw exc; }
                 );
             } else {
-                console.error("Missing else branch?");
+                console.error("[OSMD] osmd.load(string): Could not process string. Missing else branch?");
             }
         }
 
@@ -150,6 +156,7 @@ export class OpenSheetMusicDisplay {
         }
         log.info(`[OSMD] Loaded sheet ${this.sheet.TitleString} successfully.`);
 
+        this.needBackendUpdate = true;
         this.updateGraphic();
 
         return Promise.resolve({});
@@ -173,11 +180,31 @@ export class OpenSheetMusicDisplay {
         if (!this.graphic) {
             throw new Error("OpenSheetMusicDisplay: Before rendering a music sheet, please load a MusicXML file");
         }
-        this.drawer.clear(); // clear canvas before setting width
+        if (this.drawer) {
+            this.drawer.clear(); // clear canvas before setting width
+        }
+        // musicSheetCalculator.clearSystemsAndMeasures() // maybe? don't have reference though
+        // musicSheetCalculator.clearRecreatedObjects();
 
         // Set page width
-        const width: number = this.container.offsetWidth;
+        let width: number = this.container.offsetWidth;
+        if (EngravingRules.Rules.RenderSingleHorizontalStaffline) {
+            width = 32767; // set safe maximum (browser limit), will be reduced later
+            // reduced later in MusicSheetCalculator.calculatePageLabels (sets sheet.pageWidth to page.PositionAndShape.Size.width before labels)
+            // rough calculation:
+            // width = 600 * this.sheet.SourceMeasures.length;
+        }
+        // log.debug("[OSMD] render width: " + width);
+
         this.sheet.pageWidth = width / this.zoom / 10.0;
+        if (EngravingRules.Rules.PageFormat && !EngravingRules.Rules.PageFormat.IsUndefined) {
+            EngravingRules.Rules.PageHeight = this.sheet.pageWidth / EngravingRules.Rules.PageFormat.aspectRatio;
+            log.debug("[OSMD] PageHeight: " + EngravingRules.Rules.PageHeight);
+        } else {
+            log.debug("[OSMD] endless/undefined pageformat, id: " + EngravingRules.Rules.PageFormat.idString);
+            EngravingRules.Rules.PageHeight = 100001; // infinite page height // TODO maybe Number.MAX_VALUE or Math.pow(10, 20)?
+        }
+
         // Before introducing the following optimization (maybe irrelevant), tests
         // have to be modified to ensure that width is > 0 when executed
         //if (isNaN(width) || width === 0) {
@@ -186,19 +213,91 @@ export class OpenSheetMusicDisplay {
 
         // Calculate again
         this.graphic.reCalculate();
-        const height: number = this.graphic.MusicPages[0].PositionAndShape.BorderBottom * 10.0 * this.zoom;
+
         if (this.drawingParameters.drawCursors) {
             this.graphic.Cursors.length = 0;
         }
-        // Update Sheet Page
-        this.drawer.resize(width, height);
-        this.drawer.scale(this.zoom);
+
+        // needBackendUpdate is well intentioned, but we need to cover all cases.
+        //   backends also need an update when this.zoom was set from outside, which unfortunately doesn't have a setter method to set this in.
+        //   so just for compatibility, we need to assume users set osmd.zoom, so we'd need to check whether it was changed compared to last time.
+        if (true || this.needBackendUpdate) {
+            this.createOrRefreshRenderBackend();
+            this.needBackendUpdate = false;
+        }
+
+        this.drawer.setZoom(this.zoom);
         // Finally, draw
         this.drawer.drawSheet(this.graphic);
+
+        this.enableOrDisableCursor(this.drawingParameters.drawCursors);
+
         if (this.drawingParameters.drawCursors && this.cursor) {
             // Update the cursor position
             this.cursor.update();
         }
+        //console.log("[OSMD] render finished");
+    }
+
+    private createOrRefreshRenderBackend(): void {
+        // console.log("[OSMD] createOrRefreshRenderBackend()");
+
+        // Remove old backends
+        if (this.drawer && this.drawer.Backends) {
+            // removing single children to remove all is error-prone, because sometimes a random SVG-child remains.
+            // for (const backend of this.drawer.Backends) {
+            //     backend.removeFromContainer(this.container);
+            // }
+            if (this.drawer.Backends[0]) {
+                this.drawer.Backends[0].removeAllChildrenFromContainer(this.container);
+            }
+            this.drawer.Backends.clear();
+        }
+
+        // Create the drawer
+        this.drawer = new VexFlowMusicSheetDrawer(this.drawingParameters); // note that here the drawer.drawableBoundingBoxElement is lost. now saved in OSMD.
+        this.drawer.drawableBoundingBoxElement = this.DrawBoundingBox;
+        this.drawer.bottomLineVisible = this.drawBottomLine;
+        this.drawer.skyLineVisible = this.drawSkyLine;
+
+        // Set page width
+        let width: number = this.container.offsetWidth;
+        if (EngravingRules.Rules.RenderSingleHorizontalStaffline) {
+            width = this.graphic.MusicPages[0].PositionAndShape.Size.width * 10 * this.zoom;
+            // this.container.style.width = width + "px";
+            // console.log("width: " + width)
+        }
+        // TODO width may need to be coordinated with render() where width is also used
+        let height: number;
+        const canvasDimensionsLimit: number = 32767; // browser limitation. Chrome/Firefox (16 bit, 32768 causes an error).
+        // Could be calculated by canvas-size module.
+        // see #678 on Github and here: https://stackoverflow.com/a/11585939/10295942
+
+        // TODO check if resize is necessary. set needResize or something when size was changed
+        for (const page of this.graphic.MusicPages) {
+            const backend: VexFlowBackend = this.createBackend(this.backendType, page);
+            const sizeWarningPartTwo: string = " exceeds CanvasBackend limit of 32767. Cutting off score.";
+            if (backend.getOSMDBackendType() === BackendType.Canvas && width > canvasDimensionsLimit) {
+                console.log("[OSMD] Warning: width of " + width + sizeWarningPartTwo);
+                width = canvasDimensionsLimit;
+            }
+            if (EngravingRules.Rules.PageFormat && !EngravingRules.Rules.PageFormat.IsUndefined) {
+                height = width / EngravingRules.Rules.PageFormat.aspectRatio;
+                // console.log("pageformat given. height: " + page.PositionAndShape.Size.height);
+            } else {
+                height = (page.PositionAndShape.Size.height + 15) * this.zoom * 10.0;
+                // console.log("pageformat not given. height: " + page.PositionAndShape.Size.height);
+            }
+            if (backend.getOSMDBackendType() === BackendType.Canvas && height > canvasDimensionsLimit) {
+                console.log("[OSMD] Warning: height of " + height + sizeWarningPartTwo);
+                height = Math.min(height, canvasDimensionsLimit); // this cuts off the the score, but doesn't break rendering.
+                // TODO optional: reduce zoom to fit the score within the limit.
+            }
+
+            backend.resize(width, height);
+            backend.clear(); // set bgcolor if defined (EngravingRules.Rules.PageBackgroundColor, see OSMDOptions)
+            this.drawer.Backends.push(backend);
+        }
     }
 
     /** States whether the render() function can be safely called. */
@@ -230,31 +329,18 @@ export class OpenSheetMusicDisplay {
                 (<any>DrawingParametersEnum)[options.drawingParameters.toLowerCase()];
         }
 
-        const updateExistingBackend: boolean = this.backend !== undefined;
-        if (options.backend !== undefined || this.backend === undefined) {
-            if (updateExistingBackend) {
-                // TODO doesn't work yet, still need to create a new OSMD object
-
-                this.drawer.clear();
-
-                // musicSheetCalculator.clearSystemsAndMeasures() // maybe? don't have reference though
-                // musicSheetCalculator.clearRecreatedObjects();
-            }
-            if (options.backend === undefined || options.backend.toLowerCase() === "svg") {
-                this.backend = new SvgVexFlowBackend();
-            } else {
-                this.backend = new CanvasVexFlowBackend();
-            }
-            this.backend.initialize(this.container);
-            this.canvas = this.backend.getCanvas();
-            this.innerElement = this.backend.getInnerElement();
-            this.enableOrDisableCursor(this.drawingParameters.drawCursors);
-            // Create the drawer
-            this.drawer = new VexFlowMusicSheetDrawer(this.canvas, this.backend, this.drawingParameters);
+        const backendNotInitialized: boolean = !this.drawer || !this.drawer.Backends || this.drawer.Backends.length < 1;
+        let needBackendUpdate: boolean = backendNotInitialized;
+        if (options.backend !== undefined) {
+            const backendTypeGiven: BackendType = OSMDOptions.BackendTypeFromString(options.backend);
+            needBackendUpdate = needBackendUpdate || this.backendType !== backendTypeGiven;
+            this.backendType = backendTypeGiven;
         }
+        this.needBackendUpdate = needBackendUpdate;
+        // TODO this is a necessary step during the OSMD constructor. Maybe move this somewhere else
 
         // individual drawing parameters options
-        if (options.autoBeam !== undefined) {
+        if (options.autoBeam !== undefined) { // only change an option if it was given in options, otherwise it will be undefined
             EngravingRules.Rules.AutoBeamNotes = options.autoBeam;
         }
         const autoBeamOptions: AutoBeamOptions = options.autoBeamOptions;
@@ -286,8 +372,8 @@ export class OpenSheetMusicDisplay {
         }
         if (options.disableCursor) {
             this.drawingParameters.drawCursors = false;
-            this.enableOrDisableCursor(this.drawingParameters.drawCursors);
         }
+
         // alternative to if block: this.drawingsParameters.drawCursors = options.drawCursors !== false. No if, but always sets drawingParameters.
         // note that every option can be undefined, which doesn't mean the option should be set to false.
         if (options.drawHiddenNotes) {
@@ -386,6 +472,15 @@ export class OpenSheetMusicDisplay {
             this.autoResizeEnabled = false;
             // we could remove the window EventListener here, but not necessary.
         }
+        if (options.pageFormat !== undefined) { // only change this option if it was given, see above
+            EngravingRules.Rules.PageFormat = OpenSheetMusicDisplay.StringToPageFormat(options.pageFormat);
+        }
+        if (options.pageBackgroundColor !== undefined) {
+            EngravingRules.Rules.PageBackgroundColor = options.pageBackgroundColor;
+        }
+        if (options.renderSingleHorizontalStaffline !== undefined) {
+            EngravingRules.Rules.RenderSingleHorizontalStaffline = options.renderSingleHorizontalStaffline;
+        }
     }
 
     public setColoringMode(options: IOSMDOptions): void {
@@ -551,11 +646,9 @@ export class OpenSheetMusicDisplay {
     public enableOrDisableCursor(enable: boolean): void {
         this.drawingParameters.drawCursors = enable;
         if (enable) {
-            if (!this.cursor) {
-                this.cursor = new Cursor(this.innerElement, this);
-                if (this.sheet && this.graphic) { // else init is called in load()
-                    this.cursor.init(this.sheet.MusicPartManager, this.graphic);
-                }
+            this.cursor = new Cursor(this.drawer.Backends[0].getInnerElement(), this);
+            if (this.sheet && this.graphic) { // else init is called in load()
+                this.cursor.init(this.sheet.MusicPartManager, this.graphic);
             }
         } else { // disable cursor
             if (!this.cursor) {
@@ -568,8 +661,125 @@ export class OpenSheetMusicDisplay {
         }
     }
 
+    public createBackend(type: BackendType, page: GraphicalMusicPage): VexFlowBackend {
+        let backend: VexFlowBackend;
+        if (type === undefined || type === BackendType.SVG) {
+            backend = new SvgVexFlowBackend();
+        } else {
+            backend = new CanvasVexFlowBackend();
+        }
+        backend.graphicalMusicPage = page; // the page the backend renders on. needed to identify DOM element to extract image/SVG
+        backend.initialize(this.container);
+        return backend;
+    }
+
+    /** Standard page format options like A4 or Letter, in portrait and landscape. E.g. PageFormatStandards["A4_P"] or PageFormatStandards["Letter_L"]. */
+    public static PageFormatStandards: { [type: string]: PageFormat } = {
+        "A3_L": new PageFormat(420, 297, "A3_L"), // id strings should use underscores instead of white spaces to facilitate use as URL parameters.
+        "A3_P": new PageFormat(297, 420, "A3_P"),
+        "A4_L": new PageFormat(297, 210, "A4_L"),
+        "A4_P": new PageFormat(210, 297, "A4_P"),
+        "A5_L": new PageFormat(210, 148, "A5_L"),
+        "A5_P": new PageFormat(148, 210, "A5_P"),
+        "A6_L": new PageFormat(148, 105, "A6_L"),
+        "A6_P": new PageFormat(105, 148, "A6_P"),
+        "Endless": PageFormat.UndefinedPageFormat,
+        "Letter_L": new PageFormat(279.4, 215.9, "Letter_L"),
+        "Letter_P": new PageFormat(215.9, 279.4, "Letter_P")
+    };
+
+    public static StringToPageFormat(pageFormatString: string): PageFormat {
+        let pageFormat: PageFormat = PageFormat.UndefinedPageFormat; // default: 'endless' page height, take canvas/container width
+
+        // check for widthxheight parameter, e.g. "800x600"
+        if (pageFormatString.match("^[0-9]+x[0-9]+$")) {
+            const widthAndHeight: string[] = pageFormatString.split("x");
+            const width: number = Number.parseInt(widthAndHeight[0], 10);
+            const height: number = Number.parseInt(widthAndHeight[1], 10);
+            if (width > 0 && width < 32768 && height > 0 && height < 32768) {
+                pageFormat = new PageFormat(width, height, `customPageFormat${pageFormatString}`);
+            }
+        }
+
+        // check for formatId from OpenSheetMusicDisplay.PageFormatStandards
+        pageFormatString = pageFormatString.replace(" ", "_");
+        pageFormatString = pageFormatString.replace("Landscape", "L");
+        pageFormatString = pageFormatString.replace("Portrait", "P");
+        //console.log("change format to: " + formatId);
+        if (OpenSheetMusicDisplay.PageFormatStandards.hasOwnProperty(pageFormatString)) {
+            pageFormat = OpenSheetMusicDisplay.PageFormatStandards[pageFormatString];
+            return pageFormat;
+        }
+        return pageFormat;
+    }
+
+    /** Sets page format by string. Alternative to setOptions({pageFormat: PageFormatStandards.Endless}) for example. */
+    public setPageFormat(formatId: string): void {
+        const newPageFormat: PageFormat = OpenSheetMusicDisplay.StringToPageFormat(formatId);
+        this.needBackendUpdate = !(newPageFormat.Equals(EngravingRules.Rules.PageFormat));
+        EngravingRules.Rules.PageFormat = newPageFormat;
+    }
+
+    public setCustomPageFormat(width: number, height: number): void {
+        if (width > 0 && height > 0) {
+            const f: PageFormat = new PageFormat(width, height);
+            EngravingRules.Rules.PageFormat = f;
+        }
+    }
+
+    /**
+     * Creates a Pdf of the currently rendered MusicXML
+     * @param pdfName if no name is given, the composer and title of the piece will be used
+     */
+    public createPdf(pdfName: string = undefined): void {
+        if (this.backendType !== BackendType.SVG) {
+            console.log("[OSMD] osmd.createPdf(): Warning: createPDF is only supported for SVG background for now, not for Canvas." +
+                " Please use osmd.setOptions({backendType: SVG}).");
+            return;
+        }
+
+        if (pdfName === undefined) {
+            pdfName = this.sheet.FullNameString + ".pdf";
+        }
+
+        const backends: VexFlowBackend[] = this.drawer.Backends;
+        let svgElement: SVGElement = (<SvgVexFlowBackend>backends[0]).getSvgElement();
+
+        let pageWidth: number = 210;
+        let pageHeight: number = 297;
+        const engravingRulesPageFormat: PageFormat = EngravingRules.Rules.PageFormat;
+        if (engravingRulesPageFormat && !engravingRulesPageFormat.IsUndefined) {
+            pageWidth = engravingRulesPageFormat.width;
+            pageHeight = engravingRulesPageFormat.height;
+        } else {
+            pageHeight = pageWidth * svgElement.clientHeight / svgElement.clientWidth;
+        }
+
+        const orientation: string = pageHeight > pageWidth ? "p" : "l";
+        // create a new jsPDF instance
+        const pdf: any = new jspdf(orientation, "mm", [pageWidth, pageHeight]);
+        const scale: number = pageWidth / svgElement.clientWidth;
+        for (let idx: number = 0, len: number = backends.length; idx < len; ++idx) {
+            if (idx > 0) {
+                pdf.addPage();
+            }
+            svgElement = (<SvgVexFlowBackend>backends[idx]).getSvgElement();
+
+            // render the svg element
+            svg2pdf(svgElement, pdf, {
+                scale: scale,
+                xOffset: 0,
+                yOffset: 0
+            });
+        }
+
+        // simply save the created pdf
+        pdf.save(pdfName);
+    }
+
     //#region GETTER / SETTER
     public set DrawSkyLine(value: boolean) {
+        this.drawSkyLine = value;
         if (this.drawer) {
             this.drawer.skyLineVisible = value;
             this.render();
@@ -580,6 +790,7 @@ export class OpenSheetMusicDisplay {
     }
 
     public set DrawBottomLine(value: boolean) {
+        this.drawBottomLine = value;
         if (this.drawer) {
             this.drawer.bottomLineVisible = value;
             this.render();
@@ -590,11 +801,12 @@ export class OpenSheetMusicDisplay {
     }
 
     public set DrawBoundingBox(value: string) {
-        this.drawer.drawableBoundingBoxElement = value;
-        this.render();
+        this.drawBoundingBox = value;
+        this.drawer.drawableBoundingBoxElement = value; // drawer is sometimes created anew, losing this value, so it's saved in OSMD now.
+        this.render(); // may create new Drawer.
     }
     public get DrawBoundingBox(): string {
-        return this.drawer.drawableBoundingBoxElement;
+        return this.drawBoundingBox;
     }
 
     public get AutoResizeEnabled(): boolean {

+ 4 - 2
test/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer_Test.ts

@@ -28,7 +28,8 @@ describe("VexFlow Music Sheet Drawer", () => {
         const canvas: HTMLCanvasElement = document.createElement("canvas");
         const backend: VexFlowBackend = new CanvasVexFlowBackend();
         backend.initialize(canvas);
-        const drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer(canvas, backend);
+        const drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer();
+        drawer.Backends.push(backend);
         drawer.drawSheet(gms);
         done();
     });
@@ -48,7 +49,8 @@ describe("VexFlow Music Sheet Drawer", () => {
         const canvas: HTMLCanvasElement = document.createElement("canvas");
         const backend: VexFlowBackend = new CanvasVexFlowBackend();
         backend.initialize(canvas);
-        const drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer(canvas, backend, new DrawingParameters());
+        const drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer(new DrawingParameters());
+        drawer.Backends.push(backend);
         drawer.drawSheet(gms);
         done();
     });

+ 63 - 0
test/Util/DiffImages_Test_Experimental.ts

@@ -0,0 +1,63 @@
+import { OpenSheetMusicDisplay, CanvasVexFlowBackend } from "../../src";
+import { TestUtils } from "./TestUtils";
+//import * as fs from "fs";
+
+// experimental code, shouldn't be included in Karma test suite
+
+describe("GeneratePNGImages", () => {
+    // Test all the following xml files:
+    const sampleFilenames: string[] = [
+        "Beethoven_AnDieFerneGeliebte.xml",
+        // "CharlesGounod_Meditation.xml",
+        // "Debussy_Mandoline.xml",
+        // "Dichterliebe01.xml",
+        // "JohannSebastianBach_Air.xml",
+        // "JohannSebastianBach_PraeludiumInCDur_BWV846_1.xml",
+        // "JosephHaydn_ConcertanteCello.xml",
+        // "Mozart_AnChloe.xml",
+        // "Mozart_DasVeilchen.xml",
+        "MuzioClementi_SonatinaOpus36No1_Part1.xml",
+        // "MuzioClementi_SonatinaOpus36No1_Part2.xml",
+        // "MuzioClementi_SonatinaOpus36No3_Part1.xml",
+        // "MuzioClementi_SonatinaOpus36No3_Part2.xml",
+        // "Saltarello.xml",
+        // "ScottJoplin_EliteSyncopations.xml",
+        // "ScottJoplin_The_Entertainer.xml",
+        // "TelemannWV40.102_Sonate-Nr.1.1-Dolce.xml",
+        // "TelemannWV40.102_Sonate-Nr.1.2-Allegro-F-Dur.xml",
+    ];
+    for (const score of sampleFilenames) {
+        generatePNG(score);
+    }
+
+    // TODO This is just example code for now.
+    // generate PNG. TODO fs doesn't work with Karma. This is the big problem that needs to be worked around with ts/Karma.
+    function generatePNG(sampleFilename: string): void {
+        it(sampleFilename, (done: MochaDone) => {
+            // Load the xml file content
+            const score: Document = TestUtils.getScore(sampleFilename);
+            const div: HTMLElement = document.createElement("div");
+            const openSheetMusicDisplay: OpenSheetMusicDisplay =
+                new OpenSheetMusicDisplay(div, { autoResize: false, backend: "canvas"});
+            openSheetMusicDisplay.load(score);
+
+            const testDir: string = "../data/images";
+            //fs.mkdirSync(testDir, { recursive: true });
+
+            const fileName: string = `${testDir}/${sampleFilename}.png`;
+            console.log("fileName: " + fileName);
+
+            console.log("before buffer");
+            const canvasBackend: CanvasVexFlowBackend = openSheetMusicDisplay.Drawer.Backends[0] as CanvasVexFlowBackend;
+            const imageData: string = (canvasBackend.getCanvas() as HTMLCanvasElement).toDataURL().split(";base64,").pop();
+            const imageBuffer: Buffer = Buffer.from(imageData, "base64");
+            console.log("imageBuffer.length: " + imageBuffer.length);
+            //console.log("after buffer");
+            //let arraybuffer = Uint8Array.from(imageBuffer, 'base64').buffer;
+
+            //fs.writeFileSync(fileName, imageBuffer, { encoding: "base64" });
+
+            done();
+            }).timeout(10000);
+    }
+});

+ 243 - 0
test/Util/generateDiffImagesPuppeteerLocalhost.js

@@ -0,0 +1,243 @@
+/*
+  Render each OSMD sample in a headless browser
+  (using puppeteer, requires ~100MB chromium download),
+  grab the generated images, and
+  dump them into a local directory as PNG files.
+
+  You may have to install puppeteer as dev dependency to run this:
+  npm i puppeteer --save-dev
+  (will download ~100MB for Chromium)
+
+  This script is made obsolete by the ~2x faster generateImages_browserless.js,
+  but may be useful for comparison.
+
+  inspired by Vexflow's generate_png_images and vexflow-tests.js
+
+  This is meant to be used with the visual regression test system in
+  `tools/visual_regression.sh`.
+*/
+
+function sleep (ms) {
+    return new Promise((resolve) => {
+        setTimeout(resolve, ms)
+    })
+}
+
+const osmdPort = 8000 // OSMD webpack server port. OSMD has to be running (npm start) when this script runs.
+
+// try this to debug: node --inspect=9229 test/Util/generateDiffImagesPuppeteerLocalhost.js test/data/ export/  5000
+
+// main function
+async function init () {
+    console.log('[OSMD.generate] init')
+
+    let [sampleDir, imageDir, pageWidth, pageHeight, filterRegex, debugFlag, debugSleepTimeString] = process.argv.slice(2, 9)
+    if (!sampleDir || !imageDir) {
+        console.log('usage: node test/Util/generateDiffImagesPuppeteerLocalhost.js sampleDirectory imageDirectory [width|0] [height|0] [filterRegex|all] [--debug] [debugSleepTime]')
+        console.log('  (use "all" to skip filterRegex parameter)')
+        console.log('example: node ./test/Util/generateDiffImagesPuppeteerLocalhost.js ./test/data/ ./export 210 297 all --debug 5000')
+        console.log('Error: need sampleDir and imageDir. Exiting.')
+        process.exit(1)
+    }
+    console.log('sampleDir: ' + sampleDir)
+    console.log('imageDir: ' + imageDir)
+
+    let pageFormatParameter = ''
+    pageHeight = Number.parseInt(pageHeight)
+    pageWidth = Number.parseInt(pageWidth)
+    const endlessPage = !(pageHeight > 0 && pageWidth > 0)
+    if (!endlessPage) {
+        pageFormatParameter = `&pageWidth=${pageWidth}&pageHeight=${pageHeight}`
+    }
+
+    const DEBUG = debugFlag === '--debug'
+    // const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
+    if (DEBUG) {
+        console.log('debug sleep time: ' + debugSleepTimeString)
+        const debugSleepTimeMs = Number.parseInt(debugSleepTimeString)
+        if (debugSleepTimeMs > 0) {
+            await sleep(Number.parseInt(debugSleepTimeMs))
+            // [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
+            // sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
+        }
+    }
+
+    const fs = require('fs')
+    // Create the image directory if it doesn't exist.
+    fs.mkdirSync(imageDir, { recursive: true })
+
+    const sampleDirFilenames = fs.readdirSync(sampleDir)
+    let samplesToProcess = [] // samples we want to process/generate pngs of, excluding the filtered out files/filenames
+    for (const sampleFilename of sampleDirFilenames) {
+        if (DEBUG) {
+            if (sampleFilename.match('^(Actor)|(Gounod)')) {
+                console.log('DEBUG: filtering big file: ' + sampleFilename)
+                continue
+            }
+        }
+        // eslint-disable-next-line no-useless-escape
+        if (sampleFilename.match('^.*(\.xml)|(\.musicxml)|(\.mxl)$')) {
+            // console.log('found musicxml/mxl: ' + sampleFilename)
+            samplesToProcess.push(sampleFilename)
+        } else {
+            console.log('discarded file/directory: ' + sampleFilename)
+        }
+    }
+
+    // filter samples to process by regex if given
+    if (filterRegex && filterRegex !== '' && filterRegex !== 'all') {
+        console.log('filtering samples for regex: ' + filterRegex)
+        samplesToProcess = samplesToProcess.filter((filename) => filename.match(filterRegex))
+        console.log(`found ${samplesToProcess.length} matches: `)
+        for (let i = 0; i < samplesToProcess.length; i++) {
+            console.log(samplesToProcess[i])
+        }
+    }
+
+    const puppeteer = require('puppeteer')
+    const browser = await puppeteer.launch({ headless: true })
+    const page = await browser.newPage() // TODO set width/height
+
+    const defaultTimeoutInMs = 30000
+    page.setDefaultNavigationTimeout(defaultTimeoutInMs) // default setting for page navigationtimeout is 30000ms.
+
+    // fix navigation error
+    var responseEventOccurred = false
+    var responseHandler = function (event) { responseEventOccurred = true }
+
+    var responseWatcher = new Promise(function (resolve, reject) {
+        setTimeout(function () {
+            if (!responseEventOccurred) {
+                resolve(true)
+            } else {
+                setTimeout(function () { resolve(true) }, defaultTimeoutInMs)
+            }
+            page.removeListener('response', responseHandler)
+        }, 1000)
+    })
+
+    page.on('response', responseHandler)
+    if (DEBUG) {
+        // pipe console output on the page to the console node is running from, otherwise these logs from the headless browser aren't visible
+        page.on('console', msg => console.log(msg.text()))
+    }
+    page.on('error', err => console.log(err))
+    page.on('pageerror', err => console.log(err)) // this one triggers for js errors in index.js, for example
+
+    // get image data
+    const getDataUrl = async (page, sampleFilename) => {
+        return page.evaluate(async () => {
+            return new Promise(resolve => {
+                const imageDataArray = []
+                let canvasImage
+
+                for (let pageNumber = 1; pageNumber < 999; pageNumber++) {
+                    canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)
+                    if (!canvasImage) {
+                        break
+                    }
+                    if (!canvasImage.toDataURL) {
+                        console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`)
+                        break
+                    }
+                    imageDataArray.push(canvasImage.toDataURL())
+                }
+                // while (canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)) {
+                //     imageData = canvasImage.toDataURL()
+                //     console.log("got em. " + pageNumber)
+                // }
+                // TODO fetch multiple pages from multiple OSMD backends
+                resolve(imageDataArray)
+            })
+        })
+    }
+
+    // generate png for all given samples
+    for (let i = 0; i < samplesToProcess.length; i++) {
+        const sampleFilename = encodeURIComponent(samplesToProcess[i]) // escape slashes, '&' and so on
+        const sampleParameter = `&openUrl=${sampleFilename}&endUrl`
+        const pageUrl = `http://localhost:${osmdPort}?showHeader=0&debugControls=0&backendType=canvas&pageBackgroundColor=FFFFFF` +
+            sampleParameter +
+            pageFormatParameter
+
+        console.log('puppeteer: page.goto url: ' + pageUrl)
+        try {
+            await page.goto(pageUrl, { waitUntil: 'networkidle2' })
+        } catch (error) {
+            console.log(error)
+            console.log('[OSMD.generateImages] Error generating images: could not reach local OSMD server. ' +
+                'Make sure to start OSMD local webpack server (npm start) before running this script.')
+            process.exit(-1) // exit script with error. otherwise process will continue running
+        }
+        console.log('puppeteer.page.goto done. (now fetching image data)')
+
+        var navigationWatcher = page.waitForNavigation()
+        await Promise.race([responseWatcher, navigationWatcher])
+        console.log('navigation race done')
+        const dataUrls = await getDataUrl(page, sampleFilename)
+        if (dataUrls.length === 0) {
+            console.log(`error: could not get imageData for sample: ${sampleFilename}`)
+            console.log('   (dataUrls was empty list)')
+            continue
+        }
+        for (let urlIndex = 0; urlIndex < dataUrls.length; urlIndex++) {
+            const pageNumberingString = `_${urlIndex + 1}`
+            // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
+            var pageFilename = `${imageDir}/${sampleFilename}${pageNumberingString}.png`
+
+            const dataUrl = dataUrls[urlIndex]
+            if (!dataUrl || !dataUrl.split) {
+                console.log(`error: could not get dataUrl (imageData) for page ${urlIndex + 1} of sample: ${sampleFilename}`)
+                continue
+            }
+            const imageData = dataUrl.split(';base64,').pop()
+            const imageBuffer = Buffer.from(imageData, 'base64')
+
+            console.log('got image data, saving to: ' + pageFilename)
+            fs.writeFileSync(pageFilename, imageBuffer, { encoding: 'base64' })
+        }
+        /* bneumann's SVG method */
+        // const clone = this.ctx.svg.cloneNode(true) // SVGElement
+        // // create a doctype that is SVG
+        // const svgDocType = document.implementation.createDocumentType(
+        //     'svg',
+        //     '-//W3C//DTD SVG 1.1//EN',
+        //     'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
+        // )
+        // // Create a new svg document
+        // const svgDoc = document.implementation.createDocument('http://www.w3.org/2000/svg', 'svg', svgDocType)
+        // // replace the documentElement with our clone
+        // svgDoc.replaceChild(clone, svgDoc.documentElement)
+        // // get the data
+        // const svgData = (new XMLSerializer()).serializeToString(svgDoc)
+        // var blob = new Blob([svgData.replace(/></g, '>\n\r<')])
+        // fs.writeFileSync(filename, blob)
+        // //fs.writeFileSync(filename, svgData)
+        // return
+        /* end bneumann's svg method */
+    }
+
+    // const html = await page.content();
+    // console.log('page content: ' + html);
+    browser.close()
+    console.log('\n[OSMD.generate] Done. Puppeteer browser closed. Exiting.')
+}
+
+init()
+
+// function start() {
+//     // await (async () => {
+//     //     init();
+//     // });
+
+//     (async function(){
+//         await init();
+//         // more code here or the await is useless
+//     })();
+// }
+
+// function resizeCanvas (elementId, width, height) {
+//     $('#' + elementId).width(width)
+//     $('#' + elementId).attr('width', width)
+//     $('#' + elementId).attr('height', height)
+// }

+ 272 - 0
test/Util/generateImages_browserless.js

@@ -0,0 +1,272 @@
+/*
+  Render each OSMD sample, grab the generated images, and
+  dump them into a local directory as PNG files.
+
+  inspired by Vexflow's generate_png_images and vexflow-tests.js
+
+  This can be used to generate PNGs from OSMD without a browser.
+  It's also used with the visual regression test system in
+  `tools/visual_regression.sh`.
+
+  Note: this script needs to "fake" quite a few browser elements, like window, document, and a Canvas HTMLElement.
+  For that it needs the canvas package installed.
+  There are also some hacks needed to set the container size (offsetWidth) correctly.
+
+  Otherwise you'd need to run a headless browser, which is way slower,
+  see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
+*/
+
+function sleep (ms) {
+    return new Promise((resolve) => {
+        setTimeout(resolve, ms)
+    })
+}
+
+async function init () {
+    console.log('[OSMD.generate] init')
+
+    let [osmdBuildDir, sampleDir, imageDir, pageWidth, pageHeight, filterRegex, debugFlag, debugSleepTimeString] = process.argv.slice(2, 10)
+    if (!osmdBuildDir || !sampleDir || !imageDir) {
+        console.log('usage: ' +
+            'node test/Util/generateImages_browserless.js osmdBuildDir sampleDirectory imageDirectory [width|0] [height|0] [filterRegex|all|allSmall] [--debug] [debugSleepTime]')
+        console.log('  (use "all" to skip filterRegex parameter. "allSmall" skips two huge OSMD samples that take forever to render)')
+        console.log('example: node test/Util/generateImages_browserless.js ../../build ./test/data/ ./export 210 297 allSmall --debug 5000')
+        console.log('Error: need sampleDir and imageDir. Exiting.')
+        process.exit(1)
+    }
+
+    const DEBUG = debugFlag === '--debug'
+    // const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
+    if (DEBUG) {
+        console.log(' (note that --debug slows down the script by about 0.3s per file, through logging)')
+        const debugSleepTimeMs = Number.parseInt(debugSleepTimeString)
+        if (debugSleepTimeMs > 0) {
+            console.log('debug sleep time: ' + debugSleepTimeString)
+            await sleep(Number.parseInt(debugSleepTimeMs))
+            // [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
+            // sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
+        }
+    }
+    debug('sampleDir: ' + sampleDir, DEBUG)
+    debug('imageDir: ' + imageDir, DEBUG)
+
+    let pageFormat = 'Endless'
+    pageWidth = Number.parseInt(pageWidth)
+    pageHeight = Number.parseInt(pageHeight)
+    const endlessPage = !(pageHeight > 0 && pageWidth > 0)
+    if (!endlessPage) {
+        pageFormat = `${pageWidth}x${pageHeight}`
+    }
+
+    // ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
+    const { JSDOM } = require('jsdom')
+    const dom = new JSDOM('<!DOCTYPE html></html>')
+    // eslint-disable-next-line no-global-assign
+    window = dom.window
+    // eslint-disable-next-line no-global-assign
+    document = dom.window.document
+
+    // eslint-disable-next-line no-global-assign
+    global.window = dom.window
+    // eslint-disable-next-line no-global-assign
+    global.document = window.document
+    global.HTMLElement = window.HTMLElement
+    global.HTMLAnchorElement = window.HTMLAnchorElement
+    global.XMLHttpRequest = window.XMLHttpRequest
+    global.DOMParser = window.DOMParser
+    global.Node = window.Node
+    global.Canvas = window.Canvas
+
+    // fix Blob not found (to support external modules like is-blob)
+    global.Blob = require('cross-blob')
+
+    const div = document.createElement('div')
+    div.id = 'browserlessDiv'
+    document.body.appendChild(div)
+    // const canvas = document.createElement('canvas')
+    // div.canvas = document.createElement('canvas')
+
+    const zoom = 1.0
+    // width of the div / PNG generated
+    let width = pageWidth * zoom
+    // TODO sometimes the width is way too small for the score, may need to adjust zoom.
+    if (endlessPage) {
+        width = 1440
+    }
+    let height = pageHeight
+    if (endlessPage) {
+        height = 32767
+    }
+    div.width = width
+    div.height = height
+    div.offsetWidth = width // doesn't work, offsetWidth is always 0 from this. see below
+    div.clientWidth = width
+    div.clientHeight = height
+    div.scrollHeight = height
+    div.scrollWidth = width
+    div.setAttribute('width', width)
+    div.setAttribute('height', height)
+    div.setAttribute('offsetWidth', width)
+    debug('div.offsetWidth: ' + div.offsetWidth, DEBUG)
+    debug('div.height: ' + div.height, DEBUG)
+
+    // hack: set offsetWidth reliably
+    Object.defineProperties(window.HTMLElement.prototype, {
+        offsetLeft: {
+            get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0 }
+        },
+        offsetTop: {
+            get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0 }
+        },
+        offsetHeight: {
+            get: function () { return height }
+        },
+        offsetWidth: {
+            get: function () { return width }
+        }
+    })
+    debug('div.offsetWidth: ' + div.offsetWidth, DEBUG)
+    debug('div.height: ' + div.height, DEBUG)
+    if (!DEBUG) {
+        // deactivate console logs (mostly)
+        console.log = (msg) => {}
+    }
+    // ---- end browser hacks (hopefully) ----
+
+    const OSMD = require(`${osmdBuildDir}/opensheetmusicdisplay.min.js`)
+
+    const fs = require('fs')
+    // Create the image directory if it doesn't exist.
+    fs.mkdirSync(imageDir, { recursive: true })
+
+    const sampleDirFilenames = fs.readdirSync(sampleDir)
+    let samplesToProcess = [] // samples we want to process/generate pngs of, excluding the filtered out files/filenames
+    for (const sampleFilename of sampleDirFilenames) {
+        if (filterRegex === 'allSmall') {
+            if (sampleFilename.match('^(Actor)|(Gounod)')) { // TODO maybe filter by file size instead
+                debug('filtering big file: ' + sampleFilename, DEBUG)
+                continue
+            }
+        }
+        // eslint-disable-next-line no-useless-escape
+        if (sampleFilename.match('^.*(\.xml)|(\.musicxml)|(\.mxl)$')) {
+            // console.log('found musicxml/mxl: ' + sampleFilename)
+            samplesToProcess.push(sampleFilename)
+        } else {
+            debug('discarded file/directory: ' + sampleFilename, DEBUG)
+        }
+    }
+
+    // filter samples to process by regex if given
+    if (filterRegex && filterRegex !== '' && filterRegex !== 'all' && filterRegex !== 'allSmall') {
+        debug('filtering samples for regex: ' + filterRegex, DEBUG)
+        samplesToProcess = samplesToProcess.filter((filename) => filename.match(filterRegex))
+        debug(`found ${samplesToProcess.length} matches: `, DEBUG)
+        for (let i = 0; i < samplesToProcess.length; i++) {
+            debug(samplesToProcess[i], DEBUG)
+        }
+    }
+
+    const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
+        autoResize: false,
+        backend: 'canvas',
+        pageBackgroundColor: '#FFFFFF',
+        pageFormat: pageFormat
+        // defaultFontFamily: 'Arial',
+        // drawTitle: false
+    })
+    // for more options check OSMDOptions.ts
+
+    // you can set finer-grained rendering/engraving settings in EngravingRules:
+    osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 9.0 is default
+    osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
+    osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
+    // note that for now the png and canvas will still have the height given in the script argument,
+    //   so even with a margin of 0 the image will be filled to the full height.
+    osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
+    osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
+    // osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
+    // osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
+    // for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
+
+    // await sleep(5000)
+    if (DEBUG) {
+        osmdInstance.setLogLevel('debug')
+        // console.log(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
+        console.log(`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`)
+        console.log('PageHeight: ' + osmdInstance.EngravingRules.PageHeight)
+    } else {
+        osmdInstance.setLogLevel('info') // doesn't seem to work, log.debug still logs
+    }
+
+    debug('generateImages', DEBUG)
+    for (let i = 0; i < samplesToProcess.length; i++) {
+        var sampleFilename = samplesToProcess[i]
+        debug('sampleFilename: ' + sampleFilename, DEBUG)
+
+        let loadParameter = fs.readFileSync(sampleDir + '/' + sampleFilename)
+        if (sampleFilename.endsWith('.mxl')) {
+            loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter)
+        } else {
+            loadParameter = loadParameter.toString()
+        }
+        // console.log('loadParameter: ' + loadParameter)
+        // console.log('typeof loadParameter: ' + typeof loadParameter)
+
+        await osmdInstance.load(loadParameter).then(function () {
+            debug('xml loaded', DEBUG)
+            try {
+                osmdInstance.render()
+            } catch (ex) {
+                console.log('renderError: ' + ex)
+            }
+            debug('rendered', DEBUG)
+
+            const dataUrls = []
+            let canvasImage
+
+            for (let pageNumber = 1; pageNumber < 999; pageNumber++) {
+                canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)
+                if (!canvasImage) {
+                    break
+                }
+                if (!canvasImage.toDataURL) {
+                    console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`)
+                    break
+                }
+                dataUrls.push(canvasImage.toDataURL())
+            }
+            for (let urlIndex = 0; urlIndex < dataUrls.length; urlIndex++) {
+                const pageNumberingString = `_${urlIndex + 1}`
+                // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
+                var pageFilename = `${imageDir}/${sampleFilename}${pageNumberingString}.png`
+
+                const dataUrl = dataUrls[urlIndex]
+                if (!dataUrl || !dataUrl.split) {
+                    console.log(`error: could not get dataUrl (imageData) for page ${urlIndex + 1} of sample: ${sampleFilename}`)
+                    continue
+                }
+                const imageData = dataUrl.split(';base64,').pop()
+                const imageBuffer = Buffer.from(imageData, 'base64')
+
+                debug('got image data, saving to: ' + pageFilename, DEBUG)
+                fs.writeFileSync(pageFilename, imageBuffer, { encoding: 'base64' })
+            }
+        }) // end render then
+        //     },
+        //     function (e) {
+        //         console.log('error while rendering: ' + e)
+        //     }) // end load then
+        // }) // end read file
+    }
+
+    console.log('[OSMD.generate_browserless] exit')
+}
+
+function debug (msg, debugEnabled) {
+    if (debugEnabled) {
+        console.log(msg)
+    }
+}
+
+init()

+ 243 - 0
test/Util/visual_regression.sh

@@ -0,0 +1,243 @@
+#!/bin/bash
+# This script runs a visual regression test on all the images
+# generated from OSMD samples (npm run generate:current and npm run generate:blessed)
+#
+#   inspired by and adapted from Vexflow's visual regression tests.
+#
+# Prerequisites: ImageMagick
+#
+# On OSX:   $ brew install imagemagick
+# On Linux: $ apt-get install imagemagick
+#
+# Usage:
+#
+#
+#  First generate the known good or previous state PNG images you want to compare to, e.g. the develop branch or last release:
+#    (Server has to be running for this: npm start)
+#
+#    npm run generate:blessed
+#
+#  Make changes in OSMD, then generate your new images:
+#
+#    npm run generate:current
+#
+#  Run the regression tests against the blessed images in visual_regression/blessed.
+#
+#    npm run test:visual
+#    # npm will navigate to the base folder automatically
+#
+#    # or: (this should be done from the main OSMD folder)
+#    # sh test/Util/visual_regression.sh [imageBaseFolder] [sampleShellPrefix]
+#    #    example: sh test/Util/visual_regression.sh ./visual_regression OSMD_function_test_
+#    #        will run visual regression tests for all images matching OSMD_function_test_*.png.
+#
+#  Check visual_regression/diff/results.txt for results. This file is sorted
+#  by PHASH difference (most different files on top.) The composite diff
+#  images for failed tests (i.e., PHASH > 1.0) are stored in visual_regression/diff.
+#
+#  (If you are satisfied with the differences, copy *.png from visual_regression/current
+#  into visual_regression/blessed, and submit your change (TODO))
+
+# PNG viewer on OSX. Switch this to whatever your system uses.
+# VIEWER=open
+
+# Show images over this PHASH threshold.
+# 0.01 is probably too low, but a good first pass.
+# 0.0001 catches for example a repetition ending not having a down line at the end (see Saltarello bar 10) (0.001 doesn't catch this)
+THRESHOLD=0.0001
+
+# Set up Directories
+#   It does not matter where this script is executed, as long as these folders are given correctly (and blessed/current have png images set up correctly)
+BUILDFOLDER=./visual_regression
+if [ "$1" != "" ]
+then
+  BUILDFOLDER=$1
+fi
+BLESSED=$BUILDFOLDER/blessed
+CURRENT=$BUILDFOLDER/current
+DIFF=$BUILDFOLDER/diff
+# diff also acts as the temp folder here, unlike in Vexflow, where it is current.
+# it would be nice to have a tmp folder (for temporary files), but we'd want to delete the folder entirely, and we'd better not risk using rm -rf in a script
+
+# All results are stored here.
+RESULTS=$DIFF/results.txt
+WARNINGS=$DIFF/warnings.txt
+
+# If no prefix is provided, test all images.
+if [ "$2" == "" ]
+then
+  files=*.png
+else
+  files=$2*.png
+  echo "image filter (shell): $files"
+fi
+
+## Sanity checks: some simple checks that the script can run correctly (doesn't validate pngs)
+folderWarningStringMsg="Exiting without running visual regression tests."
+totalCurrentImages=`ls -1 $CURRENT/$files | wc -l | xargs` # xargs trims space
+if [ $? -ne 0 ] || [ "$totalCurrentImages" -lt 1 ] # $? returns the exit code of the previous command (ls). (0 is success)
+then
+  echo Missing images in $CURRENT.
+  echo Please run \"npm run generate:current\"
+  exit 1
+fi
+
+totalBlessedImages=`ls -1 $BLESSED/$files | wc -l | xargs`
+if [ $? -ne 0 ] || [ "$totalBlessedImages" -lt 1 ]
+then
+  echo Missing images in $BLESSED.
+  echo Please run \"npm run generate:blessed\"
+  exit 1
+fi
+
+# check that #currentImages == #blessedImages (will continue anyways)
+if [ ! "$totalCurrentImages" -eq "$totalBlessedImages" ]
+then
+  echo "Warning: Number of current images ($totalCurrentImages) is not the same as blessed images ($totalBlessedImages). Continuing anyways.\n"
+else
+  echo "Found $totalCurrentImages current and $totalBlessedImages blessed png files (not tested if valid). Continuing.\n"
+fi
+# ----------------- end of sanity checks -----------------
+
+mkdir -p $DIFF
+if [ -e "$RESULTS" ]
+then
+  rm $DIFF/*
+fi
+touch $RESULTS
+touch $RESULTS.fails
+#   this shouldn't be named .fail because we have a *.fail shell match further below, which will loop endlessly if files are in the same folder (diff).
+touch $WARNINGS
+
+# Number of simultaneous jobs
+nproc=$(sysctl -n hw.physicalcpu 2> /dev/null || nproc)
+if [ -n "$NPROC" ]; then
+  nproc=$NPROC
+fi
+
+total=`ls -l $BLESSED/$files | wc -l | sed 's/[[:space:]]//g'`
+
+echo "Running $total tests with threshold $THRESHOLD (nproc=$nproc)..."
+
+function ProgressBar {
+    let _progress=(${1}*100/${2}*100)/100
+    let _done=(${_progress}*4)/10
+    let _left=40-$_done
+    _fill=$(printf "%${_done}s")
+    _empty=$(printf "%${_left}s")
+
+    printf "\rProgress : [${_fill// /#}${_empty// /-}] ${_progress}%%"
+}
+
+function diff_image() {
+  local image=$1
+  local name=`basename $image .png`
+  local blessed=$BLESSED/$name.png
+  local current=$CURRENT/$name.png
+  local diff=$DIFF/$name.png-temp
+
+  if [ ! -e "$current" ]
+  then
+    echo " Warning: $name.png missing in $CURRENT. Skipped." >$diff.warn
+    #((total--))
+    return
+  fi
+
+  if [ ! -e "$blessed" ]
+  then
+    echo " Warning: $name.png doesn't exist in $BLESSED. Skipped." >$diff.warn
+    #((total--))
+    return
+  fi
+
+  cp $blessed $diff-a.png
+  cp $current $diff-b.png
+
+  # Calculate the difference metric and store the composite diff image.
+  local hash=`compare -metric PHASH -highlight-color '#ff000050' $diff-b.png $diff-a.png $diff-diff.png 2>&1`
+
+  local isGT=`echo "$hash > $THRESHOLD" | bc -l`
+  if [ "$isGT" == "1" ]
+  then
+    # Add the result to results.txt
+    echo $name $hash >$diff.fail
+    # Threshold exceeded, save the diff and the original, current
+    cp $diff-diff.png $DIFF/$name.png
+    cp $diff-a.png $DIFF/$name'_'Blessed.png
+    cp $diff-b.png $DIFF/$name'_'Current.png
+    echo
+    echo "Test: $name"
+    echo "  PHASH value exceeds threshold: $hash > $THRESHOLD"
+    echo "  Image diff stored in $DIFF/$name.png"
+    # $VIEWER "$diff-diff.png" "$diff-a.png" "$diff-b.png"
+    # echo 'Hit return to process next image...'
+    # read
+  else
+    echo $name $hash >$diff.pass
+  fi
+  rm -f $diff-a.png $diff-b.png $diff-diff.png
+}
+
+function wait_jobs () {
+  local n=$1
+  while [[ "$(jobs -r | wc -l)" -ge "$n" ]] ; do
+     # echo ===================================== && jobs -lr
+     # wait the oldest job.
+     local pid_to_wait=`jobs -rp | head -1`
+     # echo wait $pid_to_wait
+     wait $pid_to_wait  &> /dev/null
+  done
+}
+
+count=0
+for image in $CURRENT/$files
+do
+  count=$((count + 1))
+  ProgressBar ${count} ${total}
+  wait_jobs $nproc
+  diff_image $image &
+done
+wait
+
+cat $DIFF/*.warn 1>$WARNINGS 2>/dev/null
+rm -f $DIFF/*.warn
+
+## Check for files newly built that are not yet blessed.
+for image in $CURRENT/$files
+do
+  name=`basename $image .png`
+  blessed=$BLESSED/$name.png
+  current=$CURRENT/$name.png
+done
+
+num_warnings=`cat $WARNINGS | wc -l`
+
+cat $DIFF/*.fail 1>$RESULTS.fails 2>/dev/null
+num_fails=`cat $RESULTS.fails | wc -l`
+rm -f  $DIFF/*.fail
+
+# Sort results by PHASH
+sort -r -n -k 2 $RESULTS.fails >$RESULTS
+sort -r -n -k 2 $DIFF/*.pass 1>>$RESULTS 2>/dev/null
+rm -f $DIFF/*.pass $RESULTS.fails
+
+echo
+echo Results stored in $DIFF/results.txt
+echo All images with a difference over threshold, $THRESHOLD, are
+echo available in $DIFF, sorted by perceptual hash.
+echo
+
+if [ "$num_warnings" -gt 0 ]
+then
+  echo
+  echo "You have $num_warnings warning(s):"
+  cat $WARNINGS
+fi
+
+if [ "$num_fails" -gt 0 ]
+then
+  echo "You have $num_fails fail(s):"
+  head -n $num_fails $RESULTS
+else
+  echo "Success - All diffs under threshold!"
+fi

+ 1 - 1
test/data/OSMD_function_test_Ornaments.xml

@@ -2,7 +2,7 @@
 <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
 <score-partwise version="3.1">
   <work>
-    <work-title>OSMD function test - Ornaments</work-title>
+    <work-title>OSMD Function Test - Ornaments</work-title>
     </work>
   <identification>
     <encoding>

+ 1 - 1
tsconfig.json

@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "target": "es5",
+    "target": "es2017",
     "module": "commonjs",
     "moduleResolution": "node",
     "noUnusedLocals": true,

+ 11 - 1
webpack.dev.js

@@ -1,7 +1,17 @@
 var merge = require('webpack-merge')
 var common = require('./webpack.common.js')
+var webpack = require('webpack')
 
 module.exports = merge(common, {
     devtool: 'inline-source-map',
-    mode: 'development'
+    mode: 'development',
+    plugins: [
+        new webpack.EnvironmentPlugin({
+            STATIC_FILES_SUBFOLDER: false, // Set to other directory if NOT using webpack-dev-server
+            DEBUG: false,
+            OSMD_DEBUG_CONTROLS: true, // unfortunately, cross-env doesn't seem enough to set this in the demo when using npm start
+            OSMD_DEMO_TITLE: 'OpenSheetMusicDisplay Demo (Developer)',
+            DRAW_BOUNDING_BOX_ELEMENT: false //  Specifies the element to draw bounding boxes for (e.g. 'GraphicalLabels'). If 'all', bounding boxes are drawn for all elements.
+        })
+    ]
 })