Multicolored text with PIL - python

I'm creating a web-app that serves a dynamic image, with text.
Each string drawn may be in multiple colors.
So far I've created a parse method, and a render method. The parse method just takes the string, and parses colors from it, they are in format like this: "§aThis is green§rthis is white" (Yeah, it is Minecraft).
So this is how my font module looks like:
# Imports from pillow
from PIL import Image, ImageDraw, ImageFont
# Load the fonts
font_regular = ImageFont.truetype("static/font/regular.ttf", 24)
font_bold = ImageFont.truetype("static/font/bold.ttf", 24)
font_italics = ImageFont.truetype("static/font/italics.ttf", 24)
font_bold_italics = ImageFont.truetype("static/font/bold-italics.ttf", 24)
max_height = 21 # 9, from FONT_HEIGHT in FontRederer in MC source, multiplied by
# 3, because each virtual pixel in the font is 3 real pixels
# This number is also returned by:
# font_regular.getsize("ABCDEFGHIJKLMNOPQRSTUVWXYZ")[1]
# Create the color codes
colorCodes = [0] * 32 # Empty array, 32 slots
# This is ported from the original MC java source:
for i in range(0, 32):
j = int((i >> 3 & 1) * 85)
k = int((i >> 2 & 1) * 170 + j)
l = int((i >> 1 & 1) * 170 + j)
i1 = int((i >> 0 & 1) * 170 + j)
if i == 6:
k += 85
if i >= 16:
k = int(k/4)
l = int(l/4)
i1 = int(i1/4)
colorCodes[i] = (k & 255) << 16 | (l & 255) << 8 | i1 & 255
def _get_colour(c):
''' Get the RGB-tuple for the color
Color can be a string, one of the chars in: 0123456789abcdef
or an int in range 0 to 15, including 15
'''
if type(c) == str:
if c == 'r':
c = int('f', 16)
else:
c = int(c, 16)
c = colorCodes[c]
return ( c >> 16 , c >> 8 & 255 , c & 255 )
def _get_shadow(c):
''' Get the shadow RGB-tuple for the color
Color can be a string, one of the chars in: 0123456789abcdefr
or an int in range 0 to 15, including 15
'''
if type(c) == str:
if c == 'r':
c = int('f', 16)
else:
c = int(c, 16)
return _get_colour(c+16)
def _get_font(bold, italics):
font = font_regular
if bold and italics:
font = font_bold_italics
elif bold:
font = font_bold
elif italics:
font = font_italics
return font
def parse(message):
''' Parse the message in a format readable by render
this will return a touple like this:
[((int,int),str,str)]
so if you where to send it directly to the rederer you have to do this:
render(pos, parse(message), drawer)
'''
result = []
lastColour = 'r'
total_width = 0
bold = False
italics = False
for i in range(0,len(message)):
if message[i] == '§':
continue
elif message[i-1] == '§':
if message[i] in "01234567890abcdef":
lastColour = message[i]
if message[i] == 'l':
bold = True
if message[i] == 'o':
italics = True
if message[i] == 'r':
bold = False
italics = False
lastColour = message[i]
continue
width, height = _get_font(bold, italics).getsize(message[i])
total_width += width
result.append(((width, height), lastColour, bold, italics, message[i]))
return result
def get_width(message):
''' Calculate the width of the message
The message has to be in the format returned by the parse function
'''
return sum([i[0][0] for i in message])
def render(pos, message, drawer):
''' Render the message to the drawer
The message has to be in the format returned by the parse function
'''
x = pos[0]
y = pos[1]
for i in message:
(width, height), colour, bold, italics, char = i
font = _get_font(bold, italics)
drawer.text((x+3, y+3+(max_height-height)), char, fill=_get_shadow(colour), font=font)
drawer.text((x, y+(max_height-height)), char, fill=_get_colour(colour), font=font)
x += width
And it does work, but characters who are supposed to go below the ground line of the font, like g, y and q, are rendered on the ground line, so it looks strange, Here's an example:
Any ideas on how I can make them display corectly? Or do I have to make my own offset table, where I manually put them?

