Skip to content

feat(sheetmusic): open MusicXML scores in the viewer - #52

Open
skjnldsv wants to merge 1 commit into
mainfrom
feat/musicxml
Open

skjnldsv wants to merge 1 commit into
mainfrom
feat/musicxml

Conversation

@skjnldsv

Copy link
Copy Markdown
Contributor

Sheet music opens in the viewer, rendered with opensheetmusicdisplay. Picks up nextcloud/viewer#2752 by @bneumann, which has been open since March 2025 and closes nextcloud/viewer#424. Written fresh for this package rather than cherry-picked, but the idea and the choice of renderer are theirs and the commit carries their Co-authored-by.

The blocker that PR asked about is gone

They registered application/octet-stream alongside the two score types and sniffed the extension inside the component, and said so in the description: at the time the extensions had no mapping, so a score arrived as an unknown binary. They map since server 32:

"musicxml": ["application/vnd.recordare.musicxml+xml"],
"mxl":      ["application/vnd.recordare.musicxml"],

So only those two are claimed here. Registering the fallback type would have handed this handler every unrecognised file on the server, and there is a test pinning that it does not.

Two megabytes, loaded by the people who want it

opensheetmusicdisplay unpacks to 2 MB. It is imported inside the component rather than at the top of the module, so it does not sit in the path every viewer user walks:

dist/chunks/Sheetmusic.mjs   3329 bytes

The renderer stays a bare external import inside that chunk, so a consuming bundler gives it a chunk of its own and fetches it the first time someone opens a score. Opening a photo never touches it.

Two things this format needs that the others do not

A .mxl is a zip holding the score, so the file is handed to the renderer as the bytes it arrived as, not as text. Both forms are tested.

The bigger one: the viewer keeps a handler hidden until it emits loaded, and an element that is not displayed has no width. Drawing first and reporting afterwards deadlocks — the score renders into a frame of zero width and comes out as <svg width="0">. That is what the first four attempts here did, and it is visible in the debugging: element.offsetWidth was 0 at render time even after a frame. So this handler reports loaded once the score is parsed, then waits for the frame to have a width before drawing. The other handlers never meet this because an image sizes itself.

Covered

352 unit tests, 4 new: both score mimes enable the handler, a JPEG does not, and application/octet-stream does not.

52 e2e across Chromium and Firefox, 3 new, against two real fixtures added to the playground. They assert the score is actually drawn rather than that the modal opened: more than ten <path> elements inside the rendered SVG, and the title read out of the file (Viewer test score, and Viewer test score compressed for the zipped one, so the two cannot pass for each other). A third opens a JPEG and asserts no sheet-music element appears.

Note

Tested against the playground, not a live server. The mimes come from the server mapping above, so a score uploaded to a real instance should arrive as the right type, but that is worth confirming when you try it.

Not attempted: page navigation, zoom, transposition or playback. This renders the score and nothing more.

👾 This pull request was assisted by Claude Code, commits carry an Assisted-by trailer.

Renders .musicxml and .mxl with opensheetmusicdisplay, so sheet music
opens where every other file does.

The renderer is two megabytes, so it is imported inside the component
rather than at the top of the module: the handler's own chunk is 3 kB
and the renderer is fetched the first time somebody opens a score, never
for anyone who only opens photos.

Two things the format needs that the other handlers do not. A .mxl is a
zip holding the score, so the file goes to the renderer as the bytes it
arrived as rather than as text. And the viewer keeps a handler hidden
until it reports having loaded, which leaves nothing to measure and a
score drawn zero pixels wide, so this one reports first and draws once
the frame it was given is really there.

Only the two score types are claimed. The earlier attempt at this also
registered application/octet-stream, because the extensions had no
mapping then and a score arrived as an unknown binary; they map since
server 32, and claiming that type would have handed this handler every
unrecognised file on the server.

Co-authored-by: bneumann <bneumann@users.noreply.github.com>
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
@skjnldsv skjnldsv added status: review Waiting for reviews type: enhancement 🚀 New feature or request AI assisted labels Sep 19, 2026
@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.62%. Comparing base (a640b9f) to head (55e8b22).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #52      +/-   ##
==========================================
+ Coverage   88.11%   88.62%   +0.50%     
==========================================
  Files          37       39       +2     
  Lines        2625     2742     +117     
  Branches      558      561       +3     
==========================================
+ Hits         2313     2430     +117     
  Misses        305      305              
  Partials        7        7              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bneumann bneumann left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man this is brills. Thanks for picking up what I abandoned. To be honest I totally forgot about my work here, so it is also really nice to mention me.
For the code: It looks good, there are even tests which are - in my opinion - only plausibility tests because you need to see some bigger scores for yourself to make sure they render correctly. But then again that is more an OSMD problem.
The loading and rendering part looks good, you are using watch for the bigger scores and preventing the 0 height and 0 width problems with that.
From my experience with OSMD it looks ready to merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI assisted status: review Waiting for reviews type: enhancement 🚀 New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for MusicXML files

2 participants