memononen / nanosvg

Simple stupid SVG parser
zlib License
1.69k stars 357 forks source link

Unitless quantities are entirely unaffected by DPI #225

Open mcclure opened 2 years ago

mcclure commented 2 years ago

Say I have this basic SVG file:

<svg width="356px" height="335px" viewBox="0px 0px 356px 335px" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="356px" height="335px" fill="white"/>
<rect x="40px" y="70px" width="53px" height="58px" fill="#D9D9D9"/>
</svg>

...and I open it by calling nsvgParse(svgString, "px", 0.25);. Nanosvg converts the units of the numbers in the files to pixels (a null conversion) and then divides them by four. Everything is as I expect when I read back the numbers.

However say I then try to load this other file, which is the same but instead of "px" all values are unitless:

<svg width="356" height="335" viewBox="0 0 356 335" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="356" height="335" fill="white"/>
<rect x="40" y="70" width="53" height="58" fill="#D9D9D9"/>
</svg>

The art program I am currently using emits SVGs with this format. In this case, opening with nsvgParse(svgString, "px", 0.25); does not produce any rescaling. The DPI value appears to get totally ignored.

Is this intentional?

Expected behavior:

  1. I find many sources online suggesting that if units are not provided on a value in an SVG file, the units are to be taken as px. Looking at section 8 of the SVG spec I am a little confused as to whether this is true, but spec section 8.3 seems to say this is the case for at least the viewport (IE, the width and height of the file itself):

    If the width or height presentation attributes on the outermost svg element are in user units (i.e., no unit identifier has been provided), then the value is assumed to be equivalent to the same number of "px" units (see Units).

    So I would expect a SVG file with no units to behave exactly the same as the px units.

  2. If the behavior is intentional, and DPI does not control the scaling of user units, then I would hope for some way to manually invoke scaling in place of using DPI, as scaling a document as it is loaded is useful.

  3. Finally, whichever way nanoSvg behaves, I think the comment in the header file explaining DPI should be a little clearer. Currently all it says is:

    // The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. // DPI (dots-per-inch) controls how the unit conversion is done.

    This description does not mention user units (unitless values) as an option, and the description of what the DPI value means is vague. What does DPI mean when the value is of type "cm", or "px"?

Analysis:

I tried running NanoSVG in a debugger and breaking on nsvg__convertToPixels. What I found is that with the "unitless" sample file the switch statement was always NSVG_UNITS_USER (expected). However when I tested with the sample file where every value is px, the switch statement went to NSVG_UNITS_CM (very surprising). In the code, DPI is applied to values of type pt, pc, mm, cm, in but is not applied to values of type user-unit or pixel.

In other words, from reading the code, the original intention of the code appears to be that the DPI should be ignored in both of my two sample files above; I assumed the bug was in the unitless case, but maybe it is the px case that has the bug. I do not understand where nanosvg is getting cm from because it is not either in the file nor the unit given to my nsvgParse() call.

Configuration

I am using nanosvg from the github repo, version 3bcdf2f3cdc

memononen commented 2 years ago

User units and px should behave the same. Setting DPI to anything else than 96 does not really make sense.

That cm thing sounds odd. I changed the nsvg__convertToPixelsto following:

static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, float length)
{
    NSVGattrib* attr = nsvg__getAttr(p);
    switch (c.units) {
        case NSVG_UNITS_USER:       printf("us: %f\n", c.value); return c.value;
        case NSVG_UNITS_PX:         printf("px: %f\n", c.value); return c.value;
        case NSVG_UNITS_PT:         printf("pt: %f\n", c.value); return c.value / 72.0f * p->dpi;
        case NSVG_UNITS_PC:         printf("pc: %f\n", c.value); return c.value / 6.0f * p->dpi;
        case NSVG_UNITS_MM:         printf("mm: %f\n", c.value); return c.value / 25.4f * p->dpi;
        case NSVG_UNITS_CM:         printf("cm: %f\n", c.value); return c.value / 2.54f * p->dpi;
        case NSVG_UNITS_IN:         printf("in: %f\n", c.value); return c.value * p->dpi;
        case NSVG_UNITS_EM:         printf("em: %f\n", c.value); return c.value * attr->fontSize;
        case NSVG_UNITS_EX:         printf("ex: %f\n", c.value); return c.value * attr->fontSize * 0.52f; // x-height of Helvetica.
        case NSVG_UNITS_PERCENT:    printf("%% : %f\n", c.value); return orig + c.value / 100.0f * length;
        default:                    printf("??: %f\n", c.value); return c.value;
    }
    return c.value;
}

And with you file with units in example2.c, I get following:

px: 356.000000
px: 335.000000
px: 356.000000
px: 335.000000
px: 40.000000
px: 70.000000
px: 53.000000
px: 58.000000
px: 1.000000

and with the unitless I get:

us: 356.000000
us: 335.000000
us: 356.000000
us: 335.000000
us: 40.000000
us: 70.000000
us: 53.000000
us: 58.000000
px: 1.000000

The DPI is used to convert between physical units and pixels. If your input units are in pixels/user, and output is in pixels, there should be no scaling involved.

mcclure commented 2 years ago

@memononen Let me see if I can make a test program to reproduce the incorrect scaling with px units. You are testing with master branch HEAD?

memononen commented 2 years ago

Yes, I have latest from master branch.

Vishnu-C commented 2 years ago

@memononen After all the above discussion, my question might me stupid, but I cannot figure it out myself 😒

What should be the dpi value if I am to parse in 'mm'?

memononen commented 2 years ago

If the input file units are in "mm", and you pass in "mm" to parse, then they should come out as is.

DPI comes to play when you convert pixels to mm. Pixels don't have a specific size, so you need to specify it. If your file is unitless (pixels) and you request "mm", and dpi=96, then: 100px = 100 * 25.4 / 96 = 26.46mm. Depending on your workflow, sometimes the authoring software lets you set the DPI, I think it is quite common to be 96.