How can I create a bounding box and fit text into it? - python-3.x

I'm trying to fit text into a bounding box.
essentially the inputs for the function would be:
text -> string (list of words that are separated by ',')
width -> int (the set width of the box)
Example 1:
** If all of the text can fit inside the bounding box and in one line then keep it that way
Inputs:
"test, test2, test3", 100
It should output something like this:
Example 2:
** If the text cant fit into the box in one line it would make the box bigger (in height) and just keep writing a line down
Inputs:
"test, test2, test3, test4, test5", 100
It should output something like this:
Example 3:
** If the longest string in the list of strings that are separated by ',' cant fit into the box in one line it would make the text smaller
Inputs:
"wikipedia_is_long12345, test, test2, test3", 100
It should output something like this:
Another Example:
I would like some hints/help,
Thanks.

I've tried making it again and I did it!
Thats the code:
def fit_text(text, text_size, text_color, max_horizontal_chars, box_outline_color, box_background_color,
box_outline_width, font_file, word_bank_outline, high_res):
"""
:param text: The text that needs to be fir inside the bounding box (words separated by ,)
:type text: str
:param text_size: The size of the text
:type text_size: int
:param text_color: The color of the text
:type text_color: Tuple(int, int, int) | str
:param max_horizontal_chars: The number of the max horizontal chars allowed in one line
:type max_horizontal_chars: int
:param box_outline_color: The color of the outline of the bounding box
:type box_outline_color: Tuple(int, int, int) | str
:param box_background_color: The color of the background of the bounding box
:type box_background_color: Tuple(int, int, int) | str
:param box_outline_width: The thickness of the outline of the bounding box
:type box_outline_width: int
:param font_file: The file of the font of the text
:type font_file: str
:param word_bank_outline: Whether there will be an outline or not
:type word_bank_outline: bool
:return: The image
:rtype: Image
"""
multiplier = 2
text_size *= multiplier //2
# Replaces every ',' to '-' because the wrapper library will associate '-' as a separator and sorts by length
text = text.split(',')
text.sort(key=len, reverse=True)
text = ','.join(text).upper()
text = text.replace(',', '-')
# Changes the text size to fit the box if the longest word cannot fit in one line
font = ImageFont.truetype(font_file, text_size)
longest_word = sorted(text.split('-'), key=len)[-1]
if longest_word[0] == ' ':
longest_word = longest_word[1:]
longest_word_size = font.getsize(longest_word)[0]
while longest_word_size >= 1100:
longest_word = sorted(text.split('-'), key=len)[-1]
if longest_word[0] == ' ':
longest_word = longest_word[1:]
longest_word_size = font.getsize(longest_word)[0]
text_size -= 1
font = ImageFont.truetype(font_file, text_size)
# Initializes the text wrapper
wrapper = textwrap.TextWrapper()
wrapper.max_lines = 3
wrapper.placeholder = '...'
wrapper.break_long_words = False
wrapper.width = max_horizontal_chars
# Wrap the text
text = wrapper.fill(text=text)
# Create a new image according to the size of the text
img = Image.new('RGBA', (font.getsize_multiline(text)[0], (font.getsize_multiline(text)[1]+20+(60 if high_res else 20)+box_outline_width+(text.count('\n')*10))), (0,0,0,0))
# Initializes the ImageDraw.Draw for the img so I can draw on it
draw = ImageDraw.Draw(im=img)
# If word_bank_outline is true it will draw a bunch or rectangles according to box_outline_width
if word_bank_outline:
w, h = img.size
for i in range(0, box_outline_width):
shape = [(0 + i, 0 + i), (w - i, h - i)]
draw.rectangle(shape, box_background_color, box_outline_color)
# Replaces every '-' back to ','
text = text.replace('-', ',').upper()
# Checks if the first char is ' ' if it is it will be cut out
if text[0] == ' ':
text = text[1:]
# Draws the text onto the bounding box
draw.multiline_text(xy=(10 + (20 if high_res else 0), 0), text=text, font=font, fill=text_color, spacing=20)
return img
Essentially the main thing that made it happen is the function ttffont.getsize which returns the size of a string in pixels and so I could know the exact size of the string and I could fit it more accurately and another thing is the library textwraper which divided the text into separate lines.
So first it will check if the longest word in the string can fit inside the bounding box if not it will loop and decrement the text size until it fits
then it will call the textwrapper.fill which will divide the text and then it will just draw the text on the image and draw the outline and then it will return the image.
The end result:

