linebender / resvg

An SVG rendering library.
Mozilla Public License 2.0
2.79k stars 225 forks source link

Bounding box issue with fonts #823

Closed jermy closed 1 month ago

jermy commented 1 month ago

As noted #822, I've got a workflow which involves using resvg to generate SVGs containing just text, and having issues with the calculated bounding box. I suspect this might be related to the font in question.

The workflow is as follows:

  1. Get appropriate font - in this case, I'm using https://fonts.google.com/specimen/Pacifico
  2. Generate basic SVG with just text:
    <svg xmlns="http://www.w3.org/2000/svg">
    <text alignment-baseline="mathematical" fill="Yellow" font-family="Pacifico" font-size="20" transform="rotate(0)">
      Hello World
    </text>
    </svg>

    This renders as: pacifico-hello-world-noboundary

  3. Parse in resvg and get bounding box from resvg_get_image_bbox. In this case, I get x=0 y=-13.029999732971191 width=113.95999145507812 height=35.119998931884766
  4. Insert that as the viewbox in the SVG, and also draw a box around that area:
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -13.029999732971191 113.95999145507812 35.119998931884766">
    <rect width="113.95999145507812" height="35.119998931884766" x="0" y="-13.029999732971191" style="fill-opacity:0;stroke-width:1;stroke:red"/>
    <text alignment-baseline="mathematical" fill="Yellow" font-family="Pacifico" font-size="20" transform="rotate(0)">
      Hello World
    </text>
    </svg>

    Which renders as: pacifico-hello-world

  5. Note that the bounding box doesn't include all of the text
  6. If I enlarge the viewbox by 10 pixels on each side, then I get: pacifico-hello-world-smaller

I'm using (eg) ./resvg --background grey --zoom 4 --use-font-file fonts/Pacifico.ttf /tmp/pacifico-hello-world-smaller.svg /tmp/pacifico-hello-world-smaller.png to generate all of these, with the resvg version tagged v0.43.0, although this is the same behaviour as 0.41.

Should the whole text be included in the bounding box? Should there be quite so much space above/below since the characters don't actually extend into that space? The alignment-baseline will affect the initial position of the text on the canvas, but not the width or height otherwise.

Here's another SVG with the same process to show the size of the ascenders/descenders:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -23.029999732971191 202.37998962402344 37.559999465942383">
   <rect  width="182.37998962402344" height="17.559999465942383" x="0" y="-13.029999732971191" style="fill-opacity:0;stroke-width:1;stroke:red"/>
   <text fill="Yellow" font-family="Pacifico" font-size="10" transform="rotate(0)">
      The quick brown fox jumps over the lazy dog
   </text>
</svg>

./resvg --background grey --zoom 3 --use-font-file fonts/Pacifico.ttf /tmp/quickbrownfox.svg /tmp/quickbrownfox.png

quickbrownfox

LaurenzV commented 1 month ago

Should the whole text be included in the bounding box? Should there be quite so much space above/below since the characters don't actually extend into that space? The alignment-baseline will affect the initial position of the text on the canvas, but not the width or height otherwise.

I believe this is intended. I wrote this code, but it's been a while, but I just stumbled upon this comment: https://github.com/RazrFalcon/resvg/blob/5141a830346576edab4c52244185ef95e2a07dc5/crates/usvg/src/text/layout.rs#L395-L400

One of the main reasons that the whole bounding box calculation in usvg exists is to be able to resolve attributes that have the unit objectBoundingBox, which requires us to know the bounding box of the object a paint is applied to to resolve it. In the case of text, the SVG explicitly mentions that the text bounding box should be derived from the font metrics, and not from the actual shape of the glyphs that are drawn.

As a workaround, you can try using the stroke bounding box instead, which does give me an exact fit for the text when trying it out: test

Yes, it's a bit inconsistent that bounding box and stroke bounding box work so differently, but it is what it is.

LaurenzV commented 1 month ago

I guess we should probably add an additional text_bounding_box property to Text which contains the bounding box as required by the SVG spec, and then bounding box should just be the actual shape bounding box

jermy commented 1 month ago

