Browse Source

fix(NotePositions): GraphicalNote bbox position improved (#966, #967), osmd.graphic.GetNearestNote working

merge PR #967
fixes #966

previously, the GraphicalNote.PositionAndShape.AbsolutePosition was the same
as the note’s parentVoiceEntry.
Now each note has a (more or less) accurate position.
there are just some edge cases where the position can still be imprecise
(see #966), like grace notes and partly rest notes (not centered).

other new functions in osmd.graphic:
svgToDom(), svgToOsmd(), domToSvg() to convert positions (point x and y values)

new test: Beethoven_AnDieFerneGeliebte.xml_bboxVexFlowGraphicalNote
(standard Beethoven Geliebte sample with GraphicalNote bboxes enabled)

GetNearestGraphicalObject() code taken over from audio player.

squash merged, individual commits on branch origin/fix/graphicalNoteBoundingBoxPosition:

* fix(GetNearestNote): osmd.graphic.GetNearestNote working (WIP)

* GraphicalNote bounding box fixed in most cases, except rests, grace notes (#966)

* test: generateImages: add graphicalnote bbox test (#966), refactor generateSampleImage options

* fix GetNearestNote() after refactor, remove some logs and imports

* fix npm start build issue (#966), remove debug nearest note line drawing

* add osmd.graphic.svgToOsmd(), svgToDom() (#966). fix generateImages tests

refactor VexFlowMeasure.correctNotePositions()

* comment (#966)

* refactor: remove test function

* test: clarify bbox test filename

* test: refactor bbox argument for generateSampleImage

* refactor: remove unused imports

* refactor: remove unnecessary if

* refactor: remove unnecessary if
Simon 4 years ago
parent
commit
1fde0f6ca3

+ 17 - 8
src/MusicalScore/Graphical/BoundingBox.ts

@@ -5,6 +5,7 @@ import {SizeF2D} from "../../Common/DataObjects/SizeF2D";
 import {RectangleF2D} from "../../Common/DataObjects/RectangleF2D";
 import { StaffLineActivitySymbol } from "./StaffLineActivitySymbol";
 import { EngravingRules } from "./EngravingRules";
+import { GraphicalObject } from "./GraphicalObject";
 
 /**
  * A bounding box delimits an area on the 2D plane.
@@ -572,23 +573,31 @@ export class BoundingBox {
         return undefined;
     }
 
-    public getObjectsInRegion<T>(region: BoundingBox, liesInside: boolean = true): T[] {
-        if (<T>this.dataObject) {
+    //Generics don't work like this in TS. Casting doesn't filter out objects.
+    //instanceof doesn't work either with generic types. Hopefully instanceof becomes available at some point, for now we have to do annoyingly
+    //specific implementations after calling this to filter the objects.
+    public getObjectsInRegion<T extends GraphicalObject>(region: BoundingBox, liesInside: boolean = true,
+                                                         className: string = GraphicalObject.name): T[] {
+        let result: T[] = [];
+        for (const child of this.childElements) {
+            result = result.concat(child.getObjectsInRegion<T>(region, liesInside, className));
+        }
+
+        //if (!result || result.length === 0) {
+        // audioplayer: this.dataObject as T
+        if (this.dataObject && (this.dataObject as T).isInstanceOfClass(className)) {
             if (liesInside) {
                 if (region.liesInsideBorders(this)) {
-                    return [this.dataObject as T];
+                    result.push(this.dataObject as T);
                 }
             } else {
                 if (region.collisionDetection(this)) {
-                    return [this.dataObject as T];
+                    result.push(this.dataObject as T);
                 }
             }
             // FIXME Andrea: add here "return []"?
         }
-        const result: T[] = [];
-        for (const child of this.childElements) {
-            result.concat(child.getObjectsInRegion<T>(region, liesInside));
-        }
+        //}
         return result;
         //return this.childElements.SelectMany(psi => psi.getObjectsInRegion<T>(region, liesInside));
     }

+ 118 - 37
src/MusicalScore/Graphical/GraphicalMusicSheet.ts

@@ -23,6 +23,11 @@ import {CollectionUtil} from "../../Util/CollectionUtil";
 import {SelectionStartSymbol} from "./SelectionStartSymbol";
 import {SelectionEndSymbol} from "./SelectionEndSymbol";
 import {OutlineAndFillStyleEnum} from "./DrawingEnums";
+import { MusicSheetDrawer } from "./MusicSheetDrawer";
+import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
+import { GraphicalObject } from "./GraphicalObject";
+// import { VexFlowMusicSheetDrawer } from "./VexFlow/VexFlowMusicSheetDrawer";
+// import { SvgVexFlowBackend } from "./VexFlow/SvgVexFlowBackend"; // causes build problem with npm start
 
 /**
  * The graphical counterpart of a [[MusicSheet]]
@@ -38,6 +43,7 @@ export class GraphicalMusicSheet {
     private musicSheet: MusicSheet;
     //private fontInfo: FontInfo = FontInfo.Info;
     private calculator: MusicSheetCalculator;
+    public drawer: MusicSheetDrawer;
     private musicPages: GraphicalMusicPage[] = [];
     /** measures (i,j) where i is the measure number and j the staff index (e.g. staff indices 0, 1 for two piano parts) */
     private measureList: GraphicalMeasure[][] = [];
@@ -495,61 +501,136 @@ export class GraphicalMusicSheet {
         return false;
     }
 
-    public GetNearestNote(clickPosition: PointF2D, maxClickDist: PointF2D): GraphicalNote {
-        const initialSearchArea: number = 10;
-        const foundNotes: GraphicalNote[] = [];
-
-        // Prepare search area
-        const region: BoundingBox = new BoundingBox();
-        region.BorderLeft = clickPosition.x - initialSearchArea;
-        region.BorderTop = clickPosition.y - initialSearchArea;
-        region.BorderRight = clickPosition.x + initialSearchArea;
-        region.BorderBottom = clickPosition.y + initialSearchArea;
-        region.AbsolutePosition = new PointF2D(0, 0);
-
-        // Search for StaffEntries in region
-        for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
-            const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
-            const entries: GraphicalNote[] = graphicalMusicPage.PositionAndShape.getObjectsInRegion<GraphicalNote>(region);
-            //let entriesArr: GraphicalNote[] = __as__<GraphicalNote[]>(entries, GraphicalNote[]) ? ? entries;
-            if (!entries) {
-                continue;
-            } else {
-                for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
-                    const note: GraphicalNote = entries[idx2];
-                    if (Math.abs(note.PositionAndShape.AbsolutePosition.x - clickPosition.x) < maxClickDist.x
-                        && Math.abs(note.PositionAndShape.AbsolutePosition.y - clickPosition.y) < maxClickDist.y) {
-                        foundNotes.push(note);
+    /**
+     * Generic method to find graphical objects on the sheet at a given location.
+     * @param clickPosition Position in units where we are searching on the sheet
+     * @param className String representation of the class we want to find. Must extend GraphicalObject
+     * @param startSearchArea The area in units around our point to look for our graphical object, default 5
+     * @param maxSearchArea The max area we want to search around our point
+     * @param searchAreaIncrement The amount we expand our search area for each iteration that we don't find an object of the given type
+     * @param shouldBeIncludedTest A callback that determines if the object should be included in our results- return false for no, true for yes
+     */
+    private GetNearestGraphicalObject<T extends GraphicalObject>(
+        clickPosition: PointF2D, className: string = GraphicalObject.name,
+        startSearchArea: number = 5, maxSearchArea: number = 20, searchAreaIncrement: number = 5,
+        shouldBeIncludedTest: (objectToTest: T) => boolean = undefined): T {
+        const foundEntries: T[] = [];
+        //Loop until we find some, or our search area is out of bounds
+        while (foundEntries.length === 0 && startSearchArea <= maxSearchArea) {
+            //Prepare search area
+            const region: BoundingBox = new BoundingBox(undefined);
+            region.BorderLeft = clickPosition.x - startSearchArea;
+            region.BorderTop = clickPosition.y - startSearchArea;
+            region.BorderRight = clickPosition.x + startSearchArea;
+            region.BorderBottom = clickPosition.y + startSearchArea;
+            region.AbsolutePosition = new PointF2D(clickPosition.x, clickPosition.y);
+            region.calculateAbsolutePosition();
+            //Loop through music pages
+            for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
+                const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
+                const entries: T[] = graphicalMusicPage.PositionAndShape.getObjectsInRegion<T>(region, false, className);
+                //If we have no entries on this page, skip to next (if exists)
+                if (!entries || entries.length === 0) {
+                    continue;
+                } else {
+                    //Otherwise test all our entries if applicable, store on our found list
+                    for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
+                        if (!shouldBeIncludedTest) {
+                            foundEntries.push(entries[idx2]);
+                        } else if (shouldBeIncludedTest(entries[idx2])) {
+                            foundEntries.push(entries[idx2]);
+                        }
                     }
                 }
             }
+            //Expand search area, we haven't found anything yet
+            startSearchArea += searchAreaIncrement;
         }
-
         // Get closest entry
-        let closest: GraphicalNote = undefined;
-        for (let idx: number = 0, len: number = foundNotes.length; idx < len; ++idx) {
-            const note: GraphicalNote = foundNotes[idx];
+        let closest: T = undefined;
+        for (let idx: number = 0, len: number = foundEntries.length; idx < len; ++idx) {
+            const object: T = foundEntries[idx];
             if (closest === undefined) {
-                closest = note;
+                closest = object;
             } else {
-                if (!note.parentVoiceEntry.parentStaffEntry.relInMeasureTimestamp) {
-                    continue;
-                }
-                const deltaNew: number = this.CalculateDistance(note.PositionAndShape.AbsolutePosition, clickPosition);
+                const deltaNew: number = this.CalculateDistance(object.PositionAndShape.AbsolutePosition, clickPosition);
                 const deltaOld: number = this.CalculateDistance(closest.PositionAndShape.AbsolutePosition, clickPosition);
                 if (deltaNew < deltaOld) {
-                    closest = note;
+                    closest = object;
                 }
             }
         }
         if (closest) {
             return closest;
         }
-        // TODO No staff entry was found. Feedback?
-        // throw new ArgumentException("No staff entry found");
         return undefined;
     }
 
+    public GetNearestVoiceEntry(clickPosition: PointF2D): GraphicalVoiceEntry {
+        return this.GetNearestGraphicalObject<GraphicalVoiceEntry>(clickPosition, GraphicalVoiceEntry.name, 5, 20, 5,
+                                                                   (object: GraphicalVoiceEntry) =>
+                                                                        object.parentStaffEntry.relInMeasureTimestamp !== undefined);
+    }
+
+    public GetNearestNote(clickPosition: PointF2D, maxClickDist: PointF2D): GraphicalNote {
+        const nearestVoiceEntry: GraphicalVoiceEntry = this.GetNearestVoiceEntry(clickPosition);
+        if (!nearestVoiceEntry) {
+            return undefined;
+        }
+        let closestNote: GraphicalNote;
+        let closestDist: number = Number.MAX_SAFE_INTEGER;
+        // debug: show position in sheet. line starts from the click position, until clickposition.x + 2
+        // (this.drawer as any).DrawOverlayLine( // as VexFlowMusicSheetDrawer
+        //     clickPosition,
+        //     new PointF2D(clickPosition.x + 2, clickPosition.y),
+        //     this.MusicPages[0]);
+        for (const note of nearestVoiceEntry.notes) {
+            const posY: number = note.PositionAndShape.AbsolutePosition.y;
+            const distX: number = Math.abs(note.PositionAndShape.AbsolutePosition.x - clickPosition.x);
+            const distY: number = Math.abs(posY - clickPosition.y);
+            // console.log("note: " + note.sourceNote.Pitch.ToString());
+            if (distX + distY < closestDist) {
+                closestNote = note;
+                closestDist = distX + distY;
+            }
+        }
+        return closestNote;
+    }
+
+    public domToSvg(point: PointF2D): PointF2D {
+        return this.domToSvgTransform(point, true);
+    }
+
+    public svgToDom(point: PointF2D): PointF2D {
+        return this.domToSvgTransform(point, false);
+    }
+
+    public svgToOsmd(point: PointF2D): PointF2D {
+        const pt: PointF2D = new PointF2D(point.x, point.y);
+        pt.x /= 10; // unitInPixels would need to be imported from VexFlowMusicSheetDrawer
+        pt.y /= 10;
+        return pt;
+    }
+
+    // TODO move to VexFlowMusicSheetDrawer? better fit for imports
+    private domToSvgTransform(point: PointF2D, inverse: boolean): PointF2D {
+        const svgBackend: any = (this.drawer as any).Backends[0]; // as SvgVexFlowBackend;
+        // TODO importing SvgVexFlowBackend here causes build problems. Importing VexFlowMusicSheetDrawer seems to be fine, but unnecessary.
+        // if (!(svgBackend instanceof SvgVexFlowBackend)) {
+        //     return undefined;
+        // }
+        const svg: SVGSVGElement = svgBackend.getSvgElement() as SVGSVGElement;
+        const pt: SVGPoint = svg.createSVGPoint();
+        pt.x = point.x;
+        pt.y = point.y;
+        let transformMatrix: DOMMatrix = svg.getScreenCTM();
+        if (inverse) {
+            transformMatrix = transformMatrix.inverse();
+        }
+        const sp: SVGPoint = pt.matrixTransform(transformMatrix);
+        return new PointF2D(sp.x, sp.y);
+    }
+
     public GetClickableLabel(clickPosition: PointF2D): GraphicalLabel {
         const initialSearchAreaX: number = 4;
         const initialSearchAreaY: number = 4;

+ 2 - 1
src/MusicalScore/Graphical/GraphicalObject.ts

@@ -1,6 +1,7 @@
+import { AClassHierarchyTrackable } from "../Interfaces/AClassHierarchyTrackable";
 import {BoundingBox} from "./BoundingBox";
 
-export class GraphicalObject {
+export class GraphicalObject extends AClassHierarchyTrackable {
 
     protected boundingBox: BoundingBox;
 

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

@@ -405,9 +405,11 @@ export class VexFlowConverter {
             switch (wantedStemDirection) {
                 case(StemDirectionType.Up):
                     vfnote.setStemDirection(Vex.Flow.Stem.UP);
+                    gve.parentVoiceEntry.StemDirection = StemDirectionType.Up;
                     break;
                 case (StemDirectionType.Down):
                     vfnote.setStemDirection(Vex.Flow.Stem.DOWN);
+                    gve.parentVoiceEntry.StemDirection = StemDirectionType.Down;
                     break;
                 default:
             }

+ 15 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote.ts

@@ -33,6 +33,7 @@ export class VexFlowGraphicalNote extends GraphicalNote {
     public vfpitch: [string, string, ClefInstruction];
     // The corresponding VexFlow StaveNote (plus its index in the chord)
     public vfnote: [Vex.Flow.StemmableNote, number];
+    public vfnoteIndex: number;
     // The current clef
     private clef: ClefInstruction;
 
@@ -77,6 +78,20 @@ export class VexFlowGraphicalNote extends GraphicalNote {
      */
     public setIndex(note: Vex.Flow.StemmableNote, index: number): void {
         this.vfnote = [note, index];
+        this.vfnoteIndex = index;
+    }
+
+    public notehead(vfNote: Vex.Flow.StemmableNote = undefined): {line: number} {
+        let vfnote: any = vfNote;
+        if (!vfnote) {
+            vfnote = (this.vfnote[0] as any);
+        }
+        const noteheads: any = vfnote.note_heads;
+        if (noteheads && noteheads.length > this.vfnoteIndex && noteheads[this.vfnoteIndex]) {
+            return vfnote.note_heads[this.vfnoteIndex];
+        } else {
+            return { line: 0 };
+        }
     }
 
     /**

+ 37 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -625,6 +625,7 @@ export class VexFlowMeasure extends GraphicalMeasure {
         for (const connector of this.connectors) {
             connector.setContext(ctx).draw();
         }
+        this.correctNotePositions();
     }
 
     // this currently formats multiple measures, see VexFlowMusicSheetCalculator.formatMeasures()
@@ -636,6 +637,42 @@ export class VexFlowMeasure extends GraphicalMeasure {
             // (The width of the voices does not include the instructions (StaveModifiers))
             this.formatVoices((this.PositionAndShape.Size.width - this.beginInstructionsWidth - this.endInstructionsWidth) * unitInPixels, this);
         }
+
+        // this.correctNotePositions(); // now done at the end of draw()
+    }
+
+    // correct position / bounding box (note.setIndex() needs to have been called)
+    public correctNotePositions(): void {
+        if (this.isTabMeasure) {
+            return;
+        }
+        for (const voice of this.getVoicesWithinMeasure()) {
+            for (const ve of voice.VoiceEntries) {
+                for (const note of ve.Notes) {
+                    const gNote: VexFlowGraphicalNote = this.rules.GNote(note) as VexFlowGraphicalNote;
+                    const vfnote: Vex.Flow.StemmableNote = gNote.vfnote[0];
+                    // if (note.isRest()) // TODO somehow there are never rest notes in ve.Notes
+                    // TODO also, grace notes are not included here, need to be fixed as well. (and a few triple beamed notes in Bach Air)
+                    let relPosY: number = 0;
+                    if (gNote.parentVoiceEntry.parentVoiceEntry.StemDirection === StemDirectionType.Up) {
+                        relPosY += 3.5; // about 3.5 lines too high. this seems to be related to the default stem height, not actual stem height.
+                        // alternate calculation using actual stem height: somehow wildly varying.
+                        // if (ve.Notes.length > 1) {
+                        //     const stemHeight: number = vfnote.getStem().getHeight();
+                        //     // relPosY += shortFactor * stemHeight / unitInPixels - 3.5;
+                        //     relPosY += stemHeight / unitInPixels - 3.5; // for some reason this varies in its correctness between similar notes
+                        // } else {
+                        //     relPosY += 3.5;
+                        // }
+                    } else {
+                        relPosY += 0.5; // center-align bbox
+                    }
+                    const line: any = -gNote.notehead(vfnote).line; // vexflow y direction is opposite of osmd's
+                    relPosY += line + (gNote.parentVoiceEntry.notes.last() as VexFlowGraphicalNote).notehead().line; // don't move for first note: - (-vexline)
+                    gNote.PositionAndShape.RelativePosition.y = relPosY;
+                }
+            }
+        }
     }
 
     /**

+ 15 - 0
src/MusicalScore/Interfaces/AClassHierarchyTrackable.ts

@@ -0,0 +1,15 @@
+export abstract class AClassHierarchyTrackable {
+    //TODO: This pattern doesn't account for interfaces, only classes.
+    //At present, it seems that interfaces need tested manually when they are needed.
+    //Perhaps there is a better solution, but right now I don't see it. This is fine for our requirements currently
+    public isInstanceOfClass(className: string): boolean {
+        let proto: any = this.constructor.prototype;
+        while (proto) {
+            if (className === proto.constructor.name) {
+                return true;
+            }
+            proto = proto.__proto__;
+        }
+        return false;
+    }
+}

+ 1 - 0
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -314,6 +314,7 @@ export class OpenSheetMusicDisplay {
             backend.resize(width, height);
             backend.clear(); // set bgcolor if defined (this.rules.PageBackgroundColor, see OSMDOptions)
             this.drawer.Backends.push(backend);
+            this.graphic.drawer = this.drawer;
         }
     }
 

+ 16 - 6
test/Util/generateImages_browserless.js

@@ -228,11 +228,13 @@ async function init () {
         const sampleFilename = samplesToProcess[i];
         debug("sampleFilename: " + sampleFilename, DEBUG);
 
-        await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, false);
+        await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {}, DEBUG);
 
         if (osmdTestingMode && !osmdTestingSingleMode && sampleFilename.startsWith("Beethoven") && sampleFilename.includes("Geliebte")) {
             // generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
-            await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, true, DEBUG);
+            await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {skyBottomLine: true}, DEBUG);
+            // generate one more testing image with GraphicalNote positions
+            await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {boundingBoxes: "VexFlowGraphicalNote"}, DEBUG);
         }
     }
 
@@ -242,7 +244,7 @@ async function init () {
 // eslint-disable-next-line
 // let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
 async function generateSampleImage (sampleFilename, directory, osmdInstance, osmdTestingMode,
-    includeSkyBottomLine = false, DEBUG = false) {
+    options = {}, DEBUG = false) {
     const samplePath = directory + "/" + sampleFilename;
     let loadParameter = FS.readFileSync(samplePath);
 
@@ -255,6 +257,8 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
     // console.log('typeof loadParameter: ' + typeof loadParameter)
 
     // set sample-specific options for OSMD visual regression testing
+    let includeSkyBottomLine = false;
+    let drawBoundingBoxString;
     if (osmdTestingMode) {
         const isFunctionTestAutobeam = sampleFilename.startsWith("OSMD_function_test_autobeam");
         const isFunctionTestAutoColoring = sampleFilename.startsWith("OSMD_function_test_auto-custom-coloring");
@@ -263,6 +267,11 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
         const defaultOrCompactTightMode = sampleFilename.startsWith("OSMD_Function_Test_Container_height") ? "compacttight" : "default";
         const isTestFlatBeams = sampleFilename.startsWith("test_drum_tuplet_beams");
         const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith("test_end_measure_clefs_staffentry_bbox");
+        if (isTestEndClefStaffEntryBboxes) {
+            drawBoundingBoxString = "VexFlowStaffEntry";
+        } else {
+            drawBoundingBoxString = options.boundingBoxes; // undefined is also a valid value: no bboxes
+        }
         osmdInstance.setOptions({
             autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
             coloringMode: isFunctionTestAutoColoring ? 2 : 0,
@@ -277,10 +286,10 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
             pageBackgroundColor: "#FFFFFF", // reset by drawingparameters default
             pageFormat: pageFormat // reset by drawingparameters default
         });
+        includeSkyBottomLine = options.skyBottomLine ? options.skyBottomLine : false; // apparently es6 doesn't have ?? operator
         osmdInstance.drawSkyLine = includeSkyBottomLine; // if includeSkyBottomLine, draw skyline and bottomline, else not
         osmdInstance.drawBottomLine = includeSkyBottomLine;
-        const drawBoundingBoxValue = isTestEndClefStaffEntryBboxes ? "VexFlowStaffEntry" : undefined;
-        osmdInstance.setDrawBoundingBox(drawBoundingBoxValue, false); // false: don't render (now)
+        osmdInstance.setDrawBoundingBox(drawBoundingBoxString, false); // false: don't render (now). also (re-)set if undefined!
         if (isTestFlatBeams) {
             osmdInstance.EngravingRules.FlatBeams = true;
             // osmdInstance.EngravingRules.FlatBeamOffset = 30;
@@ -330,8 +339,9 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
     for (let pageIndex = 0; pageIndex < Math.max(dataUrls.length, markupStrings.length); pageIndex++) {
         const pageNumberingString = `${pageIndex + 1}`;
         const skybottomlineString = includeSkyBottomLine ? "skybottomline_" : "";
+        const graphicalNoteBboxesString = drawBoundingBoxString ? "bbox" + drawBoundingBoxString + "_" : "";
         // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
-        const pageFilename = `${imageDir}/${sampleFilename}_${skybottomlineString}${pageNumberingString}.${imageFormat}`;
+        const pageFilename = `${imageDir}/${sampleFilename}_${skybottomlineString}${graphicalNoteBboxesString}${pageNumberingString}.${imageFormat}`;
 
         if (imageFormat === "png") {
             const dataUrl = dataUrls[pageIndex];