Given that you can't get the offsets from PIL, you could do this by slicing up images since PIL combines multiple characters appropriately. Here I have two approaches, but I think the first presented is better, though both are just a few lines. The first approach gives this result (it's also a zoom in on a small font which is why it's pixelated):
To explain the idea here, say I want the letter 'j', and instead of just making an image of just 'j', I make an image of ' o j' since that will keep the 'j' aligned correctly. Then I crop of the part I don't want and just keep the 'j' (by using textsize on both ' o ' and ' o j').
import Image, ImageDraw
from random import randint
make_color = lambda : (randint(50, 255), randint(50, 255), randint(50,255))
image = Image.new("RGB", (1200,20), (0,0,0)) # scrap image
draw = ImageDraw.Draw(image)
image2 = Image.new("RGB", (1200, 20), (0,0,0)) # final image
fill = " o "
x = 0
w_fill, y = draw.textsize(fill)
x_draw, x_paste = 0, 0
for c in "The quick brown fox jumps over the lazy dog.":
w_full = draw.textsize(fill+c)[0]
w = w_full - w_fill # the width of the character on its own
draw.text((x_draw,0), fill+c, make_color())
iletter = image.crop((x_draw+w_fill, 0, x_draw+w_full, y))
image2.paste(iletter, (x_paste, 0))
x_draw += w_full
x_paste += w
image2.show()
Btw, I use ' o ', rather than just 'o' since adjacent letters seem to slightly corrupt each other.
The second way is to make an image of the whole alphabet, slice it up, and then repaste this together. It's easier than it sounds. Here's an example, and both building the dictionary and concatenating into the images is only a few lines of code each:
import Image, ImageDraw
import string
A = " ".join(string.printable)
image = Image.new("RGB", (1200,20), (0,0,0))
draw = ImageDraw.Draw(image)
# make a dictionary of character images
xcuts = [draw.textsize(A[:i+1])[0] for i in range(len(A))]
xcuts = [0]+xcuts
ycut = draw.textsize(A)[1]
draw.text((0,0), A, (255,255,255))
# ichars is like {"a":(width,image), "b":(width,image), ...}
ichars = dict([(A[i], (xcuts[i+1]-xcuts[i]+1, image.crop((xcuts[i]-1, 0, xcuts[i+1], ycut)))) for i in range(len(xcuts)-1)])
# Test it...
image2 = Image.new("RGB", (400,20), (0,0,0))
x = 0
for c in "This is just a nifty text string":
w, char_image = ichars[c]
image2.paste(char_image, (x, 0))
x += w
Here's a (zoomed in) image of the resulting string:
Here's an image of the whole alphabet:
One trick here was that I had to put a space in between each character in my original alphabet image or I got the neighboring characters affecting each other.
I guess if you needed to do this a for a finite range of fonts and characters, it would be a good idea precalculate a the alphabet image dictionary.
Or, for a different approach, using a tool like numpy you could easily determine the yoffset of each character in the ichar dictionary above (eg, take the max along each horizontal row, and then find the max and min on the nonzero indices).

I simply solved this problem like this:
image = Image.new("RGB", (1000,1000), (255,255,255)) # 1000*1000 white empty image
# image = Image.fromarray(frame) # or u can get image from cv2 frame
draw = ImageDraw.Draw(image)
fontpath = "/etc/fonts/bla/bla/comic-sans.ttf"
font = ImageFont.truetype(fontpath, 35) # U can use default fonts
x = 20 # image draw start pixel x position
y = 100 # image draw start pixel y position
xDescPxl = draw.textsize("Descrition", font= font)[0]
draw.text((x, y), "Descrition" , font = font, fill = (0, 255, 0, 0)) # Green Color
draw.text((x + xDescPxl, y), ": u can do bla bla", font = font, fill = (0, 0, 0, 0)) # Black Color
Result:
Description: u can do bla bla
(20px space)---(Green Part)--(Black Part)

Related

Can't paste image in square with a loop

