SVG: Crisper Lines

• May 16, 2018 - 20:53
Reported version
S5 - Suggestion

SVG lines render best when they are precisely placed on the canvas. SVG centers all lines on the coordinates, so lines with a stroke width of an odd number of pixels must be placed on a coordinate that ends in .5; lines with even numbered stroke widths should be placed on integer values. The is especially important for horizontal and vertical lines, especially in MuseScore where horizontal staff lines and vertical stems and bar lines are everywhere. Then there are ledger lines and beams, which are also very common (beams may not be purely horizontal, but their left and right edges are always vertical).

With the DPI_F = 5 factor in 3.0, SVG Export is exporting these lines at whole number stroke widths, which makes this improvement plausible. (this change cannot be applied to version 2)
Staff lines stroke width is 2px, barlines is 4px, both even numbers.
Stems and ledger lines are 3px.
The other reason that DPI_F enables this change is that now the canvas is 5 times larger in both dimensions. It means that rounding values, changing them by a maximum of 0.5 pixels, will not be visible to anyone's eye, not even on an 8K monitor. The only thing that's visible is the improved rendering, crisper lines, sharper edges.

Here is the diff page for the branch:…
It changes 2 functions in 1 file: drawPath() and drawPolygon(). They happen to be the last two functions in the file.
edit: the x and y code is reversed here:…
I'm in the middle of something else on my working branch right now. I'll fix this and post new example files.

It's a simple set of changes:
1) Beams are drawn by Qt as path elements in SVG. They have no stroke, only fill, so the best thing is to round all the x and y coordinates in the path. Paths without fill look best with whole number values. This change is in drawPath().

2) Staff lines and bar lines are both even numbers, so all of their coordinates can be rounded.
3) Stems need their x coordinate to end in .5
4) Ledger lines need their y coordinate to end in .5
All three of these - 2,3,4 - are drawn as polyline elements in SVG. This change is in drawPolygon().

Currently MuseScore exports SVG files that render as very large pages by default. I have edited the two attached SVG files to zoom them out enough to see them in a browser window without scrollbars. It's a more normal size for viewing. In your browser, or whatever container, zoom in and out and compare the two files. I do it with two tabs, and switch between the tabs with the zoom levels aligned. Your browser/container might not give you a lot of options, but at 100% I can see differences, stems that are fuzzy versus clean, for example. Zooming to different levels exposes different fudging/rounding that the browser does. Part of the reason you don't see a big difference here is because this score has the default spatium value of 25.000. That means the staff lines are already on integer y-coordinates, so the new code has no effect on them.

This is an small change to one file that improves SVG file rendering at any zoom level, in any container. It's ready for a PR whenever you are.

Also note: I have edited these SVG files to extend the staff lines to the proper width. For more information on the fix I have ready for that open issue, see here:

Attachment Size
y-current-1.svg 171.63 KB
y-crispy-1.svg 168.82 KB


In the interest of absolute precision, I present this deficiency:
This new code moves stems and ledger lines without moving the corresponding note heads, accidentals, etc.
within the SVG only , not in any score

A few things to consider:
1) SVG Export iterates over elements by page, and thus by system, not by segment/chordrest. It is a kind of printing process.
2) svggenerator.cpp doesn't know about the score or segments, it focuses on the element it is painting at this moment. So how does the code know which stem or ledger line matches which note head?
3) Note heads in SVG are drawn as text elements, using the MScore font family.

Thankfully, in the current scheme, stems and ledger lines are painted before note heads. While painting each stem/ledger, the code could collect the x and/or y offset by tick in a std::map<int, pair<qreal, qreal>>. Later, when it paints the note heads, it could apply the x and/or y offset to the elements' x and y coordinates, using Element.tick() to lookup the offsets. All of the offsets are <= 0.5
edit - actually Accidentals appear before stems and ledger lines in the current sort order. that would need to change. My working branch sorts things differently, which is why I misstated this originally.

I'll let you know how it goes implementing this in my working branch, and you can decide if you want to include it. It's a minute set of adjustments, hopefully the code is similarly minute.

The code has been updated here:
I have reattached all the files to this comment for ease of access.
I have also attached 3 files where the score's spatium is adjusted slightly by switching units to inches. The spatium changes from 1.764mm to 1.753mm. This causes more noticeable differences in the before and after files. A bigger change to the spatium would probably illustrate bigger differences, but I don't feel like generating tons of files from two different compiles.

Attachment Size
y-current-1.svg 171.63 KB
y-crispy-1.svg 168.75 KB
y-1.753-current-1.svg 179.8 KB
y-1.753-crispy-1.svg 176.97 KB
y-spat.mscz 15.33 KB

I have added the code to normalize notes, note dots, articulations, and hooks relative to the stem and ledger line position offsets:…

Accidentals are problematic because they naturally sort before stems and ledger lines. Any suggestions for another way to sort things here?

Are there other element types that are aligned around the note head/stem? Not things like staff text, where a half a pixel is meaningless. Things that are right around the note itself. I don't think glissandi should be adjusted, as they fit between two notes. Triplets fit between 3 notes. Articulations covers a lot of sub-types.

There is another setting that impacts the rendering of lines/edges, and that's the SVG shape-rendering attribute.
If you set <svg shape-rendering="crispEdges">, then zoom in and out, you can see a marked difference in the consistency of the staff lines, bar lines and stems.
SVG Export needs its own dialog or preferences for things like that, or people need to learn how to edit their .svg files. IMO, for sheet music, crispEdges looks better across different zoom levels. It's not perfect, but it's less glitchy than without it. But since it's not part of MuseScore SVG Export today, I won't confuse things by posting examples. I mention it as something to understand and/or play with. Maybe some time in the future there will be an SVG Export Options pane or dialog.

I misspoke in my previous post about this: These are not text elements, they are path elements. I confused my working branch with the MuseScore master when verifying this. So this modifies all of each path's x and y coordinates by the offset amounts. It's not a lot of extra code, though it must be done by staff, so it's a lightly nested set of STL containers:…

I am posting one sample file, generated by the master, none of my code, in a debug build. All the lines and beams are fine, but the clefs, notes, rest, and other things are way too large. This was happening for me running the debug .exe file outside the debugger previously, but now it always outputs SVG like this, even running inside the debugger. Any ideas? I haven't tried a release build yet. Maybe tomorrow. This doesn't affect my working branch because I use text (MScore font family), not paths for these elements.

Attachment Size
y-1.svg 171.6 KB