How to keep text inside a circle using Cairo? - python

I a drawing a graph using Cairo (pycairo specifically) and I need to know how can I draw text inside a circle without overlapping it, by keeping it inside the bounds of the circle. I have this simple code snippet that draws a letter "a" inside the circle:
'''
Created on May 8, 2010
#author: mrios
'''
import cairo, math
WIDTH, HEIGHT = 1000, 1000
#surface = cairo.PDFSurface ("/Users/mrios/Desktop/exampleplaces.pdf", WIDTH, HEIGHT)
surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, WIDTH, HEIGHT)
ctx = cairo.Context (surface)
ctx.scale (WIDTH/1.0, HEIGHT/1.0) # Normalizing the canvas
ctx.rectangle(0, 0, 1, 1) # Rectangle(x0, y0, x1, y1)
ctx.set_source_rgb(255,255,255)
ctx.fill()
ctx.arc(0.5, 0.5, .4, 0, 2*math.pi)
ctx.set_source_rgb(0,0,0)
ctx.set_line_width(0.03)
ctx.stroke()
ctx.arc(0.5, 0.5, .4, 0, 2*math.pi)
ctx.set_source_rgb(0,0,0)
ctx.set_line_width(0.01)
ctx.set_source_rgb(255,0,255)
ctx.fill()
ctx.set_source_rgb(0,0,0)
ctx.select_font_face("Georgia",
cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
ctx.set_font_size(1.0)
x_bearing, y_bearing, width, height = ctx.text_extents("a")[:4]
print ctx.text_extents("a")[:4]
ctx.move_to(0.5 - width / 2 - x_bearing, 0.5 - height / 2 - y_bearing)
ctx.show_text("a")
surface.write_to_png ("/Users/mrios/Desktop/node.png") # Output to PNG
The problem is that my labels have variable amount of characters (with a limit of 20) and I need to set the size of the font dynamically. It must fit inside the circle, no matter the size of the circle nor the size of the label. Also, every label has one line of text, no spaces, no line breaks.
Any suggestion?

I had a similar issue, where I need to adjust the size of the font to keep the name of my object within the boundaries of rectangles, not circles. I used a while loop, and kept checking the text extent size of the string, decreasing the font size until it fit.
Here what I did: (this is using C++ under Kylix, a Delphi derivative).
double fontSize = 20.0;
bool bFontFits = false;
while (bFontFits == false)
{
m_pCanvas->Font->Size = (int)fontSize;
TSize te = m_pCanvas->TextExtent(m_name.c_str());
if (te.cx < (width*0.90)) // Allow a little room on each side
{
// Calculate the position
m_labelOrigin.x = rectX + (width/2.0) - (te.cx/2);
m_labelOrigin.y = rectY + (height/2.0) - te.cy/2);
m_fontSize = fontSize;
bFontFits = true;
break;
}
fontSize -= 1.0;
}
Of course, this doesn't show error checking. If the rectangle (or your circle) is too small, you'll have to break out of the loop.

Since the size of the circle does not matter you should draw them in the opposite order than your code.
Print the text on screen
Calculate the text boundaries (using text extents)
Draw a circle around the text that is just a little bigger from the text.

Related

Pillow, centering of text not working, how is this accomplished?