I can't achieve to paste images in a square form (if I choose 9 for n_album, I should have a 3x3 collage). It only works for 1x1, if it's more it will paste the same image where another image is supposed to be.
Here is my code:
def make_montage(n_album, path):
x_offset = width #Constant resized image width
y_offset = height #Constant resized image height
c = []
x = 0
img = Image.new('RGB', (n_album*height + y_offset*2, n_album*width + x_offset*2), color = (0, 0, 0))
for file_name in os.listdir(path):
print(f"Processing {file_name}")
c.append(file_name)
print(f"root of n_album = {int(math.sqrt(n_album))}")
#Loop in square
for i in range(int(math.sqrt(n_album))):
for j in range(int(math.sqrt(n_album))):
try:
cover = Image.open(os.path.join(path, c[i + j]))
print(f"Pasting {str(c[i + j])}")
img.paste(cover, (int(i * height + y_offset), int(j * width + x_offset)))
except:
print("Je code mal mdr")
img.save(f'{path}\\{n_album}x{n_album}_musical.png')
#Clean
for file_name in os.listdir(path):
if file_name != f'{n_album}x{n_album}_musical.png':
print(f"Deleting {file_name}")
os.remove(os.path.join(path, file_name))
And here's a result for a 2x2 with images of (the order it was supposed to be pasted): Link, Mario, Princess Zelda, Peach.
I see several issues in your code:
In your method declaration, you should also pass the (desired) width and height of each image. (As is, your method assumes, that width and height are properly set outside.) This has also the advantage, that you can resize your images on-the-fly within your loop.
You don't pay attention, when to use n_album and when int(math.sqrt(n_album)). (See your 2x2 montage: You obviously initialized a 4x4 montage.) For the latter, create a variable like n_per_axis, so you don't have this math.sqrt term all the time.
In your nested loop, you have i = 1, 2, 3, and j = 1, 2, 3. Using c[i + j] then isn't the correct way to access the proper images from c. (In your 2x2 montage, you get (0 + 1) = 1 and (1 + 0) = 1, so Mario two times.) Set up a (global) image counter (let's say k), and increment it with each entering of the inner loop.
Don't post code including deleting of files, if that's not the main point of your question.
Here's your code with some modifications:
def make_montage(n_album, path, width, height): # <-- Width, height!?
x_offset = width
y_offset = height
c = []
n_per_axis = int(math.sqrt(n_album))
img = Image.new('RGB',
(n_per_axis*height + y_offset*2, # <-- n per axis!?
n_per_axis*width + x_offset*2), # <-- n per axis!?
color=(0, 0, 0))
for file_name in os.listdir(path):
print(f"Processing {file_name}")
c.append(file_name)
print(f"root of n_album = {n_per_axis}")
# Loop in square
k = -1 # <-- Image counter
for i in range(n_per_axis):
for j in range(n_per_axis):
k += 1
try:
cover = Image.open(os.path.join(path, c[k]))\
.resize((width, height)) # <-- Might be omitted here
print(f"Pasting {str(c[k])}")
img.paste(cover,
(int(i * height + y_offset),
int(j * width + x_offset)))
except:
print("Je code mal mdr")
img.save(f'{path}\\{n_per_axis}x{n_per_axis}_musical.png') # <-- n per axis!?
for file_name in os.listdir(path):
if file_name != f'{n_per_axis}x{n_per_axis}_musical.png':
print(f"Deleting {file_name}")
#os.remove(os.path.join(path, file_name))
Using
make_montage(9, 'path_with_nine_images/', 100, 100)
I get the following output:
Hope that helps!

Python script that converts webcam image to ASCII in cmd that won't print Unicode

I found this python script that converts webcam input into ASCII into the command line and made a few changes to suit my needs. I would like to add a Unicode character to the output, but it currently only prints out a box instead of the proper Unicode whenever I actually run it. What do I need to change to make sure that it prints out the actual Unicode character?
import cv2
from time import sleep, time
import sys
import curses
from curses import wrapper
import locale
locale.setlocale(locale.LC_ALL, '')
#stdscr.addstr(0, 0, mystring.encode('UTF-8'))
x = 160
sx = 4
chars = ['\u23FA', ".", ".", " ", " ", " ", " ", " ", " ", " "]
size = x*sx, int(x*sx*.75)
operating = False
ongoing = True
#font.set_bold(True)
fpsa = 0
br = (255, 255, 255)
fr = (0, 0, 0)
lenc = len(chars)
visload = False
mt = False
aa = False
def toAscii(pic, scr):
global operating
m = 0
for y in pic:
tm = max(y)
if tm > m:
m = tm
fx = 0
fy = 0
h,w = scr.getmaxyx()
#for y in pic:
#for x in y:
for _y in range(h-1):
for _x in range(w-1):
y = pic[int(_y/float(h) * len(pic))]
x = y[int(_x/float(w) * len(y))]
scr.addstr(_y, _x, chars[int(x/m*(lenc-1))].encode('UTF-8'), curses.color_pair(1))
fx += 1
fy += 1
fx = 0
operating = False
cap = cv2.VideoCapture(0)
def main(scr):
global operating
while True:
ret, frame = cap.read()
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
colored = cv2.resize(cv2.resize(cv2.cvtColor(frame, 0), (x, int(x*0.75))), (640, 480))
colored = cv2.cvtColor(frame, 0)
gray = cv2.resize(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), (x, int(x*.75)))
if not operating:
operating = True
if mt:
_thread.start_new_thread(toAscii, (gray, scr))
else:
toAscii(gray, scr)
scr.refresh()
#gray = cv2.resize(gray, (640, 480), interpolation = cv2.INTER_NEAREST)
#cv2.imshow('frame',cv2.resize(gray, (640, 480)))
def _main(scr):
try:
main(scr)
except KeyboardInterrupt:
pass
wrapper(_main)
cap.release()
cv2.destroyAllWindows()
It depends on the terminal emulator that you are using. The reason why the emoji character appears as a box in your terminal is that your terminal emulator doesn't support Unicode character.
I just tested your code on iTerm2, which support Unicode, and it works with no problem.

