2025, Oct 27 07:00
How to Place Text at 1/3 and 2/3 on PIL/Tkinter Images: Fixing Step Calculation with Proper Parentheses
Learn why PIL, ImageTk, and tkinter text coordinates stack in the center and how to fix them by dividing by segments, not /2 + 1. Code and step-by-step math.
Placing text accurately on a PIL image shown via tkinter can look deceptively simple until arithmetic gets in the way. A common scenario: you want to draw text at regular positions over a 900×900 image, expecting placements at 300×300, 300×600, 600×300, and 600×600. Instead, everything piles up near the center. The culprit isn’t a mismatch between PIL and ImageTk coordinate systems, but the way the math is grouped.
Problem setup
Below is a distilled example that calculates step sizes from the image dimensions and draws the same label in a 2×2 grid. The names are conventional, and the logic is the one that leads to the misplacement.
global painter, tk_img
painter = ImageDraw.Draw(pil_img)
face = ImageFont.load_default()
for row in range(1, int(2)+1):
    print(tk_img.height() / int(2) + 1)
    y_step = (tk_img.height() / int(2) + 1) * row
    for col in range(1, int(2)+1):
        x_step = (tk_img.width() / int(2) + 1) * col
        print(x_step)
        painter.text(xy=(x_step, y_step), text="TESTER", fill=(255,255,255), font=face)
tk_img = ImageTk.PhotoImage(pil_img)
image_holder.image = tk_img
image_holder.config(image=tk_img)What actually goes wrong
The issue is purely arithmetic. The expressions for x and y build a step using division by 2 and then add 1. In other words, the step is computed as (dimension / 2) + 1. For a 900×900 image, that’s roughly 451. When the loops multiply this step by 1 and 2, the coordinates become 451 and 902 in each axis. That gives attempted placements at (451,451), (451,902), (902,451), (902,902). Only the first one is inside the image; the remaining attempts fall on or beyond the bottom/right boundaries and won’t appear. It’s not a PIL vs ImageTk units problem; it’s the grouping of operations.
The intended goal is to split the space into thirds and place the text at the 1/3 and 2/3 positions. That means the step should be dimension / 3, not (dimension / 2) + 1. The +1 doesn’t belong after the division; it belongs in the divisor that defines the number of segments.
The fix
Parentheses change the meaning. Divide by the number of segments you want, not by 2 and then add 1. The corrected grouping divides by (2 + 1), yielding a clean third of the dimension.
global painter, tk_img
painter = ImageDraw.Draw(pil_img)
face = ImageFont.load_default()
for row in range(1, int(2)+1):
    y_step = (tk_img.height() / (int(2) + 1)) * row
    for col in range(1, int(2)+1):
        x_step = (tk_img.width() / (int(2) + 1)) * col
        painter.text(xy=(x_step, y_step), text="TESTER", fill=(255,255,255), font=face)
tk_img = ImageTk.PhotoImage(pil_img)
image_holder.image = tk_img
image_holder.config(image=tk_img)With a 900×900 image, the step becomes 300. The loop places text at 300 and 600 in both axes, producing the expected four positions at 300×300, 300×600, 600×300, and 600×600.
Why this matters
Operator precedence and grouping have very visible effects when you compute coordinates for graphics. A tiny shift in parentheses can move content outside the drawable area without triggering any error. It’s also easy to get distracted by the GUI stack and suspect unit mismatches between tkinter, ImageTk, and PIL. In cases like this, both libraries agree on pixel units; the math is what needs attention.
Another practical angle is iteration control. If the goal is to draw four instances in a 2×2 layout, the loops should produce exactly two steps along each axis. Expanding the ranges increases the number of placements and can push many of them out of bounds. Keeping the divisor tied to the intended partitioning avoids overshooting the canvas.
Takeaways
When distributing elements across an image, think in segments. If you want positions at 1/3 and 2/3, compute a step as dimension divided by the number of segments and then multiply by the index. Keep the divisor inside the parentheses so that you divide by the total number of partitions, not by a partial value plus an offset. And keep iteration counts aligned with how many placements you actually need on each axis.