libvips icon indicating copy to clipboard operation
libvips copied to clipboard

vertical alignment options for vips text

Open dloebl opened this issue 7 months ago • 3 comments

Currently, vips text seems to lack built-in options for vertical text alignment (top, middle, bottom, baseline) of single line text. I found a workaround using invisible placeholder characters to achieve proper vertical centering of text elements, but it isn't ideal.

vips text out.png --rgba "<span font=\"Sans 32\" background=\"green\" foreground=\"white\">Ship North Corp</span>"
Image

You can see, that the S sticks to the top, because of the descender p. This makes it difficult to center text on the base characters (middle of the S).

Current Workaround: To vertically center text on the baseline, I'm currently using invisible unicode characters as placeholders:

  • (with 1% opacity) to establish the top boundary
  • (with 1% opacity) to establish the bottom boundary
  • The actual visible text in between
vips text out.png --rgba "<span font=\"Sans 32\" background=\"green\" foreground=\"#00FF0001\">⎡</span><span font=\"Sans 32\" background=\"green\" foreground=\"white\">Ship North Corp</span><span font=\"Sans 32\" background=\"green\" foreground=\"#00FF0001\">⎮</span>"
Image

You can see that there is slightly more padding to the top, it still isn't ideal and there is of course added padding to the sides, but it helps with centering text. I'm wondering if there is an existing way to achieve better vertical alignment or perhaps an option that we could add to vips text?

dloebl avatar Sep 18 '25 13:09 dloebl

I think you can use a similar hack to the one mentioned in https://github.com/libvips/php-vips/issues/267#issuecomment-2746138586. For example, with this test program:

#!/usr/bin/env python3

import sys
import pyvips

canvas = pyvips.Image.black(500, 100)
x = 20
y = 50

# get centerline offset from logical position
A = pyvips.Image.text('A', dpi=300)
centerline = (A.height // 2) + A.yoffset

for arg in sys.argv[2:]:
    text = pyvips.Image.text(arg, dpi=300)
    canvas = canvas.insert(text, x + text.xoffset, y - centerline + text.yoffset)
    # use 20 as the inter-word space 
    x += text.width + text.xoffset + 20

# draw the expected centerline position for reference
canvas = canvas.draw_line(128, 0, y, canvas.width, y)

canvas.write_to_file(sys.argv[1])

And running:

$ ./text-align.py x.png Ship North Corp

I see: Image

kleisauke avatar Sep 18 '25 13:09 kleisauke

Thanks @kleisauke! This seems to work well for regular text: With ./text-align.py x.png "Ship North Corp" I also see Image However, this only seems to work well if the "A" or "S" are the tallest characters, it starts to break once emojis or other tall characters are added:

./text-align.py x.png "Ship North Corp 🚢"
Image

or

./text-align.py x.png "Ship North Corp ⎡"
Image

dloebl avatar Sep 19 '25 11:09 dloebl

I don't think it needs the A or S to be tallest. Any character that sits exactly on the baseline will work, since it's measuring baseline from logical.

For example, with the original code from https://github.com/libvips/php-vips/issues/267#issuecomment-2746138586 I can run (no quotes, so we get four separate text renders that must be vertically aligned):

$ ./text-align.py x.png Ship North Corp 🚢

To make:

Image

You can see the baseline is correct, the alignment is correct, and there's no clipping. You can swap the A for a w and you'll get the same result.

jcupitt avatar Sep 19 '25 12:09 jcupitt