Related

how to load np.array from text file using np.genfromtxt

The file has the following structure:
h
w
P1,1_r P1,1_g P1,1_b
P1,2_r P1,2_g P1,2_b
...
P1,w_r P1,w_g P1,w_b
P2,1_r P2,1_g P2,1_b
...
Ph,w_r Ph,w_g Ph,w_b
The first line contains one integer, h, which is the height of the image. The second line contains one integer, w, which is the width of the image. The next h x w lines contain the pixel value as a list of three values corresponding to red, green, and blue color of the pixel. The list of pixels are arranged in the top-to-bottom then left-to-right order.
How can convert this to a np.array using np.genfromtxt?
If you want to read in the data from a .txt file as a numpy array, you need to first read the file data in, then use np.genfromtext on the resulting object:
file = open("filename.txt")
#separate the first two lines containing h and w
hw = file.readlines(2)
# get h and w, as integers from hw by indexing them by line numbers
h = int(hw[0])
w = int(hw[1])
This leaves the rest of file containing the np.array with the image data in. Use np.genfromtxt on this:
figure = np.genfromtxt(file)

Convert Excel column width between characters unit and pixels (points)

"One unit of column width is equal to the width of one character in the Normal style. For proportional fonts, the width of the character 0 (zero) is used."
So ColumnWidth in Excel is measured as a number of "0" characters which fits in a column. How can this value be converted into pixels and vice versa?
As already mentioned ColumnWidth value in Excel depends on default font of a Workbook which can be obtained via Workbook.Styles("Normal").Font. Also it depends on current screen DPI.
After carrying out some research for different fonts and sizes in Excel 2013 I've found out that we have 2 linear functions (Arial cannot be seen because it overlaps with Tahoma.):
As it can be seen in the picture the function for ColumnWidth < 1 is different from the major part of the line chart. It's calculated as a number of pixels in a column / number of pixels needed to fit one "0" character in a column.
Now let's see what a typical cell width consists of.
A - "0" character width in the Normal Style
B - left and right padding
C - 1px right margin
A can be calculated with GetTextExtentPoint32 Windows API function, but font size should be a little bit bigger. By experiment I chose +0.3pt which worked for me for different fonts with 8-48pt base size. B is (A + 1) / 4 rounded to integer using "round half up". Also screen DPI will be needed here (see Python 3 implementation below)
Here are equations for character-pixel conversion and their implementation in Python 3:
import win32print, win32gui
from math import floor
def get_screen_dpi():
dc = win32gui.GetDC(0)
LOGPIXELSX, LOGPIXELSY = 88, 90
dpi = [win32print.GetDeviceCaps(dc, i) for i in (LOGPIXELSX,
LOGPIXELSY)]
win32gui.ReleaseDC(0, dc)
return dpi
def get_text_metrics(fontname, fontsize):
"Measures '0' char size for the specified font name and size in pt"
dc = win32gui.GetDC(0)
font = win32gui.LOGFONT()
font.lfFaceName = fontname
font.lfHeight = -fontsize * dpi[1] / 72
hfont = win32gui.CreateFontIndirect(font)
win32gui.SelectObject(dc, hfont)
metrics = win32gui.GetTextExtentPoint32(dc, "0")
win32gui.ReleaseDC(0, dc)
return metrics
def ch_px(v, unit="ch"):
"""
Convert between Excel character width and pixel width.
`unit` - unit to convert from: 'ch' (default) or 'px'
"""
rd = lambda x: floor(x + 0.5) # round half up
# pad = left cell padding + right cell padding + cell border(1)
pad = rd((z + 1) / 4) * 2 + 1
z_p = z + pad # space (px) for "0" character with padding
if unit == "ch":
return v * z_p if v < 1 else v * z + pad
else:
return v / z_p if v < z_p else (v - pad) / z
font = "Calibri", 11
dpi = get_screen_dpi()
z = get_text_metrics(font[0], font[1] + 0.3)[0] # "0" char width in px
px = ch_px(30, "ch")
ch = ch_px(px, "px")
print("Characters:", ch, "Pixels:", px, "for", font)
2022 and still the same Problem... Found threads going back to 2010 having the issue...
To start of: Pixel != Points
Points are defined as 72points/inch: https://learn.microsoft.com/en-us/office/vba/language/glossary/vbe-glossary#point
Though that definition seems stupid, as a shape with a fixed width of 100points, would display the exact same size in inch on every monitor independent of monitor configuration, which is not the case.
Characters is a unit that is defined to the number of 0 characters of the default text format. A cell set to a width of 10 characters, can fit 10 "0" characters, when the cell content is formatted to the default format.
My case is that I need to place pictures into the document and place text into cells next to it. But pictures hover over the document and cells are hidden below it. Depending on the size of the Picture, more or less cells are hidden. Thus, I can't just say I place text 5 cells to the left of the picture. Autosizing a column to the contents of the cells of the column, does not account for the hovering picture.
A picture is bound to the cell that is below the top left corner of the picture. I need to set the size of that cell to the size of the picture to solve the issue.
A Picture is a Shape. A Shape returns its width as Points (Shape.Width).
A Range can be set to a cell like Worksheet.Range["A1"]. From a Range you can get the width in Characters (Range.ColumnWidth) or in Points (Range.Width). But you can only set the width of a Range in Characters (Range.ColumnWidth).
So we can retrieve the size of the Picture (Shape) in Points and need to convert them to Characters to set the cell to the correct width...
Some research showed that the Points size of a cell contains a constant for spacing (padding before and after the cell content) and probably the seperator lines between cells.
On my system:
A cell set to a width of 1 **Characters** = 9 **Points**
A cell set to a width of 2 **Characters** = 14.25 **Points**
A cell set to a width of 3 **Characters** = 19.5 **Points**
As I said, there is a constant within the Points. Thus going from 1 Characters, to 2 Characters, the difference is only the size of the letter.
SizeOfLetter = 14.25 Points - 9 Points = 5.25 Points
we can then subtract that SizeOfLetter from the Points for 1 Characters and get the Points constant.
PointsConstant = 9 Points - 5.25 Points = 3.75 Points
Verify:
Points size for a cell containing 3 "0" letters = 3SizeOfLetter + PointsConstant = 35.25 Points + 3.75 Points = 19.5 Points
As the values depend on your system, YOU CAN'T USE THOSE VALUES!
Best way is to use code to calculate it for your system:
C# code:
Excel.Application excelApp = new Excel.Application();
Excel.Workbook workbook1 = excelApp.Workbooks.Add();
Excel.Worksheet sheet1 = (Excel.Worksheet)workbook1.ActiveSheet;
// Evaluate the Points data for the document
double previousColumnWidth = (double)sheet1.Range["A1"].ColumnWidth;
sheet1.Range["A1"].ColumnWidth = 1; // Make the cell fit 1 character
double points1 = (double)sheet1.Range["A1"].Width;
sheet1.Range["A1"].ColumnWidth = 2; // Make the cell fit 2 characters
double points2 = (double)sheet1.Range["A1"].Width;
double SizeOfLetter = points2 - points1;
double PointsConstant = points1 - pointsPerCharater;
// Reset the column width
sheet1.Range["A1"].ColumnWidth = previousColumnWidth;
// Create a function for the conversion
Func<double, double> PointsToCharacters = (double points) => (points - PointsConstant ) / SizeOfLetter ;

