kyamagu / skia-python

Python binding to Skia Graphics Library
https://kyamagu.github.io/skia-python/
BSD 3-Clause "New" or "Revised" License
232 stars 42 forks source link

Getting correct text bounds #168

Open prashant-saxena opened 2 years ago

prashant-saxena commented 2 years ago

Describe the bug I'm trying to render a text at certain point and a rectangle around it. This example is doing exactly what I need. In python implementation font.measureText returns only advance of text. As per the doc only "TextBlob" class is having "bounds" method and to calculate it first you need to create an instance of it:

textblob = skia.TextBlob(txt, self.font)
bounds = textblob.bounds()

I think it's an unnecessary step to calculate bounds. We already have our font instance available to calculate bounds. The doc also suggests about paint.getTextBounds(...) method but it's not implemented yet.

To Reproduce Steps to reproduce the behavior: As shown in the above example at link:

# Init
typeface = skia.Typeface('Arial')
font = skia.Font(typeface, 16.0, 1.0, 0.0)
textblob = skia.TextBlob('Hello World', font)
bounds = textblob.bounds()
bounds = bounds.makeOffset(100,100)

# Draw
canvas.drawString('Hello World', 100, 100, font, p)
canvas.drawRect(bounds, strokePaint)

bug3 Neither rect is having correct size nor it's drawn properly around the text. Expected behavior Get correct text bounds.

Desktop (please complete the following information):

Additional context A simple example that shows getting the correct text bounds.

Regards.

prashant-saxena commented 2 years ago

Ok, I have created this little function for text alignment and hope other users would find it useful.

# Horizontal align
ALIGN_LEFT      = 1 << 0 # Default, align text horizontally to left.
ALIGN_CENTER    = 1 << 1 # Align text horizontally to center.
ALIGN_RIGHT     = 1 << 2 # Align text horizontally to right.
# Vertical align
ALIGN_TOP       = 1 << 3 # Align text vertically to top.
ALIGN_MIDDLE    = 1 << 4 # Align text vertically to middle.
ALIGN_BOTTOM    = 1 << 5 # Align text vertically to bottom.
ALIGN_BASELINE  = 1 << 6 # Default, align text vertically to baseline.

def draw_text(canvas, txt, x, y, font, paint, flags):
    # Get bounds of txt
    rect = skia.Rect.MakeXYWH(0, 0, 0, 0)
    font.measureText(txt, bounds=rect)

    px = 0.0
    py = 0.0
    if ALIGN_LEFT & flags:
        px = rect.x()
    elif ALIGN_CENTER & flags:
        px = rect.width()/2.0
    elif ALIGN_RIGHT & flags:
        px = rect.width()

    if ALIGN_TOP & flags:
        py = rect.y()
    elif ALIGN_MIDDLE & flags:
        py = -rect.height()/2.0
    elif ALIGN_BOTTOM & flags:
        py = 0.0
    elif ALIGN_BASELINE & flags:
        pass

    canvas.drawString('Hello World', x-px, y-py, font, paint)

#Example
draw_text(canvas, "Hello World", 100, 100, font, paint, ALIGN_CENTER|ALIGN_MIDDLE)
HinTak commented 1 month ago

But your code answered your own question -

rect=skia.Rect() # the default constructor should do
advanced = font.measureText(txt, bounds=rect)

The routine returns the advance and the bound box, and one of them needs to be passed in as a parameter to be filled.

And it seems that drawstring uses the top left as the origin.

What exactly are you complaining? Lack of a full example (Feel free to submit a pull)? Lack of an obvious named method (we can consider adding that)?

We can't change the meaning of upstream tokens - measureText seems to calculate the ink box (the box of having any ink), whereas the TextBlob's bounds method really isn't expected to be the ink box - it is probably documented somewhere what exactly it does, but since it is "textblob", it probably means what it means, user is expected to see "comfortable spaces" around it, which means if you stack one above the other, nothing touches, and perhaps also, side ways.

Where does it say anything about paint.getTextBounds(...)? The paint class doesn't know anything about text, why should it have such a method? I just looked upstream and there isn't.

What do you like to have changed or added? As I said, the one thing we cannot change is that we can't change the meaning of upstream tokens. The skia.TextBlob.bounds skia-python call is a direct call to upstream's SkTextBlob::bounds c++ method.

HinTak commented 1 month ago

"Text bounds" is a very vague term. There is the ink box (actual ink used), there is the design box (the font designer's recommended line spacings plus ascenders and descenders), offsetted by the left bearing and right bearings (the font designer's intentional drawing outside of the design box). Glyphs in Arabic fonts often have negative side-bearings - Arabic letters often extends outside their advance width. (I.e. one letter going over the top or the bottom of the previous letter or the next letter.)

I think you mean the ink box, but TextBlob.bounds is very much NOT that.

HinTak commented 1 month ago

Upstream documentation : https://api.skia.org/classSkTextBlob.html

=== Returns conservative bounding box.

Uses SkPaint associated with each glyph to determine glyph bounds, and unions all bounds. Returned bounds may be larger than the bounds of all glyphs in runs.

===