I have tested the calculations and the math is correct (and takes into account the height and width of the font), but after Python creates the image and I put it into Photoshop, the vertical and horizontal centering of the text is not correct. Should I be doing something else with my code?
from PIL import Image, ImageDraw, ImageFont
# base = Image.open("Images/Phones/KK17018_Navy_KH10089.jpg").convert("RGBA")
base = Image.open('Images/Tablets/KK17076_Hunter_KH235.jpg').convert('RGB')
# Create blank rectangle to write on
draw = ImageDraw.Draw(base)
message = 'GGS'
num1, num2 = base.size
bounding_box = [0, 0, num1, num2]
x1, y1, x2, y2 = bounding_box # For easy reading
# font = ImageFont.truetype('Fonts/Circle/Circle Monograms Three White.ttf', size=413)
font = ImageFont.truetype('Fonts/Modern/Vhiena_Monoline.otf', size=800)
font2 = ImageFont.truetype('Fonts/Modern/Vhiena_Base.otf', size=800)
font3 = ImageFont.truetype('Fonts/Modern/Vhiena_Extrude A.otf', size=800)
# Calculate the width and height of the text to be drawn, given font size
w, h = draw.textsize(message, font=font3)
# Calculate the mid points and offset by the upper left corner of the bounding box
x = (x2 - x1 - w) / 2 + x1
y = (y2 - y1 - h) / 2 + y1
# Write the text to the image, where (x,y) is the top left corner of the text
draw.text((x, y), message, align='center', font=font3, fill='orange')
draw.text((x, y), message, align='center', font=font2, fill='black')
draw.text((x, y), message, align='center', font=font, fill='white')
# Draw the bounding box to show that this works
# draw.rectangle([x1, y1, x2, y2])
base.show()
base.save('test_image.jpg')
The text's upper left x,y coordinates should be (874,1399.5), but in Photoshop, they show as (875,1586). The Python code above does calculate (874,1399.5) correctly, but something is placing the font lower than it should be.
Also, I'm stacking fonts like this because it gives a regular font, a shadow font and a font that makes it look beveled in the middle of the font. Would there be a better method or is stacking fonts an ok practice?
EDIT: Upon further testing, something is adding a 22% top margin to the font as the font size increases. I could account for this, but this seems rather odd.
I don't have your exact font handy to test, but this is probably because Pillow's textsize and text methods, by default, anchor to the ascender height instead of the actual rendered top; see this note in the docs. Try using textbbox instead of textsize, and specifying the top anchor in both that and the text method, and see if that behaves more intuitively.
Note that you could probably just anchor the text to 'mm', middle/middle, to center it based on the coordinates of the image's midpoint. This anchors vertically to halfway between the ascenders and descenders, though, so it may not actually look centered, depending on the font and what glyphs you render.

Writing a number/text in a Pygame Rect

At the moment I create a Visualization Tool for my Sudoku solver.
Now I want to display the numbers in the grid with pygame.
def draw(win):
global grid
w = 70
x,y = 0,0
for row in grid:
for col in grid:
rect = pygame.Rect(x,y,w,w)
pygame.draw.rect(win,BLACK,rect)
rect2 = pygame.Rect(x+2, y+2, w-1, w-1)
pygame.draw.rect(win,WHITE,rect2)
pygame.display.flip()
x = x + w
y = y + w
x = 0
The code is ugly, I know but my grid works. And I can iterate over it. My problem now, I don't know how to fill the Rect with a Number?
I want to add the Number from the Sudoku grid at the position [row][col] within rect2.
I hope one of you guys can help me.
To draw text in a rectangle there's a few things you need. The first is a pygame Font object. This is basically just a configured font. You can pass it a full path to a True Type Font (possibly others), or use a system font.
number_font = pygame.font.SysFont( None, 16 ) # default font, size 16
Then to render a number, pass it as text to the font's render() method, giving it a foreground and background colour. The second parameter is whether you want the font nice and smooth. Generally I always leave this True.
number_font = pygame.font.SysFont( None, 16 ) # Default font, Size 16
number_image = number_font.render( "8", True, BLACK, WHITE ) # Number 8
So that creates a number_image - a pyagme.Surface containing the "rendered" number.
Now that has to be centred in each cell. We can do this by working out the size difference between the surrounding rectangle, and the size of the number-image. Splitting this in half should give us a centre position. I just guessed at a font-size of 16, it might be too big for your grid (or way too small).
# Creating the font object needs to be only done once, so don't put it inside a loop
number_font = pygame.font.SysFont( None, 16 ) # default font, size 16
...
for row in grid:
for col in grid:
rect = pygame.Rect(x,y,w,w)
pygame.draw.rect(win,BLACK,rect)
rect2 = pygame.Rect(x+2, y+2, w-1, w-1)
pygame.draw.rect(win,WHITE,rect2)
# make the number from grid[row][col] into an image
number_text = str( grid[row][col] )
number_image = number_font.render( number_text, True, BLACK, WHITE )
# centre the image in the cell by calculating the margin-distance
margin_x = ( w-1 - number_image.width ) // 2
margin_y = ( w-1 - number_image.height ) // 2
# Draw the number image
win.blit( number_image, ( x+2 + margin_x, y+2 + margin_y ) )
I dont know how soduku works but this is how you render text in pygame. Create a font first.
fontName = pygame.font.get_default_font()
size = 10 # This means the text will be 10 pixels in height.
# The width will be scaled automatically.
font = pygame.font.Font(fontName, size)
Then create a text surface from the font.
text = number
antislias = True
color = (0, 0, 0)
surface = font.render(f"{text}", antialias, color)
Note that text argument always has to be a string, so in your case you have to use an fstring because you are rendering number. This surface is like any other surface in pygame, so you can simply render it using win.blit(surface, (row, col)).