How to access the values left, top individually in pywinauto.controls.hwndrapper.rectangle?

On pywinauto.rectangle() i get the position on screen of the window that I set_focus(), and on client_rect() I get the size of that window.
I want to grab the left and top of the window position on screen and the width and height of the window size.
I manage to get the width and height with a different function:
w=pywinauto.win32structures.RECT.width(client_rect)
h=pywinauto.win32structures.RECT.height(client_rect)
but I cant access the left and top.
app = controls.hwndwrapper.HwndWrapper(title)
rect = app.rectangle()
# print out
rect (L1293, T6, R1851, B1026)
rect type <class 'pywinauto.win32structures.RECT'>
app = controls.hwndwrapper.HwndWrapper(title)
client_rect = app.client_rect()
# print out
client_rect (L0, T0, R558, B1020)
client_rect type <class 'pywinauto.win32structures.RECT'>
# I cant access like this either:
values = []
app = controls.hwndwrapper.HwndWrapper(title)
rect = app.rect()
values.append(rect)
# print out
values [<RECT L1293, T6, R1851, B1026>]
values type <class 'list'>
# I try this too:
x = values[0]
# prints out:
x (L1293, T6, R1851, B1026)
x = values[1]
# prints out
x = values[1]
IndexError: list index out of range
There are just simple attributes of the client_rect object:
print(client_rect.left, client_rect.top, client_rect.right, client_rect.bottom)
Also there is additional method to get central point of the rect:
print(client_rect.mid_point())
Height and width are also methods:
print(client_rect.width())
print(client_rect.height())

