Sfoglia il codice sorgente

merge osmd-public/develop (1.8.5): many fixes, see public osmd changelog

sschmidTU 1 anno fa
parent
commit
954ac830fb

+ 1 - 1
.appveyor.yml

@@ -3,7 +3,7 @@ environment:
   timeout: 20000
   matrix:
     # - nodejs_version: "12"
-    - nodejs_version: "16"
+    - nodejs_version: "20"
 platform:
   # - x86
   - x64

+ 1 - 1
karma.conf.js

@@ -105,7 +105,7 @@ module.exports = function (config) {
         autoWatch: false,
 
         // start these browsers
-        browsers: ['ChromeHeadlessNoSandbox'],
+        browsers: ['FirefoxHeadless'],
 
         // For security reasons, Google Chrome is unable to provide sandboxing
         // when it is running in container-based environments (e.g. CI).

+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "osmd-extended",
-  "version": "1.8.4",
+  "version": "1.8.5",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "main": "build/opensheetmusicdisplay.min.js",
   "types": "build/dist/src/index.d.ts",
@@ -102,10 +102,10 @@
     "jquery": "^3.6.0",
     "jsdom": "^21.0.0",
     "jspdf": "^2.5.1",
-    "karma": "^6.3.9",
+    "karma": "^6.4.2",
     "karma-base64-to-js-preprocessor": "^0.1.0",
     "karma-chai": "^0.1.0",
-    "karma-chrome-launcher": "^3.1.0",
+    "karma-chrome-launcher": "^3.2.0",
     "karma-firefox-launcher": "^2.1.2",
     "karma-mocha": "^2.0.1",
     "karma-mocha-reporter": "^2.2.5",

+ 2 - 1
src/Common/DataObjects/Fraction.ts

@@ -64,7 +64,8 @@ export class Fraction {
       return 1;
     }
 
-    while (Math.abs(b) > 1e-8) { // essentially b > 0, accounts for floating point inaccuracies (0.000...01)
+    while (Math.abs(b) > 1e-8 && Math.abs(a) > 1e-8) { // essentially b > 0, accounts for floating point inaccuracies (0.000...01)
+      // if we don't check a > 1e-8, we infinite loop for e.g. a = 2.666666666666667, b = 4. See #1478 (rare)
       if (a > b) {
         a -= b;
       } else {

+ 2 - 0
src/MusicalScore/Graphical/EngravingRules.ts

@@ -412,6 +412,7 @@ export class EngravingRules {
     public DynamicExpressionMaxDistance: number;
     public DynamicExpressionSpacer: number;
     public IgnoreRepeatedDynamics: boolean;
+    public ExpressionsUseXMLColor: boolean;
     public MpatMode: boolean;
 
     public ArticulationPlacementFromXML: boolean;
@@ -752,6 +753,7 @@ export class EngravingRules {
         this.DynamicExpressionMaxDistance = 2;
         this.DynamicExpressionSpacer = 0.5;
         this.IgnoreRepeatedDynamics = false;
+        this.ExpressionsUseXMLColor = true;
 
         // Line Widths
         this.VexFlowDefaultNotationFontScale = 39; // scales notes, including rests. default value 39 in Vexflow.

+ 1 - 0
src/MusicalScore/Graphical/GraphicalLabel.ts

@@ -19,6 +19,7 @@ export class GraphicalLabel extends Clickable {
     public SVGNode: Node;
     /** Read-only informational variable only set once by lyrics centering algorithm. */
     public CenteringXShift: number = 0;
+    public ColorXML: string;
 
     /**
      * Creates a new GraphicalLabel from a Label

+ 9 - 1
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -1422,7 +1422,12 @@ export abstract class MusicSheetCalculator {
         const startCollideBox: BoundingBox =
             this.dynamicExpressionMap.get(graphicalContinuousDynamic.ContinuousDynamic.StartMultiExpression.AbsoluteTimestamp.RealValue);
         if (startCollideBox) {
-            startPosInStaffline.x = startCollideBox.RelativePosition.x + startCollideBox.BorderMarginRight + this.rules.WedgeHorizontalMargin;
+            startPosInStaffline.x = startCollideBox.RelativePosition.x + this.rules.WedgeHorizontalMargin;
+            if ((startCollideBox.DataObject as any).ParentStaffLine === staffLine) {
+                // TODO the dynamicExpressionMap doesn't distinguish between staffLines, so we may react to a different staffline otherwise
+                //   so the more fundamental solution would be to fix dynamicExpressionMap mapping across stafflines.
+                startPosInStaffline.x += startCollideBox.BorderMarginRight;
+            }
         }
         //currentMusicSystem and currentStaffLine
         const musicSystem: MusicSystem = staffLine.ParentMusicSystem;
@@ -1960,6 +1965,9 @@ export abstract class MusicSheetCalculator {
                                                                        this.rules.UnknownTextHeight,
                                                                        textAlignment,
                                                                        this.rules.TempoYSpacing);
+                if (entry.Expression.ColorXML && this.rules.ExpressionsUseXMLColor) {
+                    graphLabel.ColorXML = entry.Expression.ColorXML;
+                }
 
                 if (entry.Expression instanceof InstantaneousTempoExpression) {
                     //already added?

+ 10 - 3
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -34,7 +34,6 @@ import { GraphicalGlissando } from "../GraphicalGlissando";
 import { VexFlowGlissando } from "./VexFlowGlissando";
 import { VexFlowGraphicalNote } from "./VexFlowGraphicalNote";
 import { SvgVexFlowBackend } from "./SvgVexFlowBackend";
-import { CanvasVexFlowBackend } from "./CanvasVexFlowBackend";
 import { VexflowVibratoBracket } from "./VexflowVibratoBracket";
 
 /**
@@ -250,12 +249,17 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
                     let stemTip: PointF2D;
                     let stemHeight: number;
                     const directionSign: number = vfNote.vfnote[0].getStemDirection(); // 1 or -1
+                    let stemElement: HTMLElement;
                     if (this.backend instanceof SvgVexFlowBackend) {
-                        const stemElement: HTMLElement = vfNote.getStemSVG();
+                        stemElement = vfNote.getStemSVG();
+                    }
+                    const hasBbox: boolean = (stemElement as any)?.getBbox !== undefined;
+                    if (hasBbox) {
+                        // apparently sometimes the stemElement is null, in that case we need to use the canvas method.
                         const rect: SVGRect = (stemElement as any).getBBox();
                         stemTip = new PointF2D(rect.x / 10, rect.y / 10);
                         stemHeight = rect.height / 10;
-                    } else if (this.backend instanceof CanvasVexFlowBackend) {
+                    } else { // if this.backend instanceof CanvasVexFlowBackend // also seems to work for SVG
                         stemHeight = vfNote.vfnote[0].getStemLength() / 10;
                         stemTip = new PointF2D(
                             (vfNote.vfnote[0].getStem() as any).x_begin / 10,
@@ -603,6 +607,9 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
         let color: string;
         if (this.rules.ColoringEnabled) {
             color = graphicalLabel.Label.colorDefault;
+            if (graphicalLabel.ColorXML) {
+                color = graphicalLabel.ColorXML;
+            }
             if (graphicalLabel.Label.color) {
                 color = graphicalLabel.Label.color.toString();
             }

+ 16 - 6
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -572,6 +572,7 @@ export class ExpressionReader {
         const fontStyleAttr: Attr = wordsNode.attribute("font-style");
         let fontStyleText: string;
         let fontWeightText: string;
+        let fontColor: string;
         if (fontStyleAttr) {
             fontStyleText = fontStyleAttr.value;
             if (fontStyleText === "italic") {
@@ -588,6 +589,10 @@ export class ExpressionReader {
                 }
             }
         }
+        const colorAttr: Attr = wordsNode.attribute("color");
+        if (colorAttr) {
+            fontColor = colorAttr.value;
+        }
         let defaultYXml: number;
         if (currentMeasure.Rules.PlaceWordsInsideStafflineFromXml) {
             const defaultYString: string = wordsNode.attribute("default-y")?.value;
@@ -602,7 +607,7 @@ export class ExpressionReader {
             if (this.checkIfWordsNodeIsRepetitionInstruction(text)) {
                 return;
             }
-            this.fillMultiOrTempoExpression(text, currentMeasure, inSourceMeasureCurrentFraction, fontStyle, defaultYXml);
+            this.fillMultiOrTempoExpression(text, currentMeasure, inSourceMeasureCurrentFraction, fontStyle, fontColor, defaultYXml);
             this.initialize();
         }
     }
@@ -706,9 +711,8 @@ export class ExpressionReader {
                 } else if (type === "stop") {
                     for (const openCont of this.openContinuousDynamicExpressions) {
                         if (openCont.NumberXml === numberXml) {
-                            if (openCont.NumberXml === numberXml) {
-                                this.closeOpenContinuousDynamic(openCont, currentMeasure, inSourceMeasureCurrentFraction);
-                            }
+                            // if (openCont.NumberXml === numberXml) { // was there supposed to be another check here? someone wrote the same check twice.
+                            this.closeOpenContinuousDynamic(openCont, currentMeasure, inSourceMeasureCurrentFraction);
                         }
                     }
                 }
@@ -720,7 +724,7 @@ export class ExpressionReader {
         }
     }
     private fillMultiOrTempoExpression(inputString: string, currentMeasure: SourceMeasure, inSourceMeasureCurrentFraction: Fraction,
-        fontStyle: FontStyles, defaultYXml: number = undefined): void {
+        fontStyle: FontStyles, fontColor: string, defaultYXml: number = undefined): void {
         if (!inputString) {
             return;
         }
@@ -729,7 +733,7 @@ export class ExpressionReader {
         //const splitStrings: string[] = tmpInputString.split(/([\s,\r\n]and[\s,\r\n]|[\s,\r\n]und[\s,\r\n]|[\s,\r\n]e[\s,\r\n]|[\s,\r\n])+/g);
 
         //for (const splitStr of splitStrings) {
-        this.createExpressionFromString("", tmpInputString, currentMeasure, inSourceMeasureCurrentFraction, inputString, fontStyle, defaultYXml);
+        this.createExpressionFromString("", tmpInputString, currentMeasure, inSourceMeasureCurrentFraction, inputString, fontStyle, fontColor, defaultYXml);
         //}
     }
     /*
@@ -763,6 +767,7 @@ export class ExpressionReader {
     private createExpressionFromString(prefix: string, stringTrimmed: string,
                                        currentMeasure: SourceMeasure, inSourceMeasureCurrentFraction, inputString: string,
                                        fontStyle: FontStyles,
+                                       fontColor: string,
                                        defaultYXml: number = undefined): boolean {
         if (InstantaneousTempoExpression.isInputStringInstantaneousTempo(stringTrimmed) ||
             ContinuousTempoExpression.isInputStringContinuousTempo(stringTrimmed)) {
@@ -785,6 +790,7 @@ export class ExpressionReader {
                                                                                                                       this.staffNumber,
                                                                                                                       this.soundTempo,
                                                                                                                       this.currentMultiTempoExpression);
+                instantaneousTempoExpression.ColorXML = fontColor;
                 this.currentMultiTempoExpression.addExpression(instantaneousTempoExpression, prefix);
                 return true;
             }
@@ -794,6 +800,7 @@ export class ExpressionReader {
                     this.placement,
                     this.staffNumber,
                     this.currentMultiTempoExpression);
+                continuousTempoExpression.ColorXML = fontColor;
                 this.currentMultiTempoExpression.addExpression(continuousTempoExpression, prefix);
                 return true;
             }
@@ -825,6 +832,7 @@ export class ExpressionReader {
                     this.activeInstantaneousDynamic,
                     -1,
                     stringTrimmed);
+            continuousDynamicExpression.ColorXML = fontColor;
             const openWordContinuousDynamic: MultiExpression = this.getMultiExpression;
             if (openWordContinuousDynamic) {
                 this.closeOpenContinuousDynamic(openWordContinuousDynamic.StartingContinuousDynamic, currentMeasure, inSourceMeasureCurrentFraction);
@@ -843,6 +851,7 @@ export class ExpressionReader {
             currentMeasure.hasMoodExpressions = true;
             const moodExpression: MoodExpression = new MoodExpression(stringTrimmed, this.placement, this.staffNumber);
             moodExpression.fontStyle = fontStyle;
+            moodExpression.ColorXML = fontColor;
             multiExpression.addExpression(moodExpression, prefix);
             return true;
         }
@@ -876,6 +885,7 @@ export class ExpressionReader {
         const unknownExpression: UnknownExpression = new UnknownExpression(
             stringTrimmed, this.placement, textAlignment, this.staffNumber);
         unknownExpression.fontStyle = fontStyle;
+        unknownExpression.ColorXML = fontColor;
         unknownExpression.defaultYXml = defaultYXml;
         unknownMultiExpression.addExpression(unknownExpression, prefix);
         return false;

+ 1 - 0
src/MusicalScore/VoiceData/Expressions/AbstractExpression.ts

@@ -3,6 +3,7 @@ import { SourceMeasure } from "../SourceMeasure";
 export class AbstractExpression {
     protected placement: PlacementEnum;
     public parentMeasure: SourceMeasure; // could be undefined
+    public ColorXML: string;
 
     constructor(placement: PlacementEnum) {
         this.placement = placement;

+ 1 - 1
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -35,7 +35,7 @@ import { DynamicsCalculator } from "../MusicalScore/ScoreIO/MusicSymbolModules/D
  * After the constructor, use load() and render() to load and render a MusicXML file.
  */
 export class OpenSheetMusicDisplay {
-    private version: string = "1.8.4-audio-extended"; // getter: this.Version
+    private version: string = "1.8.5-audio-extended"; // getter: this.Version
     // at release, bump version and change to -release, afterwards to -dev again
 
     /**

+ 269 - 0
src/VexFlowPatch/src/bend.js

@@ -0,0 +1,269 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+//
+// ## Description
+//
+// This file implements tablature bends.
+
+import { Vex } from './vex';
+import { Flow } from './tables';
+import { Modifier } from './modifier';
+/**
+   @param text Text for bend ("Full", "Half", etc.) (DEPRECATED)
+   @param release If true, render a release. (DEPRECATED)
+   @param phrase If set, ignore "text" and "release", and use the more
+                 sophisticated phrase specified.
+
+   Example of a phrase:
+
+     [{
+       type: UP,
+       text: "whole"
+       width: 8;
+     },
+     {
+       type: DOWN,
+       text: "whole"
+       width: 8;
+     },
+     {
+       type: UP,
+       text: "half"
+       width: 8;
+     },
+     {
+       type: UP,
+       text: "whole"
+       width: 8;
+     },
+     {
+       type: DOWN,
+       text: "1 1/2"
+       width: 8;
+     }]
+ */
+export class Bend extends Modifier {
+  static get CATEGORY() { return 'bends'; }
+
+  static get UP() {
+    return 0;
+  }
+  static get DOWN() {
+    return 1;
+  }
+
+  // ## Static Methods
+  // Arrange bends in `ModifierContext`
+  static format(bends, state) {
+    if (!bends || bends.length === 0) return false;
+
+    let last_width = 0;
+    // Bends are always on top.
+    const text_line = state.top_text_line;
+
+    // Format Bends
+    for (let i = 0; i < bends.length; ++i) {
+      const bend = bends[i];
+      bend.setXShift(last_width);
+      last_width = bend.getWidth();
+      bend.setTextLine(text_line);
+    }
+
+    state.right_shift += last_width;
+    state.top_text_line += 1;
+    return true;
+  }
+
+  // ## Prototype Methods
+  constructor(text, release, phrase) {
+    super();
+    this.setAttribute('type', 'Bend');
+
+    this.text = text;
+    this.x_shift = 0;
+    this.release = release || false;
+    this.font = '10pt Arial';
+    this.render_options = {
+      line_width: 1.5,
+      line_style: '#777777',
+      bend_width: 8,
+      release_width: 8,
+    };
+
+    if (phrase) {
+      this.phrase = phrase;
+    } else {
+      // Backward compatibility
+      this.phrase = [{ type: Bend.UP, text: this.text }];
+      if (this.release) this.phrase.push({ type: Bend.DOWN, text: '' });
+    }
+
+    this.updateWidth();
+  }
+
+  getCategory() { return Bend.CATEGORY; }
+
+  setXShift(value) {
+    this.x_shift = value;
+    this.updateWidth();
+  }
+  setFont(font) { this.font = font; return this; }
+  getText() { return this.text; }
+  updateWidth() {
+    const that = this;
+
+    function measure_text(text) {
+      let text_width;
+      if (that.context) {
+        text_width = that.context.measureText(text).width;
+      } else {
+        text_width = Flow.textWidth(text);
+      }
+
+      return text_width;
+    }
+
+    let total_width = 0;
+    for (let i = 0; i < this.phrase.length; ++i) {
+      const bend = this.phrase[i];
+      if ('width' in bend) {
+        total_width += bend.width;
+      } else {
+        const additional_width = (bend.type === Bend.UP) ?
+          this.render_options.bend_width : this.render_options.release_width;
+
+        bend.width = Vex.Max(additional_width, measure_text(bend.text)) + 3;
+        bend.draw_width = bend.width / 2;
+        total_width += bend.width;
+      }
+    }
+
+    this.setWidth(total_width + this.x_shift);
+    return this;
+  }
+  draw() {
+    this.checkContext();
+    if (!(this.note && (this.index != null))) {
+      throw new Vex.RERR('NoNoteForBend', "Can't draw bend without a note or index.");
+    }
+
+    this.setRendered();
+
+    const start = this.note.getModifierStartXY(Modifier.Position.RIGHT,
+      this.index);
+    start.x += 3;
+    start.y += 0.5;
+    const x_shift = this.x_shift;
+
+    const ctx = this.context;
+    const bend_height = this.note.getStave().getYForTopText(this.text_line) + 3;
+    const annotation_y = this.note.getStave().getYForTopText(this.text_line) - 1;
+    const that = this;
+
+    function renderBend(x, y, width, height) {
+      const cp_x = x + width;
+      const cp_y = y;
+
+      ctx.save();
+      ctx.beginPath();
+      ctx.setLineWidth(that.render_options.line_width);
+      ctx.setStrokeStyle(that.render_options.line_style);
+      ctx.setFillStyle(that.render_options.line_style);
+      ctx.moveTo(x, y);
+      ctx.quadraticCurveTo(cp_x, cp_y, x + width, height);
+      ctx.stroke();
+      ctx.restore();
+    }
+
+    function renderRelease(x, y, width, height) {
+      ctx.save();
+      ctx.beginPath();
+      ctx.setLineWidth(that.render_options.line_width);
+      ctx.setStrokeStyle(that.render_options.line_style);
+      ctx.setFillStyle(that.render_options.line_style);
+      ctx.moveTo(x, height);
+      ctx.quadraticCurveTo(
+        x + width, height,
+        x + width, y);
+      ctx.stroke();
+      ctx.restore();
+    }
+
+    function renderArrowHead(x, y, direction) {
+      const width = 4;
+      const dir = direction || 1;
+
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.lineTo(x - width, y + width * dir);
+      ctx.lineTo(x + width, y + width * dir);
+      ctx.closePath();
+      ctx.fill();
+    }
+
+    function renderText(x, text) {
+      ctx.save();
+      ctx.setRawFont(that.font);
+      const render_x = x - (ctx.measureText(text).width / 2);
+      ctx.fillText(text, render_x, annotation_y);
+      ctx.restore();
+    }
+
+    let last_bend = null;
+    let last_drawn_width = 0;
+    for (let i = 0; i < this.phrase.length; ++i) {
+      const bend = this.phrase[i];
+      if (i === 0) {
+        //VexflowPatch: Save unmodified width so that we don't increase x on every render
+        bend.draw_width_unmodified = bend.draw_width;
+        bend.draw_width += x_shift;
+      }
+
+      last_drawn_width = bend.draw_width +
+        (last_bend ? last_bend.draw_width : 0) -
+        (i === 1 ? x_shift : 0);
+      if (bend.type === Bend.UP) {
+        if (last_bend && last_bend.type === Bend.UP) {
+          renderArrowHead(start.x, bend_height);
+        }
+
+        renderBend(start.x, start.y, last_drawn_width, bend_height);
+      }
+
+      if (bend.type === Bend.DOWN) {
+        if (last_bend && last_bend.type === Bend.UP) {
+          renderRelease(start.x, start.y, last_drawn_width, bend_height);
+        }
+
+        if (last_bend && last_bend.type === Bend.DOWN) {
+          renderArrowHead(start.x, start.y, -1);
+          renderRelease(start.x, start.y, last_drawn_width, bend_height);
+        }
+
+        if (last_bend === null) {
+          last_drawn_width = bend.draw_width;
+          renderRelease(start.x, start.y, last_drawn_width, bend_height);
+        }
+      }
+
+      renderText(start.x + last_drawn_width, bend.text);
+      last_bend = bend;
+      last_bend.x = start.x;
+
+      start.x += last_drawn_width;
+    }
+
+    // Final arrowhead and text
+    if (last_bend.type === Bend.UP) {
+      renderArrowHead(last_bend.x + last_drawn_width, bend_height);
+    } else if (last_bend.type === Bend.DOWN) {
+      renderArrowHead(last_bend.x + last_drawn_width, start.y, -1);
+    }
+    // VexflowPatch: reset draw_width, so that bend doesn't get longer every re-render
+    for (let i = 0; i < this.phrase.length; ++i) {
+        const bend = this.phrase[i];
+        if (bend.draw_width_unmodified) {
+            bend.draw_width = bend.draw_width_unmodified;
+        }
+    }
+  }
+}

File diff suppressed because it is too large
+ 0 - 0
src/VexFlowPatch/src/fonts/vexflow_font.js


+ 3 - 2
src/VexFlowPatch/src/notehead.js

@@ -98,14 +98,15 @@ export class NoteHead extends Note {
     this.x_shift = head_options.x_shift || 0;
     // Swap out the glyph with ledger lines
     if (this.glyph.rest && (this.line > 5 || this.line < 0)) {
+      this.isLedgerLinedRest = true;
       if (this.duration === 'h') {
         head_options.custom_glyph_code = 'rhl';
         this.x_shift_ledger_rest -= 4;
-        this.y_shift_ledger_rest = -5; // was too far down
+        // this.y_shift_ledger_rest = 5; // was too far up
       } else if (this.duration === 'w') {
         head_options.custom_glyph_code = 'rwl';
         this.x_shift_ledger_rest -= 4;
-        this.y_shift_ledger_rest = 5; // was too far up
+        this.y_shift_ledger_rest = -5; // was too far down
       }
     }
     if (head_options.custom_glyph_code) {

+ 15 - 1
src/VexFlowPatch/src/stavenote.js

@@ -28,6 +28,9 @@ const isInnerNoteIndex = (note, index) =>
 
 // Helper methods for rest positioning in ModifierContext.
 function shiftRestVertical(rest, note, dir) {
+  if (rest.note.shiftRestVerticalDisabled) {
+    return;
+  }
   const delta = (note.isrest ? 0.0 : 1.0) * dir;
 
   rest.line += delta;
@@ -124,7 +127,9 @@ export class StaveNote extends StemmableNote {
 
     // for two voice backward compatibility, ensure upper voice is stems up
     // for three voices, the voices must be in order (upper, middle, lower)
-    if (voices === 2 && noteU.stemDirection === -1 && noteL.stemDirection === 1) {
+    if (voices === 2 && noteU.stemDirection === -1 && noteL.stemDirection === 1 &&
+      !noteU.isrest && !noteL.isRest // no need to switch positions if one is a rest
+    ) {
       noteU = notesList[1];
       noteL = notesList[0];
     }
@@ -149,9 +154,15 @@ export class StaveNote extends StemmableNote {
         if (noteU.isrest) {
           // shift rest up
           shiftRestVertical(noteU, noteL, 1);
+          if (noteU.note.hasLedgerLinedRest) {
+            noteU.note.shiftRestVerticalDisabled = true; // don't shift again on re-render
+          }
         } else if (noteL.isrest) {
           // shift rest down
           shiftRestVertical(noteL, noteU, -1);
+          if (noteL.note.hasLedgerLinedRest) {
+            noteL.note.shiftRestVerticalDisabled = true; // don't shift again on re-render
+          }
         } else {
           xShift = voiceXShift;
           //Vexflowpatch: Instead of shifting notes, remove the appropriate flag.
@@ -522,6 +533,9 @@ export class StaveNote extends StemmableNote {
         stem_down_y_shift: noteProps.stem_down_y_shift,
         line: noteProps.line,
       });
+      if (notehead.isLedgerLinedRest) {
+        this.hasLedgerLinedRest = true;
+      }
 
       this.note_heads[i] = notehead;
     }

+ 2 - 1
test/Common/OSMD/OSMD_Test.ts

@@ -172,7 +172,8 @@ describe("OpenSheetMusicDisplay Main Export", () => {
         );
     });
 
-    it("load something invalid by URL", (done: Mocha.Done) => {
+    // skip: this test is unnecessary and creates traffic (to google)
+    it.skip("load something invalid by URL", (done: Mocha.Done) => {
         const url: string = "https://www.google.com";
         const div: HTMLElement = TestUtils.getDivElement(document);
         const opensheetmusicdisplay: OpenSheetMusicDisplay = TestUtils.createOpenSheetMusicDisplay(div);

+ 477 - 0
test/data/test_crescendo_multi-instrument.musicxml

@@ -0,0 +1,477 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!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>test_crescendo_multi-instrument</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2024-01-09</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.5</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1828</page-height>
+      <page-width>1292</page-width>
+      <page-margins type="both">
+        <left-margin>92.3077</left-margin>
+        <right-margin>92.3077</right-margin>
+        <top-margin>92.3077</top-margin>
+        <bottom-margin>92.3077</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Times New Roman,serif" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="646" default-y="1735.69" justify="center" valign="top" font-family="ZapfChancery Light" font-size="29.94">test_crescendo_multi-instrument</credit-words>
+    </credit>
+  <part-list>
+    <part-group type="start" number="1">
+      <group-symbol>bracket</group-symbol>
+      </part-group>
+    <score-part id="P1">
+      <part-name>Violin I</part-name>
+      <part-abbreviation>Vln.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Violin I</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>41</midi-program>
+        <volume>78.7402</volume>
+        <pan>-67</pan>
+        </midi-instrument>
+      </score-part>
+    <score-part id="P2">
+      <part-name>Violin II</part-name>
+      <part-abbreviation>Vln.</part-abbreviation>
+      <score-instrument id="P2-I1">
+        <instrument-name>Violin II</instrument-name>
+        </score-instrument>
+      <midi-device id="P2-I1" port="1"></midi-device>
+      <midi-instrument id="P2-I1">
+        <midi-channel>4</midi-channel>
+        <midi-program>41</midi-program>
+        <volume>74.8031</volume>
+        <pan>67</pan>
+        </midi-instrument>
+      </score-part>
+    <score-part id="P3">
+      <part-name>Viola</part-name>
+      <part-abbreviation>Vla.</part-abbreviation>
+      <score-instrument id="P3-I1">
+        <instrument-name>Viola</instrument-name>
+        </score-instrument>
+      <midi-device id="P3-I1" port="1"></midi-device>
+      <midi-instrument id="P3-I1">
+        <midi-channel>7</midi-channel>
+        <midi-program>42</midi-program>
+        <volume>70.8661</volume>
+        <pan>-46</pan>
+        </midi-instrument>
+      </score-part>
+    <score-part id="P4">
+      <part-name>Cello</part-name>
+      <part-abbreviation>Vc.</part-abbreviation>
+      <score-instrument id="P4-I1">
+        <instrument-name>Violoncello (C2-C6)</instrument-name>
+        </score-instrument>
+      <midi-device id="P4-I1" port="1"></midi-device>
+      <midi-instrument id="P4-I1">
+        <midi-channel>11</midi-channel>
+        <midi-program>43</midi-program>
+        <volume>70.8661</volume>
+        <pan>44</pan>
+        </midi-instrument>
+      </score-part>
+    <part-group type="stop" number="1"/>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="600.32">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>85.68</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>-2</fifths>
+          <mode>major</mode>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="above">
+        <direction-type>
+          <words relative-y="10.00" font-weight="bold" font-size="12.09">Andante</words>
+          </direction-type>
+        </direction>
+      <direction placement="above">
+        <direction-type>
+          <metronome parentheses="no" default-x="-37.68" default-y="14.10" relative-y="20.00">
+            <beat-unit>quarter</beat-unit>
+            <per-minute>72</per-minute>
+            </metronome>
+          </direction-type>
+        <sound tempo="72"/>
+        </direction>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="-63.55"/>
+          </direction-type>
+        </direction>
+      <note default-x="111.61" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="2" width="421.38">
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.50" default-y="-43.55" relative-y="-25.00">
+            <f/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="106.67"/>
+        </direction>
+      <note default-x="13.00" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <note default-x="13.00" default-y="-15.00">
+        <chord/>
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  <part id="P2">
+    <measure number="1" width="600.32">
+      <print>
+        <staff-layout number="1">
+          <staff-distance>55.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>-2</fifths>
+          <mode>major</mode>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="-77.16"/>
+          </direction-type>
+        </direction>
+      <note default-x="111.61" default-y="-140.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>3</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <note default-x="355.06" default-y="-140.00">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <notations>
+          <slur type="start" placement="below" number="1"/>
+          </notations>
+        </note>
+      <note default-x="436.22" default-y="-130.00">
+        <pitch>
+          <step>F</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="517.37" default-y="-115.00">
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <slur type="stop" number="1"/>
+          </notations>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="2" width="421.38">
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.50" default-y="-57.16" relative-y="-25.00">
+            <f/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="106.67"/>
+        </direction>
+      <note default-x="13.00" default-y="-120.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  <part id="P3">
+    <measure number="1" width="600.32">
+      <print>
+        <staff-layout number="1">
+          <staff-distance>73.02</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>-2</fifths>
+          <mode>major</mode>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>C</sign>
+          <line>3</line>
+          </clef>
+        </attributes>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="-63.11"/>
+          </direction-type>
+        </direction>
+      <note default-x="111.61" default-y="-233.02">
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <slur type="start" placement="above" number="1"/>
+          </notations>
+        </note>
+      <note default-x="192.76" default-y="-248.02">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        </note>
+      <note default-x="273.91" default-y="-223.02">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        <notations>
+          <slur type="stop" number="1"/>
+          </notations>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="2" width="421.38">
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.50" default-y="-43.11" relative-y="-25.00">
+            <f/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="106.67"/>
+        </direction>
+      <note default-x="13.00" default-y="-248.02">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  <part id="P4">
+    <measure number="1" width="600.32">
+      <print>
+        <staff-layout number="1">
+          <staff-distance>55.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>-2</fifths>
+          <mode>major</mode>
+          </key>
+        <time>
+          <beats>3</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="-68.11"/>
+          </direction-type>
+        </direction>
+      <note default-x="111.61" default-y="-333.02">
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>2</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="2" width="421.38">
+      <direction placement="below">
+        <direction-type>
+          <dynamics default-x="6.50" default-y="-48.11" relative-y="-25.00">
+            <f/>
+            </dynamics>
+          </direction-type>
+        <sound dynamics="106.67"/>
+        </direction>
+      <note default-x="13.00" default-y="-348.02">
+        <pitch>
+          <step>F</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>6</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <dot/>
+        <stem>up</stem>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 106 - 0
test/data/test_direction_color.musicxml

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!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>test_direction_color</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2024-01-09</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1596.77</page-height>
+      <page-width>1233.87</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="616.935" default-y="1511.05" justify="center" valign="top" font-size="22">test_direction_color</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="306.47">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>672.23</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <direction placement="above">
+        <direction-type>
+          <words color="#0000FF" relative-x="-10.20" relative-y="11.70">Allegro</words>
+          </direction-type>
+        </direction>
+      <note default-x="80.72" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 687 - 0
test/data/test_fraction_infinite_loop_greatestCommonDenominator_measure3.musicxml

@@ -0,0 +1,687 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!DOCTYPE score-partwise PUBLIC '-//Recordare//DTD MusicXML 4.0 Partwise//EN' 'http://www.musicxml.org/dtds/partwise.dtd'>
+<score-partwise version='4.0'>
+	<work>
+		<work-title>test_fraction_infinite_loop_greatestCommonDenominator_measure3</work-title>
+    </work>
+	<identification>
+		<encoding>
+			<software>Sibelius 2023.8</software>
+			<software>Dolet 8.1 for Sibelius</software>
+			<encoding-date>2023-11-01</encoding-date>
+			<supports element='accidental' type='yes'/>
+			<supports element='transpose' type='yes'/>
+			<supports attribute='new-page' element='print' type='yes' value='yes'/>
+			<supports attribute='new-system' element='print' type='yes' value='yes'/>
+		</encoding>
+	</identification>
+	<defaults>
+		<scaling>
+			<millimeters>6.95</millimeters>
+			<tenths>40</tenths>
+		</scaling>
+		<concert-score/>
+		<page-layout>
+			<page-height>1709</page-height>
+			<page-width>1209</page-width>
+			<page-margins type='both'>
+				<left-margin>78.8489</left-margin>
+				<right-margin>78.8489</right-margin>
+				<top-margin>78.8489</top-margin>
+				<bottom-margin>78.8489</bottom-margin>
+			</page-margins>
+		</page-layout>
+		<system-layout>
+			<system-margins>
+				<left-margin>18.75</left-margin>
+				<right-margin>0</right-margin>
+			</system-margins>
+			<system-distance>92.5</system-distance>
+			<top-system-distance>75</top-system-distance>
+		</system-layout>
+		<staff-layout>
+			<staff-distance>55</staff-distance>
+		</staff-layout>
+		<?DoletSibelius StaffJustificationPercentage=50?>
+		<appearance>
+			<line-width type='beam'>5</line-width>
+			<line-width type='heavy barline'>5</line-width>
+			<line-width type='leger'>1.5625</line-width>
+			<line-width type='light barline'>1.5625</line-width>
+			<line-width type='slur middle'>2.1875</line-width>
+			<line-width type='slur tip'>0.625</line-width>
+			<line-width type='staff'>0.9375</line-width>
+			<line-width type='stem'>0.9375</line-width>
+			<line-width type='tie middle'>2.1875</line-width>
+			<line-width type='tie tip'>0.625</line-width>
+			<note-size type='grace'>60</note-size>
+			<note-size type='cue'>75</note-size>
+		</appearance>
+		<music-font font-family='Opus Std,engraved'/>
+	</defaults>
+	<part-list>
+		<score-part id='P1'>
+			<part-name print-object='no'>[Piano] (a)</part-name>
+			<part-abbreviation print-object='no'>Pno</part-abbreviation>
+			<group>score</group>
+			<score-instrument id='P1-I1'>
+				<instrument-name>Piano</instrument-name>
+				<instrument-abbreviation>Pno.</instrument-abbreviation>
+				<instrument-sound>keyboard.piano.grand</instrument-sound>
+			</score-instrument>
+			<midi-instrument id='P1-I1'>
+				<volume>79</volume>
+				<pan>-18</pan>
+			</midi-instrument>
+		</score-part>
+	</part-list>
+<!--=========================================================-->
+	<part id='P1'>
+		<measure number='1'>
+			<print>
+				<system-layout>
+					<system-margins>
+						<left-margin>18.75</left-margin>
+						<right-margin>0</right-margin>
+					</system-margins>
+				</system-layout>
+				<staff-layout>
+					<?DoletSibelius JustifyAllStaves=true?>
+				</staff-layout>
+			</print>
+			<attributes>
+				<divisions>768</divisions>
+				<key>
+					<fifths>2</fifths>
+					<mode>major</mode>
+				</key>
+				<time>
+					<beats>4</beats>
+					<beat-type>4</beat-type>
+				</time>
+				<staves>2</staves>
+				<clef number='1'>
+					<sign>G</sign>
+					<line>2</line>
+				</clef>
+				<clef number='2'>
+					<sign>F</sign>
+					<line>4</line>
+				</clef>
+			</attributes>
+			<note>
+				<pitch>
+					<step>D</step>
+					<octave>5</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>E</step>
+					<alter>1</alter>
+					<octave>5</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+			</note>
+			<backup>
+				<duration>2048</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>E</step>
+					<alter>1</alter>
+					<octave>4</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>2</voice>
+				<type>half</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<pitch>
+					<step>C</step>
+					<alter>2</alter>
+					<octave>4</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>2</voice>
+				<type>half</type>
+				<accidental>double-sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>E</step>
+					<alter>1</alter>
+					<octave>5</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>up</stem>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<backup>
+				<duration>1024</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>E</step>
+					<alter>1</alter>
+					<octave>4</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>2</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<backup>
+				<duration>3072</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>G</step>
+					<alter>1</alter>
+					<octave>2</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>5</voice>
+				<type>whole</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>2</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>G</step>
+					<alter>1</alter>
+					<octave>3</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>5</voice>
+				<type>whole</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>2</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>G</step>
+					<alter>1</alter>
+					<octave>2</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>up</stem>
+				<staff>2</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>G</step>
+					<alter>1</alter>
+					<octave>3</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>up</stem>
+				<staff>2</staff>
+			</note>
+		</measure>
+<!--=========================================================-->
+		<measure number='2'>
+			<print new-system='no'/>
+			<note>
+				<pitch>
+					<step>E</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>F</step>
+					<alter>1</alter>
+					<octave>4</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>C</step>
+					<alter>1</alter>
+					<octave>5</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>E</step>
+					<octave>5</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>E</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>up</stem>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>E</step>
+					<octave>5</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>up</stem>
+				<staff>1</staff>
+			</note>
+			<backup>
+				<duration>3072</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>A</step>
+					<alter>1</alter>
+					<octave>2</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>5</voice>
+				<type>whole</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>2</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>A</step>
+					<alter>1</alter>
+					<octave>3</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>5</voice>
+				<type>whole</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>2</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>A</step>
+					<alter>1</alter>
+					<octave>2</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>2</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>A</step>
+					<alter>1</alter>
+					<octave>3</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>2</staff>
+			</note>
+		</measure>
+<!--=========================================================-->
+		<measure number='3'>
+			<print new-system='no'/>
+			<note>
+				<pitch>
+					<step>F</step>
+					<alter>1</alter>
+					<octave>5</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>D</step>
+					<alter>1</alter>
+					<octave>6</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>1</staff>
+			</note>
+			<backup>
+				<duration>2048</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>D</step>
+					<alter>1</alter>
+					<octave>5</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>2</voice>
+				<type>half</type>
+				<accidental>sharp</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<pitch>
+					<step>B</step>
+					<octave>4</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>2</voice>
+				<type>half</type>
+				<accidental>natural</accidental>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>1</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>D</step>
+					<alter>1</alter>
+					<octave>6</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>1</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>up</stem>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<backup>
+				<duration>1024</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>D</step>
+					<alter>1</alter>
+					<octave>5</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>2</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>1</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<backup>
+				<duration>3072</duration>
+			</backup>
+			<note>
+				<pitch>
+					<step>B</step>
+					<octave>2</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>5</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>2</staff>
+				<notations>
+					<tuplet number='1' type='start' bracket='yes'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>B</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>2048</duration>
+				<voice>5</voice>
+				<type>whole</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+					<normal-type>half</normal-type>
+				</time-modification>
+				<staff>2</staff>
+			</note>
+			<note>
+				<pitch>
+					<step>B</step>
+					<octave>2</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>2</staff>
+				<notations>
+					<tuplet number='1' type='stop'/>
+				</notations>
+			</note>
+			<note>
+				<chord/>
+				<pitch>
+					<step>B</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>1024</duration>
+				<voice>5</voice>
+				<type>half</type>
+				<time-modification>
+					<actual-notes>3</actual-notes>
+					<normal-notes>2</normal-notes>
+				</time-modification>
+				<stem>down</stem>
+				<staff>2</staff>
+			</note>
+			<barline location='right'>
+				<bar-style>light-heavy</bar-style>
+			</barline>
+		</measure>
+	</part>
+<!--=========================================================-->
+</score-partwise>

+ 4 - 4
test/data/test_ledger_line_rest.musicxml

@@ -7,7 +7,7 @@
   <identification>
     <encoding>
       <software>MuseScore 3.6.2</software>
-      <encoding-date>2023-10-04</encoding-date>
+      <encoding-date>2024-01-08</encoding-date>
       <supports element="accidental" type="yes"/>
       <supports element="beam" type="yes"/>
       <supports element="print" attribute="new-page" type="yes" value="yes"/>
@@ -111,7 +111,7 @@
         </backup>
       <note>
         <rest>
-          <display-step>B</display-step>
+          <display-step>D</display-step>
           <display-octave>6</display-octave>
           </rest>
         <duration>4</duration>
@@ -145,7 +145,7 @@
         </backup>
       <note>
         <rest>
-          <display-step>D</display-step>
+          <display-step>C</display-step>
           <display-octave>3</display-octave>
           </rest>
         <duration>2</duration>
@@ -154,7 +154,7 @@
         </note>
       <note>
         <rest>
-          <display-step>D</display-step>
+          <display-step>C</display-step>
           <display-octave>3</display-octave>
           </rest>
         <duration>2</duration>

Some files were not shown because too many files changed in this diff