I am trying to create a rectangle with width = 100 and length = 50 using graphic.py library. I am a bit confused on how to specify the length

I am trying to create a rectangle with width = 100 and length = 50 using graphics.py library. I am a bit confused on how to specify the length. Here's what I have so far:
main ():
win = GraphWin("window", 300, 300)
rec = Rectangle (Point (250,250), Point(200, 200))
rec.setWidth (50)
rec.draw(win)
In short, read the documentation. SetWidth changes the line thickness, not the rectangle size.
The rectangle dimensions are entirely determined by the two opposing corners you specify when you instantiate the object. I'll change your values to illustrate:
rec = Rectangle (Point(300, 200), Point(100, 50))
This defines a rectangle with opposite corners at the given points.
The width (x direction) is abs(300-100) = 200
The height (y direction) is abs(200-50) = 150
Does that clear up your confusion?

Pygame: How do I blit and rotate an image to connect two points on the screen?

Here is a test program. I started with two random dots and the line connecting them. Now, I want to take a given image (with x,y dimensions of 79 x 1080) and blit it on top of the guide line. I understand that arctan will give me the angle between the points on a cartesian grid, but because y is backwards the screen (x,y), I have to invert some values. I'm confused about the negating step.
If you run this repeatedly, you'll see the image is always parallel to the line, and sometimes on top, but not consistently.
import math
import pygame
import random
pygame.init()
screen = pygame.display.set_mode((600,600))
#target = (126, 270)
#start = (234, 54)
target = (random.randrange(600), random.randrange(600))
start = (random.randrange(600), random.randrange(600))
BLACK = (0,0,0)
BLUE = (0,0,128)
GREEN = (0,128,0)
pygame.draw.circle(screen, GREEN, start, 15)
pygame.draw.circle(screen, BLUE, target, 15)
pygame.draw.line(screen, BLUE, start, target, 5)
route = pygame.Surface((79,1080))
route.set_colorkey(BLACK)
BMP = pygame.image.load('art/trade_route00.png').convert()
(bx, by, bwidth, bheight) = route.get_rect()
route.blit(BMP, (0,0), area=route.get_rect())
# get distance within screen in pixels
dist = math.sqrt((start[0] - target[0])**2 + (start[1] - target[1])**2)
# scale to fit: use distance between points, and make width extra skinny.
route = pygame.transform.scale(route, (int(bwidth * dist/bwidth * 0.05), int( bheight * dist/bheight)))
# and rotate... (invert, as negative is for clockwise)
angle = math.degrees(math.atan2(-1*(target[1]-start[1]), target[0]-start[0]))
route = pygame.transform.rotate(route, angle + 90 )
position = route.get_rect()
HERE = (abs(target[0] - position[2]), target[1]) # - position[3]/2)
print(HERE)
screen.blit(route, HERE)
pygame.display.update()
print(start, target, dist, angle, position)
The main problem
The error is not due to the inverse y coordinates (0 at top, max at bottom) while rotating as you seems to think. That part is correct. The error is here:
HERE = (abs(target[0] - position[2]), target[1]) # - position[3]/2)
HERE must be the coordinates of the top-left corner of the rectangle inscribing your green and blue dots connected by the blue line. At those coordinates, you need to place the Surface route after rescaling.
You can get this vertex by doing:
HERE = (min(start[0], target[0]), min(start[1], target[1]))
This should solve the problem, and your colored dots should lay on the blue line.
A side note
Another thing you might wish to fix is the scaling parameter of route:
route = pygame.transform.scale(route, (int(bwidth * dist/bwidth * 0.05), int( bheight * dist/bheight)))
If my guess is correct and you want to preserve the original widht/height ratio in the rescaled route (since your original image is not a square) this should be:
route = pygame.transform.scale(route, (int(dist* bwidth/bheight), int(dist)))
assuming that you want height (the greater size in the original) be scaled to dist. So you may not need the 0.05, or maybe you can use a different shrinking parameter (probably 0.05 will shrink it too much).