Would it be reasonable to add resvg_get_image_stroke_bbox? (Similarly to how I'd like to add resvg_get_image_transform for my other open issue)

LaurenzV commented 1 month ago

No idea, I have zero clue about the C++ API. I only worked on the Rust part. But if no API exists, I guess it makes sense to add it.

jermy commented 1 month ago

@RazrFalcon any thoughts on this?

RazrFalcon commented 1 month ago

Insert that as the viewbox in the SVG, and also draw a box around that area:

Don't do this. That's not what viewBox is for. Use width/height instead. Or at least set just width/height to viewBox, aka 0 0 W H.

Note that the bounding box doesn't include all of the text

Yes, object bbox includes font metrics, not the path bbox. See https://razrfalcon.github.io/notes-on-svg-parsing/text/bbox.html The problem with SVG is that it never works in a way you think it should. It's very unintuitive. Especially when it comes to text.

What you need here is a layer bbox, aka Node::abs_layer_bounding_box, but I guess we do not expose it in the C API. And resvg_get_image_bbox is Node::abs_bounding_box, which is a very different thing.

Sadly, I haven't wrote a chapter about different types of bounding boxes in SVG in "Notes on SVG parsing" yet. Basically, there are 3 types of bounding boxes: object bbox in object units, object bbox in user/canvas units and "layer" bbox. All 3 are different and SVG uses all 3 depending on the situation.

Try patching resvg_get_image_bbox to use abs_layer_bounding_box instead of abs_bounding_box and see if it solves your issue. If not, than there is probably a bug in text layer bbox caclulation.

And yes, resvg_get_image_size will always trim/clip your image when you're not providing the SVG size. That's just how SVG works, unfortunately... SVG size auto-detection is trash and you must not rely on it. It doesn't respect negative positions and always uses object canvas bboxes, which is never what you want. But that's by the spec. I guess I would have to improve documentation...

abs_layer_bounding_box is what you're looking for, and yes, it's missing from the C API. And it has nothing to do with viewBox flattening.

RazrFalcon commented 1 month ago

@LaurenzV

Yes, it's a bit inconsistent that bounding box and stroke bounding box work so differently, but it is what it is.

It is inconsistent, but unfortunately we're simply following the spec. It's not a bug. Stroke bbox and layer bbox are not in the spec to begin with. I made them up for my use cases. Only object bbox is in the spec and it doesn't work the way you think.

LaurenzV commented 1 month ago

Try patching resvg_get_image_bbox to use abs_layer_bounding_box instead of abs_bounding_box and see if it solves your issue. If not, than there is probably a bug in text layer bbox caclulation.

That most likely won't work, because abs_layer_bounding_box is also based on abs_bounding_box, which has the same issue.

RazrFalcon commented 1 month ago

abs_layer_bounding_box is completely different to abs_bounding_box, since it prefers stroke bbox to object bbox. And stroke bbox for text is a path bbox, even when we do not have a stroke.

https://github.com/RazrFalcon/resvg/blob/5141a830346576edab4c52244185ef95e2a07dc5/crates/usvg/src/text/mod.rs#L214-L217

Welcome to SVG...

jermy commented 1 month ago

In this:

boundingboxes

Assuming the latter is the one that I should add to the C API, is there a sensible name? The above test is using resvg_get_image_layer_bbox but I don't think that's clear. Using resvg_get_image_stroke_bbox would match the API for nodes, and mostly does the same thing, even if it uses the group layer_bounding_box internally?

jermy commented 1 month ago

Insert that as the viewbox in the SVG, and also draw a box around that area:

Don't do this. That's not what viewBox is for. Use width/height instead. Or at least set just width/height to viewBox, aka 0 0 W H.

But that's precisely what viewBox is for since it needs to handle negative numbers. I'm not interested in the first image in this thread!

I'm interested in creating an image containing text then rendering it with a tight crop around the text and there are two ways to do it:

  1. Insert a SVG viewbox with the calculated size of the bounding box.
  2. Move every text element to the origin by offsetting their x/y with the calculated bounding box's x/y, then setting width and height.

The former is straightforward, the latter might be impossible (or at least more complicated) if the x/y values involve percentages, and involves parsing the whole XML document.

Yes, object bbox includes font metrics, not the path bbox. See https://razrfalcon.github.io/notes-on-svg-parsing/text/bbox.html The problem with SVG is that it never works in a way you think it should. It's very unintuitive. Especially when it comes to text.

Thanks for that - I'm guessing the cropped 'd' in these images is due to the bounding box being based on the advancement?

Sadly, I haven't wrote a chapter about different types of bounding boxes in SVG in "Notes on SVG parsing" yet. Basically, there are 3 types of bounding boxes: object bbox in object units, object bbox in user/canvas units and "layer" bbox. All 3 are different and SVG uses all 3 depending on the situation.

This relates more to #822 where I claim this is precisely what I mean about different coordinate systems - the difference between and object units and canvas units, that different functions return different types of units, and the fact that since the "viewbox flattening" changes we no longer have a way in the C API to translate these. I can obviously just expose abs_transform() for the root element in a local fork, but would prefer to not divert too far from what you're doing.

RazrFalcon commented 1 month ago

resvg_get_image_bbox is the right name. We just call the wrong method inside it. Right now it calls abs_bounding_box, which is almost never what you want.

As for stroke vs layer, they are different and you have to use "layer". Unlike stroke bbox, layer bbox includes filters region as well. Stroke bbox for text is undefined to begin with, afaik. It's just a temporary value. As a user/caller you should care only about layer bbox. I might even hide object and stroke bboxes in the future to reduce confusion.

But that's precisely what viewBox is for since it needs to handle negative numbers.

viewBox is just a transform. And usvg flattens it for you. Think of it as a syntax sugar. If you want to shift an image, either pass the right transform to the rendering API or add a root group with your transform (which is what viewBox is doing automatically).

Thanks for that - I'm guessing the cropped 'd' in these images is due to the bounding box being based on the advancement?

Yes, glyph bbox based on TrueType metrics and glyph outline bbox do not guarantee to match. And the SVG spec requires us to use the first one during SVG processing.

the difference between and object units and canvas units, that different functions return different types of units

Sort of. But C API always uses absolute/canvas coordinates. Object bbox units are for internal use only and eventually would be removed from Rust API as well. So no, you don't have to worry about it and I have no idea how it could have affected your code.

The only problem with C API is that it returns an image bbox without stroke+filters+text-bbox. That's it.