Need Help in finding 2 seperate contours instead of a combined contour in MICR code

I an running OCR on bank cheques using pyimagesearch tutorial to detect micr code. The code used in the tutorial detects group contours & character contours from a reference image containing symbols.
In the tutorial when finding the contours for symbol below
the code uses an built-in python iterator to iterate over the contours (here 3 seperate contours) and combined to give a character for recognition purposes.
But in the cheque dataset that I use, I have the symbol with low resolution
The actual bottom of the cheque is :
which causes the iterator to consider the contour-2 & contour-3 as a single contour. Due to this the iterator iterates over the character following the above symbol (here '0') and prepares a incorrect template to match with the reference symbols. You can see the code below for better understanding.
I know here noise in the image is a factor, but is it possible to reduce the noise & also find the exact contour to detect the symbol?
I tried using noise reduction techniques like cv2.fastNlMeansDenoising & cv2.GaussianBlur before cv2.findContours step the contours 2&3 are detected as single contour instead of 2 seperate contours.
Also I tried altering the `cv2.findContours' parameters
Below is the working code where the characters are iterated for better understanding of python builtin iterator:
def extract_digits_and_symbols(image, charCnts, minW=5, minH=10):
# grab the internal Python iterator for the list of character
# contours, then initialize the character ROI and location
# lists, respectively
charIter = charCnts.__iter__()
rois = []
locs = []
# keep looping over the character contours until we reach the end
# of the list
while True:
try:
# grab the next character contour from the list, compute
# its bounding box, and initialize the ROI
c = next(charIter)
(cX, cY, cW, cH) = cv2.boundingRect(c)
roi = None
# check to see if the width and height are sufficiently
# large, indicating that we have found a digit
if cW >= minW and cH >= minH:
# extract the ROI
roi = image[cY:cY + cH, cX:cX + cW]
rois.append(roi)
cv2.imshow('roi',roi)
cv2.waitKey(0)
locs.append((cX, cY, cX + cW, cY + cH))
# otherwise, we are examining one of the special symbols
else:
# MICR symbols include three separate parts, so we
# need to grab the next two parts from our iterator,
# followed by initializing the bounding box
# coordinates for the symbol
parts = [c, next(charIter), next(charIter)]
(sXA, sYA, sXB, sYB) = (np.inf, np.inf, -np.inf,
-np.inf)
# loop over the parts
for p in parts:
# compute the bounding box for the part, then
# update our bookkeeping variables
# c = next(charIter)
# (cX, cY, cW, cH) = cv2.boundingRect(c)
# roi = image[cY:cY+cH, cX:cX+cW]
# cv2.imshow('symbol', roi)
# cv2.waitKey(0)
# roi = None
(pX, pY, pW, pH) = cv2.boundingRect(p)
sXA = min(sXA, pX)
sYA = min(sYA, pY)
sXB = max(sXB, pX + pW)
sYB = max(sYB, pY + pH)
# extract the ROI
roi = image[sYA:sYB, sXA:sXB]
cv2.imshow('symbol', roi)
cv2.waitKey(0)
rois.append(roi)
locs.append((sXA, sYA, sXB, sYB))
# we have reached the end of the iterator; gracefully break
# from the loop
except StopIteration:
break
# return a tuple of the ROIs and locations
return (rois, locs)
edit: contour 2 & 3 instead of contours 1 & 2
Try to find the right threshold value, instead of using cv2.THRESH_OTSU. It seems should be possible to find a suitable threshold from the provided example. If you can't find the threshold value that works for all images, you can try morphological closing on the threshold result with structuring element with 1-pixel width.
Edit (steps):
For threshold, you need to find appropriate value by hand, in your image threhsold value 100 seems to work:
i = cv.imread('image.png')
g = cv.cvtColor(i, cv.COLOR_BGR2GRAY)
_, tt = cv.threshold(g, 100, 255, cv.THRESH_BINARY_INV)
as for closing variant:
_, t = cv.threshold(g, 0,255,cv.THRESH_BINARY_INV | cv.THRESH_OTSU)
kernel = np.ones((12,1), np.uint8)
c = cv.morphologyEx(t, cv.MORPH_OPEN, kernel)
Note that I used import cv2 as cv. I also used opening instead of closing since in the example they inverted colors during thresholding

Fit text to existing Surface in pygame

I'm trying to create text surfaces that are of the same size no matter the text. In other words: I want longer text to have smaller font size and shorter text to have bigger font size in order to fit the text to an already existing Surface.
To create text in pygame I am:
Creating a font object. For example: font = pygame.font.SysFont('Arial', 32)
Creating a text surface from the font object. For example: text = font.render('My text', True, (255, 255, 255))
Bliting the text surface.
The problem is that I first need to create a font object of a certain size before creating the text surface. I've created a function that does what I want:
import pygame
def get_text(surface, text, color=(255, 255, 255), max_size=128, font_name='Arial'):
"""
Returns a text surface that fits inside given surface. The text
will have a font size of 'max_size' or less.
"""
surface_width, surface_height = surface.get_size()
lower, upper = 0, max_size
while True:
font = pygame.font.SysFont(font_name, max_size)
font_width, font_height = font.size(text)
if upper - lower <= 1:
return font.render(text, True, color)
elif max_size < 1:
raise ValueError("Text can't fit in the given surface.")
elif font_width > surface_width or font_height > surface_height:
upper = max_size
max_size = (lower + upper) // 2
elif font_width < surface_width or font_height < surface_height:
lower = max_size
max_size = (lower + upper) // 2
else:
return font.render(text, True, color)
Is there any other way to solve this problem that's cleaner and/or more efficient?
This, unfortunately, seems to be the most appropriate solution. Font sizes are more of a approximation and differs between fonts, so there isn't a uniform way to calculate the area a specific font will take up. Another problem is that certain characters differs in size for certain fonts.
Having a monospace font would theoretically make it more efficient to calculate. Just by dividing the surface's width with the string's length and check which size of a monospace font covers that area.
You can re-scale the text image to fit:
def fit_text_to_width(text, color, pixels, font_face = None):
font = pygame.font.SysFont(font_face, pixels *3 // len(text) )
text_surface = font.render(text, True, color)
size = text_surface.get_size()
size = ( pixels, int(size[1] * pixels / size[0]) )
return pygame.transform.scale(text_surface, size)
The Font object seems to have to methods size() -> (width, height) and metrics() -> list[(minx, maxx, miny, maxy)] that sounds useful for checking the size of each resulting character and width of text.
The simplest way would be to use size and simply scale down text to the required width and height based on some ratio screen_width / font.size()[0] for example.
For breaking the text into lines, the metrics method is needed. It should be possible to loop through the metrics list and split up the text based on the summed width for each line.

Resources