浏览代码

merge osmd-public/develop: fix tabs x-alignment, support BoldItalic text, etc

the tab x-alignment issue was a regression introduced recently, see:
https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/1489

also introduces 2 new EngravingRules for tabs (more compact spacing for tab-only scores)
sschmidTU 1 年之前
父节点
当前提交
a599ccd913

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

@@ -187,7 +187,17 @@ export class EngravingRules {
     public TupletNumbersInTabs: boolean;
     public TabBeamsRendered: boolean;
     public TabKeySignatureRendered: boolean;
+    /** Whether space should be reserved as if there was a key signature.
+     * False basically only works for tab-only scores, as it prevents vertical x-alignment with other staves.
+     * False is more compact for tab-only scores.
+     */
+    public TabKeySignatureSpacingAdded: boolean;
     public TabTimeSignatureRendered: boolean;
+    /** Whether space should be reserved as if there was a key signature.
+     * False basically only works for tab-only scores, as it prevents vertical x-alignment with other staves.
+     * False is more compact for tab-only scores.
+     */
+    public TabTimeSignatureSpacingAdded: boolean;
     public TabFingeringsRendered: boolean;
 
     public RepetitionAllowFirstMeasureBeginningRepeatBarline: boolean;
@@ -664,7 +674,9 @@ export class EngravingRules {
         this.TupletNumbersInTabs = false; // disabled by default, nonstandard in tabs, at least how we show them in non-tabs.
         this.TabBeamsRendered = true;
         this.TabKeySignatureRendered = false; // standard not to render for tab scores
+        this.TabKeySignatureSpacingAdded = true; // false only works for tab-only scores, as it will prevent vertical x-alignment.
         this.TabTimeSignatureRendered = false; // standard not to render for tab scores
+        this.TabTimeSignatureSpacingAdded = true; // false only works for tab-only scores, as it will prevent vertical x-alignment.
         this.TabFingeringsRendered = false; // tabs usually don't show fingering. This can also be duplicated when you have a classical+tab score.
 
         // Slur and Tie variables

+ 21 - 3
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -256,8 +256,11 @@ export class VexFlowMeasure extends GraphicalMeasure {
         if (!this.rules.RenderKeySignatures || !this.ShowKeySignature) {
             return;
         }
-        if (this.isTabMeasure && !this.rules.TabKeySignatureRendered) {
+        if (this.isTabMeasure && !this.rules.TabKeySignatureRendered && !this.rules.TabKeySignatureSpacingAdded) {
             return;
+            // This will ignore key signatures completely, so for non-tab-only scores, vertical x-alignment will be prevented.
+            //   If we want to x-align the startX / note startX, just not rendering the modifier is not enough.
+            //   For tab-only scores, this is more compact though.
         }
         if (this.parentSourceMeasure?.isReducedToMultiRest && !this.rules.MultipleRestMeasureAddKeySignature) {
             return;
@@ -267,6 +270,16 @@ export class VexFlowMeasure extends GraphicalMeasure {
             VexFlowConverter.keySignature(previousKey),
             undefined
         );
+        if (this.isTabMeasure && !this.rules.TabKeySignatureRendered) {
+            const modifiers: VF.StaveModifier[] = this.stave.getModifiers();
+            for (const modifier of modifiers) {
+                if (modifier instanceof VF.KeySignature) {
+                    modifier.setStyle({ fillStyle: "#00000000"}); // transparent. requires VexflowPatch
+                    // instead of not rendering the key signature, technically, we render it, but with transparent color. this helps layout / x-alignment.
+                    break;
+                }
+            }
+        }
         this.updateInstructionWidth();
     }
 
@@ -276,17 +289,22 @@ export class VexFlowMeasure extends GraphicalMeasure {
      * @param rhythm
      */
     public addRhythmAtBegin(rhythm: RhythmInstruction): void {
-        if (this.isTabMeasure && !this.rules.TabTimeSignatureRendered) {
+        if (this.isTabMeasure && !this.rules.TabTimeSignatureRendered && !this.rules.TabTimeSignatureSpacingAdded) {
             return;
+            // This will ignore time signatures completely, so for non-tab-only scores, vertical x-alignment will be prevented.
+            //   If we want to x-align the startX / note startX, just not rendering the modifier is not enough.
+            //   For tab-only scores, this is more compact though.
         }
         const timeSig: VF.TimeSignature = VexFlowConverter.TimeSignature(rhythm);
         this.stave.addModifier(
             timeSig,
             VF.StaveModifier.Position.BEGIN
         );
-        if (!this.ShowTimeSignature) {
+        if (!this.ShowTimeSignature ||
+            this.isTabMeasure && !this.rules.TabTimeSignatureRendered) {
             // extends Element is missing from class StaveModifier in DefinitelyTyped definitions, so setStyle isn't found
             timeSig.setStyle({ fillStyle: "#00000000"}); // transparent. requires VexflowPatch
+            // instead of not rendering the time signature, technically, we render it, but with transparent color. this helps layout / x-alignment.
         }
         this.updateInstructionWidth();
     }

+ 7 - 2
src/MusicalScore/ScoreIO/MusicSymbolModules/ExpressionReader.ts

@@ -570,17 +570,22 @@ export class ExpressionReader {
         }
         let fontStyle: FontStyles;
         const fontStyleAttr: Attr = wordsNode.attribute("font-style");
+        let fontStyleText: string;
+        let fontWeightText: string;
         if (fontStyleAttr) {
-            const fontStyleText: string = fontStyleAttr.value;
+            fontStyleText = fontStyleAttr.value;
             if (fontStyleText === "italic") {
                 fontStyle = FontStyles.Italic;
             }
         }
         const fontWeightAttr: Attr = wordsNode.attribute("font-weight");
         if (fontWeightAttr) {
-            const fontWeightText: string = fontWeightAttr.value;
+            fontWeightText = fontWeightAttr.value;
             if (fontWeightText === "bold") {
                 fontStyle = FontStyles.Bold;
+                if (fontStyleText === "italic") {
+                    fontStyle = FontStyles.BoldItalic;
+                }
             }
         }
         let defaultYXml: number;

+ 1 - 1
src/VexFlowPatch/src/beam.js

@@ -386,7 +386,7 @@ export class Beam extends Element {
     super();
     this.setAttribute('type', 'Beam');
 
-    if (!notes || notes === []) {
+    if (!notes || notes == []) {
       throw new Vex.RuntimeError('BadArguments', 'No notes provided for beam.');
     }
 

+ 230 - 0
src/VexFlowPatch/src/textbracket.js

@@ -0,0 +1,230 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+// Author: Cyril Silverman
+//
+// ## Description
+//
+// This file implement `TextBrackets` which extend between two notes.
+// The octave transposition markings (8va, 8vb, 15va, 15vb) can be created
+// using this class.
+
+import { Vex } from './vex';
+import { Flow } from './tables';
+import { Element } from './element';
+import { Renderer } from './renderer';
+
+// To enable logging for this class. Set `Vex.Flow.TextBracket.DEBUG` to `true`.
+function L(...args) { if (TextBracket.DEBUG) Vex.L('Vex.Flow.TextBracket', args); }
+
+export class TextBracket extends Element {
+  // FIXME: Modifier.Position is singular while this is plural, make consistent
+  static get Positions() {
+    return {
+      TOP: 1,
+      BOTTOM: -1,
+    };
+  }
+
+  static get PositionString() {
+    return {
+      top: TextBracket.Positions.TOP,
+      bottom: TextBracket.Positions.BOTTOM,
+    };
+  }
+
+  constructor({
+    start,
+    stop,
+    text = '',
+    superscript = '',
+    position = TextBracket.Positions.TOP,
+  }) {
+    super();
+    this.setAttribute('type', 'TextBracket');
+
+    this.start = start;
+    this.stop = stop;
+
+    this.text = text;
+    this.superscript = superscript;
+
+    this.position = typeof position === 'string'
+      ? TextBracket.PositionString[position]
+      : position;
+
+    this.line = 1;
+
+    this.font = {
+      family: 'Serif',
+      size: 15,
+      weight: 'italic',
+    };
+
+    this.render_options = {
+      dashed: true,
+      dash: [5],
+      color: 'black',
+      line_width: 1,
+      show_bracket: true,
+      bracket_height: 8,
+
+      // In the BOTTOM position, the bracket line can extend
+      // under the superscript.
+      underline_superscript: true,
+    };
+  }
+
+  // Apply the text backet styling to the provided `context`
+  applyStyle(context) {
+    // Apply style for the octave bracket
+    context.setFont(this.font.family, this.font.size, this.font.weight);
+    context.setStrokeStyle(this.render_options.color);
+    context.setFillStyle(this.render_options.color);
+    context.setLineWidth(this.render_options.line_width);
+
+    return this;
+  }
+
+  // Set whether the bracket line should be `dashed`. You can also
+  // optionally set the `dash` pattern by passing in an array of numbers
+  setDashed(dashed, dash) {
+    this.render_options.dashed = dashed;
+    if (dash) this.render_options.dash = dash;
+    return this;
+  }
+
+  // Set the font for the text
+  setFont(font) {
+    // We use Object.assign to support partial updates to the font object
+    this.font = { ...this.font, ...font };
+    return this;
+  }
+  // Set the rendering `context` for the octave bracket
+  setLine(line) { this.line = line; return this; }
+
+  // Draw the octave bracket on the rendering context
+  draw() {
+    const ctx = this.context;
+    this.setRendered();
+
+    let y = 0;
+    switch (this.position) {
+      case TextBracket.Positions.TOP:
+        y = this.start.getStave().getYForTopText(this.line);
+        break;
+      case TextBracket.Positions.BOTTOM:
+        y = this.start.getStave().getYForBottomText(this.line + Flow.TEXT_HEIGHT_OFFSET_HACK);
+        break;
+      default:
+        throw new Vex.RERR('InvalidPosition', `The position ${this.position} is invalid`);
+    }
+
+    // Get the preliminary start and stop coordintates for the bracket
+    const start = { x: this.start.getAbsoluteX(), y };
+    const stop = { x: this.stop.getAbsoluteX(), y };
+
+    L('Rendering TextBracket: start:', start, 'stop:', stop, 'y:', y);
+
+    const bracket_height = this.render_options.bracket_height * this.position;
+
+    ctx.save();
+    this.applyStyle(ctx);
+
+    // Draw text
+    ctx.fillText(this.text, start.x, start.y);
+
+    // Get the width and height for the octave number
+    let main_width = ctx.measureText(this.text).width;
+    // JSDOM / SVG export: getBbox doesn't work/exist in JSDOM. couldn't find a working polyfill yet.
+    //   see https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/1482
+    //   this only affects JSDOM / the nodeJS script generateImages_browserless.mjs with the SVG option
+    if (!(main_width > 0)) { // might be NaN, so Math.Max doesn't work
+      main_width = 10 * this.text.length; // 10 for "8", 20 for "15" (8va/15ma) hardcoded workaround until we have a polyfill, see above
+    }
+    let main_height = ctx.measureText('M').width;
+    if (!(main_height > 0)) {
+      main_height = 20;
+    }
+
+    // Calculate the y position for the super script
+    const super_y = start.y - (main_height / 2.5);
+
+    // Draw the superscript
+    ctx.setFont(this.font.family, this.font.size / 1.4, this.font.weight);
+    ctx.fillText(this.superscript, start.x + main_width + 1, super_y);
+
+    // Determine width and height of the superscript
+    let superscript_width = ctx.measureText(this.superscript).width;
+    if (!(superscript_width > 0)) {
+      superscript_width = 12;
+      if (this.superscript.includes("m")) {
+        superscript_width += 5; // "m" wider than "v" or "b"
+      }
+    }
+    let super_height = ctx.measureText('M').width;
+    if (!(super_height > 0)) {
+      super_height = 10;
+    }
+
+    // Setup initial coordinates for the bracket line
+    let start_x = start.x;
+    let line_y = super_y;
+    let end_x = stop.x + this.stop.getGlyph().getWidth();
+
+    // Adjust x and y coordinates based on position
+    if (this.position === TextBracket.Positions.TOP) {
+      start_x += main_width + superscript_width + 5;
+      line_y -= super_height / 2.7;
+    } else if (this.position === TextBracket.Positions.BOTTOM) {
+      line_y += super_height / 2.7;
+      start_x += main_width + 2;
+
+      if (!this.render_options.underline_superscript) {
+        start_x += superscript_width;
+      }
+    }
+
+    if (this.render_options.dashed) {
+      // VexFlowPatch: make sure bracket doesn't go backwards, also causing overlap with text
+      if (end_x < start_x + 5 && this.position === TextBracket.Positions.TOP) {
+        end_x = start_x + 5; 
+      } else if (end_x < start_x + superscript_width && this.position === TextBracket.Positions.BOTTOM) {
+        // text bracket bottom (below staff) means it already starts where the superscript starts,
+        //   so we need to end at the end of the superscript at minimum
+        end_x = start_x + superscript_width;
+      }
+      // Main line
+      Renderer.drawDashedLine(
+        ctx,
+        start_x,
+        line_y,
+        end_x,
+        line_y,
+        this.render_options.dash
+      );
+      // Ending Bracket
+      if (this.render_options.show_bracket) {
+        Renderer.drawDashedLine(
+          ctx,
+          end_x,
+          line_y + (1 * this.position),
+          end_x,
+          line_y + bracket_height,
+          this.render_options.dash
+        );
+      }
+    } else {
+      ctx.beginPath();
+      ctx.moveTo(start_x, line_y);
+      // Main line
+      ctx.lineTo(end_x, line_y);
+      if (this.render_options.show_bracket) {
+        // Ending bracket
+        ctx.lineTo(end_x, line_y + bracket_height);
+      }
+      ctx.stroke();
+      ctx.closePath();
+    }
+
+    ctx.restore();
+  }
+}

+ 6 - 0
test/Util/generateImages_browserless.mjs

@@ -329,6 +329,7 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
         isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes("test_invisible_measure_not_affecting_layout");
         const isTestWedgeMultilineCrescendo = sampleFilename.includes("test_wedge_multiline_crescendo");
         const isTestWedgeMultilineDecrescendo = sampleFilename.includes("test_wedge_multiline_decrescendo");
+        const isTestTabs4Strings = sampleFilename.includes("test_tabs_4_strings");
         osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
         if (isTestEndClefStaffEntryBboxes) {
             drawBoundingBoxString = "VexFlowStaffEntry";
@@ -383,6 +384,11 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
             isTestWedgeMultilineDecrescendo) {
             osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
         }
+        if (isTestTabs4Strings) {
+            osmdInstance.EngravingRules.TabKeySignatureSpacingAdded = false;
+            osmdInstance.EngravingRules.TabTimeSignatureSpacingAdded = false;
+            // more compact rendering. These are basically just aesthetic options, as a showcase.
+        }
     }
 
     try {

+ 110 - 0
test/data/test_bold_italic.musicxml

@@ -0,0 +1,110 @@
+<?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_bold_italic</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-12-06</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>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</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="600" default-y="1611.43" justify="center" valign="top" font-size="22">test_bold_italic</credit-words>
+    </credit>
+  <part-list>
+    <part-group type="start" number="1">
+      <group-symbol>brace</group-symbol>
+      </part-group>
+    <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="294.34">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>684.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>2</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-style="italic">BoldItalic</words>
+          </direction-type>
+        </direction>
+      <note default-x="80.72" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 218 - 0
test/data/test_svg_export_8va.musicxml

@@ -0,0 +1,218 @@
+<?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_8va_svg_export</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2023-11-15</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_8va_svg_export</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="296.40">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>0.00</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="below">
+        <direction-type>
+          <octave-shift type="up" size="8" number="1" default-y="-60.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="80.72" default-y="5.00">
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="187.66" default-y="10.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <octave-shift type="stop" size="8" number="1" relative-x="-1.70"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="2" width="224.48">
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="down" size="8" number="1" default-y="20.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="13.00" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="117.84" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>6</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="stop" size="8" number="1" relative-x="-1.70"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="3" width="224.48">
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="down" size="15" number="1" default-y="20.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="13.00" default-y="-45.00">
+        <pitch>
+          <step>D</step>
+          <octave>6</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        </note>
+      <note default-x="117.84" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>6</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>up</stem>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <octave-shift type="stop" size="15" number="1"/>
+          </direction-type>
+        </direction>
+      </measure>
+    <measure number="4" width="233.33">
+      <direction placement="below">
+        <direction-type>
+          <octave-shift type="up" size="15" number="1" default-y="-60.00"/>
+          </direction-type>
+        </direction>
+      <note default-x="13.00" default-y="0.00">
+        <pitch>
+          <step>F</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <note default-x="117.84" default-y="5.00">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>half</type>
+        <stem>down</stem>
+        </note>
+      <direction placement="below">
+        <direction-type>
+          <octave-shift type="stop" size="15" number="1"/>
+          </direction-type>
+        </direction>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 218 - 0
test/data/test_tabs_x-align_vertically.musicxml

@@ -0,0 +1,218 @@
+<?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'>
+	<movement-title>test_tab_x-align_vertically</movement-title>
+	<identification>
+		<encoding>
+			<software>Sibelius 2023.6</software>
+			<software>Dolet 8.3 for Sibelius</software>
+			<encoding-date>2023-08-31</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.5</millimeters>
+			<tenths>40</tenths>
+		</scaling>
+		<page-layout>
+			<page-height>1828</page-height>
+			<page-width>1292</page-width>
+			<page-margins type='both'>
+				<left-margin>78.1538</left-margin>
+				<right-margin>78.1538</right-margin>
+				<top-margin>78.1538</top-margin>
+				<bottom-margin>78.1538</bottom-margin>
+			</page-margins>
+		</page-layout>
+		<system-layout>
+			<system-margins>
+				<left-margin>0</left-margin>
+				<right-margin>0</right-margin>
+			</system-margins>
+			<system-distance>130</system-distance>
+			<top-system-distance>153.75</top-system-distance>
+		</system-layout>
+		<staff-layout>
+			<staff-distance>90</staff-distance>
+		</staff-layout>
+		<?DoletSibelius StaffJustificationPercentage=40?>
+		<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'>1.25</line-width>
+			<line-width type='stem'>1.25</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='Helsinki Std,engraved'/>
+		<word-font font-family='Palatino,serif'/>
+	</defaults>
+	<credit page='1'>
+		<credit-type>test_tab_x-align_vertically</credit-type>
+		<credit-words/>
+	</credit>
+	<part-list>
+		<part-group number='1' type='start'>
+			<group-symbol>bracket</group-symbol>
+			<group-barline>no</group-barline>
+		</part-group>
+		<score-part id='P1'>
+			<part-name print-object='no'>Electric Guitar</part-name>
+			<part-abbreviation print-object='no'>E. Gtr.</part-abbreviation>
+			<group>score</group>
+			<score-instrument id='P1-I1'>
+				<instrument-name>Electric Guitar [notation]</instrument-name>
+				<instrument-abbreviation>E. Gtr.</instrument-abbreviation>
+				<instrument-sound>pluck.guitar.electric</instrument-sound>
+			</score-instrument>
+			<midi-instrument id='P1-I1'>
+				<volume>79</volume>
+				<pan>0</pan>
+			</midi-instrument>
+		</score-part>
+		<score-part id='P2'>
+			<part-name print-object='no'>Acoustic Guitar</part-name>
+			<part-abbreviation print-object='no'>A. Gtr.</part-abbreviation>
+			<group>score</group>
+			<score-instrument id='P2-I1'>
+				<instrument-name>Acoustic Guitar, standard tuning (no rhythms) [tab]</instrument-name>
+				<instrument-abbreviation>A. Gtr.</instrument-abbreviation>
+				<instrument-sound>pluck.guitar.steel-string</instrument-sound>
+			</score-instrument>
+			<midi-instrument id='P2-I1'>
+				<volume>79</volume>
+				<pan>0</pan>
+			</midi-instrument>
+		</score-part>
+		<part-group number='1' type='stop'/>
+	</part-list>
+<!--=========================================================-->
+	<part id='P1'>
+		<measure number='1'>
+			<print>
+				<system-layout>
+					<top-system-distance>246.25</top-system-distance>
+				</system-layout>
+			</print>
+			<barline location='left'>
+				<bar-style>heavy-light</bar-style>
+				<repeat direction='forward'/>
+			</barline>
+			<attributes>
+				<divisions>768</divisions>
+				<key>
+					<fifths>-4</fifths>
+					<mode>major</mode>
+				</key>
+				<time>
+					<beats>4</beats>
+					<beat-type>4</beat-type>
+				</time>
+				<clef>
+					<sign>G</sign>
+					<line>2</line>
+				</clef>
+				<transpose>
+					<diatonic>0</diatonic>
+					<chromatic>0</chromatic>
+					<octave-change>-1</octave-change>
+				</transpose>
+			</attributes>
+			<sound tempo='114'/>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>3</octave>
+				</pitch>
+				<duration>3072</duration>
+				<voice>1</voice>
+				<type>whole</type>
+			</note>
+			<barline location='right'>
+				<bar-style>light-heavy</bar-style>
+				<repeat direction='backward'/>
+			</barline>
+		</measure>
+	</part>
+<!--=========================================================-->
+	<part id='P2'>
+		<measure number='1'>
+			<barline location='left'>
+				<bar-style>heavy-light</bar-style>
+				<repeat direction='forward'/>
+			</barline>
+			<attributes>
+				<divisions>768</divisions>
+				<key>
+					<fifths>-4</fifths>
+					<mode>major</mode>
+				</key>
+				<time>
+					<beats>4</beats>
+					<beat-type>4</beat-type>
+				</time>
+				<clef>
+					<sign>TAB</sign>
+				</clef>
+				<staff-details>
+					<staff-lines>6</staff-lines>
+					<staff-tuning line='1'>
+						<tuning-step>D</tuning-step>
+						<tuning-octave>2</tuning-octave>
+					</staff-tuning>
+					<staff-tuning line='2'>
+						<tuning-step>A</tuning-step>
+						<tuning-octave>2</tuning-octave>
+					</staff-tuning>
+					<staff-tuning line='3'>
+						<tuning-step>D</tuning-step>
+						<tuning-octave>3</tuning-octave>
+					</staff-tuning>
+					<staff-tuning line='4'>
+						<tuning-step>G</tuning-step>
+						<tuning-octave>3</tuning-octave>
+					</staff-tuning>
+					<staff-tuning line='5'>
+						<tuning-step>B</tuning-step>
+						<tuning-octave>3</tuning-octave>
+					</staff-tuning>
+					<staff-tuning line='6'>
+						<tuning-step>E</tuning-step>
+						<tuning-octave>4</tuning-octave>
+					</staff-tuning>
+					<staff-size>150</staff-size>
+				</staff-details>
+			</attributes>
+			<note>
+				<pitch>
+					<step>F</step>
+					<octave>2</octave>
+				</pitch>
+				<duration>3072</duration>
+				<voice>1</voice>
+				<type>whole</type>
+				<notations>
+					<technical>
+						<string>6</string>
+						<fret>3</fret>
+					</technical>
+				</notations>
+			</note>
+			<barline location='right'>
+				<bar-style>light-heavy</bar-style>
+				<repeat direction='backward'/>
+			</barline>
+		</measure>
+	</part>
+<!--=========================================================-->
+</score-partwise>