How to wrap text in pygame using pygame.font.Font()?

I am making a would you rather game, and I would like to not have character restrictions for the W.Y.R. questions. I have seen many examples here on Stack Overflow and other websites, but they use other modules and methods I don't understand how to use or want to use. So I would rather use
button_text_font = pygame.font.Font(font_location, 20)
red_button_text = button_text_font.render(red_text, True, bg_color)
blue_button_text = button_text_font.render(blue_text, True, bg_color)
I would like to know how to use this method and, for example, somehow input how far the text can go until it wraps to the next line.
Thanks
P.S. If you could, please also include centering text, etc.
This is adapted from some very old code I wrote:
def renderTextCenteredAt(text, font, colour, x, y, screen, allowed_width):
# first, split the text into words
words = text.split()
# now, construct lines out of these words
lines = []
while len(words) > 0:
# get as many words as will fit within allowed_width
line_words = []
while len(words) > 0:
line_words.append(words.pop(0))
fw, fh = font.size(' '.join(line_words + words[:1]))
if fw > allowed_width:
break
# add a line consisting of those words
line = ' '.join(line_words)
lines.append(line)
# now we've split our text into lines that fit into the width, actually
# render them
# we'll render each line below the last, so we need to keep track of
# the culmative height of the lines we've rendered so far
y_offset = 0
for line in lines:
fw, fh = font.size(line)
# (tx, ty) is the top-left of the font surface
tx = x - fw / 2
ty = y + y_offset
font_surface = font.render(line, True, colour)
screen.blit(font_surface, (tx, ty))
y_offset += fh
The basic algorithm is to split the text into words and iteratively build up lines word by word checking the resulting width each time and splitting to a new line when you would exceed the width.
As you can query how wide the rendered text will be, you can figure out where to render it to centre it.
This is messy and there is far more you can do but if you want a specific length of text for say a paragraph...
font = pygame.font.SysFont("Times New Roman, Arial", 20, bold=True)
your_text = "blah blah blah"
txtX, txtY = 125, 500
wraplen = 50
count = 0
my_wrap = textwrap.TextWrapper(width=wraplen)
wrap_list = my_wrap.wrap(text=your_text)
# Draw one line at a time further down the screen
for i in wrap_list:
txtY = txtY + 35
Mtxt = font.render(f"{i}", True, (255, 255, 255))
WIN.blit(Mtxt, (txtX, txtY))
count += 1
# Update All Window and contents
pygame.display.update()
Using the implementation in Pygame Zero, text can be wrapped with the following function.
# Adapted from https://github.com/lordmauve/pgzero/blob/master/pgzero/ptext.py#L81-L143
def wrap_text(text, font, max_width):
texts = text.replace("\t", " ").split("\n")
lines = []
for text in texts:
text = text.rstrip(" ")
if not text:
lines.append("")
continue
# Preserve leading spaces in all cases.
a = len(text) - len(text.lstrip(" "))
# At any time, a is the rightmost known index you can legally split a line. I.e. it's legal
# to add text[:a] to lines, and line is what will be added to lines if
# text is split at a.
a = text.index(" ", a) if " " in text else len(text)
line = text[:a]
while a + 1 < len(text):
# b is the next legal place to break the line, with `bline`` the
# corresponding line to add.
if " " not in text[a + 1:]:
b = len(text)
bline = text
else:
# Lines may be split at any space character that immediately follows a non-space
# character.
b = text.index(" ", a + 1)
while text[b - 1] == " ":
if " " in text[b + 1:]:
b = text.index(" ", b + 1)
else:
b = len(text)
break
bline = text[:b]
bline = text[:b]
if font.size(bline)[0] <= max_width:
a, line = b, bline
else:
lines.append(line)
text = text[a:].lstrip(" ")
a = text.index(" ", 1) if " " in text[1:] else len(text)
line = text[:a]
if text:
lines.append(line)
return lines
Bear in mind that wrapping text requires multiple lines that must be rendered separately. Here's an example of how you could render each line.
def create_text(text, color, pos, size, max_width=None, line_spacing=1):
font = pygame.font.SysFont("monospace", size)
if max_width is not None:
lines = wrap_text(text, font, max_width)
else:
lines = text.replace("\t", " ").split("\n")
line_ys = (
np.arange(len(lines)) - len(lines) / 2 + 0.5
) * 1.25 * font.get_linesize() + pos[1]
# Create the surface and rect that make up each line
text_objects = []
for line, y_pos in zip(lines, line_ys):
text_surface = font.render(line, True, color)
text_rect = text_surface.get_rect(center=(pos[0], y_pos))
text_objects.append((text_surface, text_rect))
return text_objects
# Example case
lines = create_text(
text="Some long text that needs to be wrapped",
color=(255, 255, 255), # White
pos=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2), # Center of the screen
size=16,
max_width=SCREEN_WIDTH,
)
# Render each line
for text_object in lines:
screen.blit(*text_object)