Efficiently masking a surface in pygame

I need to draw a circle filled with random gray colors and a black outline using pygame. This is what it should look like:
The radius increases by expansion_speed * dt every frame and the surface is updated 60 times per second, so however this is achieved (if even possible) needs to be fast. I tried masking an stored texture but that was too slow. My next idea was to read the pixels from this stored texture and only replace the difference between the last and current surfaces. I tried this too but was unable to translate the idea to code.
So how can this be done?
See my update to your previous related question. It has some info about performance. You could try to enable hardware acceleration in fullscreen mode, but I never personally tried it, so can't give good advice how to do it properly. Just use two differnt colorkeys for extracting circle from noise and putting the whole surface to the display. Note that if your Noise surface has pixels same as colorkey color then they also become transparent.
This example I think is what you are trying to get, move the circle with mouse and hold CTRL key to change radius.
Images:
import os, pygame
pygame.init()
w = 800
h = 600
DISP = pygame.display.set_mode((w, h), 0, 24)
clock = pygame.time.Clock( )
tile1 = pygame.image.load("2xtile1.png").convert()
tile2 = pygame.image.load("2xtile2.png").convert()
tw = tile1.get_width()
th = tile1.get_height()
Noise = pygame.Surface ((w,h))
Background = pygame.Surface ((w,h))
for py in range(0, h/th + 2) :
for px in range(0, w/tw + 2):
Noise.blit(tile1, (px*(tw-1), py*(th-1) ) )
Background.blit(tile2, (px*(tw-1), py*(th-1) ) )
color_key1 = (0, 0, 0)
color_key2 = (1, 1, 1)
Circle = pygame.Surface ((w,h))
Circle.set_colorkey(color_key1)
Mask = pygame.Surface ((w,h))
Mask.fill(color_key1)
Mask.set_colorkey(color_key2)
strokecolor = (10, 10, 10)
DISP.blit(Background,(0,0))
def put_circle(x0, y0, r, stroke):
pygame.draw.circle(Mask, strokecolor, (x0,y0), r, 0)
pygame.draw.circle(Mask, color_key2, (x0,y0), r - stroke, 0)
Circle.blit(Noise,(0,0))
Circle.blit(Mask,(0,0))
dirtyrect = (x0 - r, y0 - r, 2*r, 2*r)
Mask.fill(color_key1, dirtyrect)
DISP.blit(Circle, (0,0))
X = w/2
Y = h/2
R = 100
stroke = 2
FPS = 25
MainLoop = True
pygame.mouse.set_visible(False)
pygame.event.set_grab(True)
while MainLoop :
clock.tick(FPS)
pygame.event.pump()
Keys = pygame.key.get_pressed()
MR = pygame.mouse.get_rel() # get mouse shift
if Keys [pygame.K_ESCAPE] :
MainLoop = False
if Keys [pygame.K_LCTRL] :
R = R + MR[0]
if R <= stroke : R = stroke
else :
X = X + MR[0]
Y = Y + MR[1]
DISP.blit(Background,(0,0))
put_circle(X, Y, R, stroke)
pygame.display.flip( )
pygame.mouse.set_visible(True)
pygame.event.set_grab(False)
pygame.quit( )
Many years ago we had a font rendering challenge with the Pygame project.
Someone created an animated static text for the contest but it was far too slow.
We put our heads together and made a much quicker version. Step one was to create a smallish image with random noise. Something like 64x64. You may need a bigger image if your final image is large enough to notice the tiling.
Every frame you blit the tiled noise using a random offset. Then you take an image with the mask, in your case an inverted circle, and draw that on top. That should give you a final image containing just the unmasked noise.
The results were good. In our case it was not noticeable that the noise was just jittering around. That may be because the text did not have a large unobstrcted area. I'd be concerned your large circle would make the trick appear obvious. i guess if you really had a large enough tiled image it would still work.
The results and final source code are still online at the Pygame website,
http://www.pygame.org/pcr/static_text/index.php

Categories

Resources