pythonrotationpython-imaging-librarycoordinatesdrawing

Problem with offset when rotating text on an image (Pillow)


main.py

from PIL import Image, ImageDraw, ImageFont

base_img = Image.new("RGBA", (100, 100), (0, 255, 0))
txt = 'Sample <del color=red>text<del>'
rotate = 120
font = ImageFont.load_default(12)
SS = 4

temp_draw = ImageDraw.Draw(Image.new("RGBA", (max(1, int(0 * SS)), max(1, int(0 * SS))),(0, 0, 0, 0)))

bbox = temp_draw.textbbox((0, 0), txt, font)
txt_w, txt_h = bbox[2] - bbox[0], bbox[3] - bbox[1]

temp_img = Image.new("RGBA", (max(1, int(txt_w * SS)), max(1, int(txt_h * SS))), (0, 0, 0, 0))
temp_draw = ImageDraw.Draw(temp_img)

temp_draw.text((0, 0), txt, (0, 0, 0), font)

if 0 <= rotate <= 360:
    temp_img = temp_img.rotate(-rotate, expand=True, resample=Image.BICUBIC)
    b = temp_img.getbbox()
    w, h = temp_img.size

    if 0 <= rotate <= 90:
        x0 = -b[0]
        y0 = -b[1]
    elif 90 < rotate <= 180:
        x0 = -b[0]
        y0 = -b[1]
    elif 180 < rotate <= 270:
        x0 = -(w - b[2])
        y0 = -(h - b[3])
    elif 270 < rotate < 360:
        x0 = -b[0]
        y0 = -(h - b[3])

base_img.paste(temp_img, (x0, y0), temp_img)
base_img.show()

In short:

So far, it only works with the 0-90 degree branch. I also tweaked a few things, and it seems to be working almost perfectly for 91-180: y0 is definitely correct, but something else needs to be subtracted from x0...


Solution

  • If I understand you want to rotate around the bottom left corner.

    I would use geometry for this instead of calculations.

    First I describe it using images.
    To make it more visible I use gray background instead of transparent.
    And I add red dot in center of rotation.

    (I use real_height = ascender + descender instead of value from font.getbbox(text) so it puts text in the same place when it has letters like y j g (which add space at the bottom) and when it doesn't have y j g)

    1. Create image with text

    original text

    1. Create new image with size width*2,height*2 and put text so bottom left corner is in the center of new image (so red dot is in new_width//2, new_height//2)

    text with resized background

    1. Rotate (120 degrees) around center of new image
      (so red dot is still in the center -rotate_width//2, -rotate_height//2)

    rotated image

    1. Put it on final image using offset -rotate_width//2, -rotate_height//2

    put on final imagee

    And the same without gray background

    enter image description here


    Here minimal working code which generates all of the images.

    import sys
    from PIL import Image, ImageFont, ImageDraw
    
    TEST = True
    
    if TEST:
        TRANSPARENT = "gray"  # for tests
    else:
        TRANSPARENT = (0, 0, 0, 0)  # (x,x,x,0)
        # TRANSPARENT = (0, 0, 0, 128)  # (x,x,x,0)
    
    # --- get angle as parameter
    
    if len(sys.argv) == 1:
        angle = 45 + 90
    else:
        angle = int(sys.argv[1])
    
    # --- create real height (the same when text has `y` or not) ---
    
    # [Text anchors - Pillow (PIL Fork) 11.3.0 documentation](https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#text-anchors)
    
    font = ImageFont.truetype("Arial.ttf", 40)
    
    ascender, descender = font.getmetrics()
    real_height = ascender + descender
    print(f"{real_height = }")
    
    # ---
    
    text = "Stackoverflow"
    
    x0, y0, x1, y1 = font.getbbox(text)
    box_w = x1 - x0
    box_h = y1 - y0
    print("box xy:", x0, y0, x1, y1)
    print("box w, h:", box_w, box_h)
    
    # --- create text with image ---
    
    txt = Image.new("RGBA", (box_w, real_height), TRANSPARENT)
    txt_draw = ImageDraw.Draw(txt)
    txt_draw.text((-x0, 0), text, font=font)
    
    txt.save("output-1-text.png")
    
    # --- resize background ---
    
    for_rotation = Image.new(
        "RGBA",
        (box_w * 2, real_height * 2),
        TRANSPARENT,
    )
    for_rotation.paste(txt, (box_w, 0))
    
    # - add red dot -
    if TEST:
        for_rotation_draw = ImageDraw.Draw(for_rotation)
        for_rotation_draw.circle((box_w, real_height), 2, fill="red")
        for_rotation_draw.circle((box_w, real_height), 5, outline="red")
        for_rotation_draw.line(
            (box_w - 10, real_height, box_w + 10, real_height), fill="red", width=1
        )
        for_rotation_draw.line(
            (box_w, real_height - 10, box_w, real_height + 10), fill="red", width=1
        )
    
    for_rotation.save("output-2-for-rotation.png")
    
    # --- rotate image ---
    
    rotated = for_rotation.rotate(-angle, expand=True)
    
    rotated_w, rotated_h = rotated.size
    print("rotated:", rotated_w, rotated_h)
    
    rotated.save(f"output-3-rotated-{angle}.png")
    
    # --- put text on final image ---
    
    offset_w = -rotated_w // 2
    offset_h = -rotated_h // 2
    
    final = Image.new("RGBA", (box_w, box_w), "green")
    # combined = Image.alpha_composite(new, txt)  # different function without destination position
    final.alpha_composite(rotated, dest=(offset_w, offset_h))
    
    final.save(f"output-4-final-{angle}.png")
    

    Angle: 135 (45+90)

    enter image description here

    Angle: 30

    enter image description here

    Angle: 45

    enter image description here

    Angle: 60

    enter image description here

    Angle: 90

    enter image description here