Captcha Break or Text reader from Image: OCR-Python

I have a typical captcha image which contain only digits.
Ex.
i want to extract 78614 from this image.
I tried few library & code using OCR-Python. But its returning 0.
Sample Code-1
from captcha_solver import CaptchaSolver
solver = CaptchaSolver('browser')
with open('captcha.png', 'rb') as inp:
raw_data = inp.read()
print(solver.solve_captcha(raw_data))
Sample Code-2
from PIL import Image
def p(img, letter):
A = img.load()
B = letter.load()
mx = 1000000
max_x = 0
x = 0
for x in range(img.size[0] - letter.size[0]):
_sum = 0
for i in range(letter.size[0]):
for j in range(letter.size[1]):
_sum = _sum + abs(A[x+i, j][0] - B[i, j][0])
if _sum < mx :
mx = _sum
max_x = x
return mx, max_x
def ocr(im, threshold=200, alphabet="0123456789abcdef"):
img = Image.open(im)
img = img.convert("RGB")
box = (8, 8, 58, 18)
img = img.crop(box)
pixdata = img.load()
letters = Image.open(im)
ledata = letters.load()
# Clean the background noise, if color != white, then set to black.
for y in range(img.size[1]):
for x in range(img.size[0]):
if (pixdata[x, y][0] > threshold) \
and (pixdata[x, y][1] > threshold) \
and (pixdata[x, y][2] > threshold):
pixdata[x, y] = (255, 255, 255, 255)
else:
pixdata[x, y] = (0, 0, 0, 255)
counter = 0;
old_x = -1;
letterlist = []
for x in range(letters.size[0]):
black = True
for y in range(letters.size[1]):
if ledata[x, y][0] <> 0 :
black = False
break
if black :
if True :
box = (old_x + 1, 0, x, 10)
letter = letters.crop(box)
t = p(img, letter);
print counter, x, t
letterlist.append((t[0], alphabet[counter], t[1]))
old_x = x
counter += 1
box = (old_x + 1, 0, 140, 10)
letter = letters.crop(box)
t = p(img, letter)
letterlist.append((t[0], alphabet[counter], t[1]))
t = sorted(letterlist)
t = t[0:5] # 5-letter captcha
final = sorted(t, key=lambda e: e[2])
answer = ""
for l in final:
answer = answer + l[1]
return answer
print(ocr('captcha.png'))
Has anyone had the opportunity to get/extract text from such typical captcha?
You can use machine learning (neural networks) models to solve captchas and it will almost always outperform free OCR or any other method.
Here is a good starting point: https://medium.com/#ageitgey/how-to-break-a-captcha-system-in-15-minutes-with-machine-learning-dbebb035a710

Counting black pixels using Python

I am in my first programming class so very new. I'm trying to count the black pixels in a picture and I'm stuck. This is what I have so far:
def main():
#create a 10x10 black picture
newPict = makeEmptyPicture(10, 10)
show(newPict)
setAllPixelsToAColor(newPict,black)
#Display the picture
show(newPict)
#Initialize variabl countBlack to 0
countZero = 0
for p in getPixels(newPict):
r = getRed(p)
b = getBlue(p)
g = getGreen(p)
if (r,g,b) == (0,0,0):
countZero = countZero + 100
return countZero
How it was pointed by kedar and deets , your return is INSIDE the for, so, in the first pixel it will return the value of countZero, instead of looping over all the image, just fixing the indention should be fine :
for p in getPixels(newPict):
r = getRed(p)
b = getBlue(p)
g = getGreen(p)
if (r,g,b) == (0,0,0):
countZero = countZero + 1
return countZero

Categories

Resources