Draw Text Box Python Image
Python provides a medley of cord manipulation tools via the standard library. Combine with the text manipulation tools of imaging libraries like Pillow, Python tin can exist an accented workhorse for creating text-heavy visual media. Even with these tools, text-wrapping can be a challenge. In this article, we'll cover how to fit wrapped text to an prototype using Python and Pillow.
- 1 Pace 1: Creating a Groundwork Paradigm
- 2 Stride 2: Loading a Custom Font & Text
- 3 Pace three: Text Wrapping
- iv Pace 4: Accounting for Font Size
- 5 Step 5: Text Placement
- v.1 Increased Font Size
- 5.2 Decreased Font Size & Longer Text
- 6 Putting information technology All Together
- vii @TODO
- vii.1 Accost the problems of vertical overflow
- 8 Final Thoughts
Python offers some excellent text-wrapping utilities via the standard textwrap module. Pillow is a third-party Python imaging library that provides visual text tools—such as loading custom fonts and overlaying text on images. By using both of these toolsets in concert one tin can manage to overlay wrapped text onto an epitome and ensure the text is scaled accordingly. The best part? Information technology takes some creative problem-solving!
Stride ane: Creating a Background Paradigm
The Pillow library is an imaging library first and a text manipulation tool second. As such, to add the text we beginning need an Image class object onto which our text will be added. Think of this as a blank canvas of paper on which we'll be writing our text.
To create a canvas (background) Image there are two options: create a new prototype specifying attributes like color, size, and image mode; or but load an existing paradigm file.
For this tutorial, I'm simply loading an image that I created in Photoshop named groundwork.jpg. I'll also be using a custom font I downloaded from Google Fonts named Source Lawmaking Pro. Here'south what the project structure looks like:
ProjectRoot ├── wrapper.py ├── background.jpg └── SourceCodePro-Bold.ttf
To load the groundwork image in grooming use the post-obit lawmaking replacing the fp
(file path) statement of background.jpg with the full file path to whichever image you use. The fashion
argument specifies what file mode to open the file; the default is r
for read-only.
from PIL import Prototype, ImageDraw, ImageFont # Open image img = Epitome.open(fp='background.jpg', manner='r') >>> <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1024x683 at 0x1958734C1F0> # View prototype to ensure it loads properly img.evidence()
This code loads our initial groundwork epitome which will be our starting indicate for creating wrapped text on an image in Python.
Annotation: this returns a JpegImageFile object in RBG mode.
Nosotros won't need to know that for our purposes but, depending on the use case, ane might accept to specify mode and image blazon when using Pillow. Beneath is our background image on which we'll exist adding text.
Stride 2: Loading a Custom Font & Text
Our background image provides a canvass on which to place text. I'one thousand using a fancy yellowish paradigm for visual purposes only—a plain white, transparent, or any other color would piece of work as well. Using Pillow'southward ImageFont class we tin load the SourceCode Pro font we downloaded from Google Fonts earlier:
# Load custom font font = ImageFont.truetype(font='SourceCodePro-Bold.ttf', size=42) >>> <PIL.ImageFont.FreeTypeFont object at 0x000001BECB5005E0>
To begin theprocess of adding text to our background nosotros'll be using Pillow's ImageDraw
module. This module provides a range of tools that permit one to draw arcs, lines, points, lines, and many more geometric constructs.
Check out the official documentation for more on the available methods.
For now, we'll be restricting use to the text
method to add some text. The following code will create a custom ImageDraw object, load our custom font, and add together text to our groundwork image.
# Create DrawText object draw = ImageDraw.Draw(im=img) >>> <PIL.ImageDraw.ImageDraw object at 0x000001EEB39991F0> # Define our text text = """Simplicity--the art of maximizing the amount of piece of work not done--is essential.""" # Add text to the image draw.text(xy=(0, 0), text=text, font=font, fill up='#000000') # View the updated prototype img.prove()
Before the grand reveal, let me explain some arguments that I've added:
- xy: The x + y coordinates, as a tuple of pixel values, relative to our epitome, at which the text placement will beginning. The (0, 0) value specifies the top-left corner.
- font: The ImageFont object referencing our custom SourceCode Pro font.
- fill: The colour, as a hexadecimal value, assigned to our text.
At that place are a number of other optional arguments that tin can be provided. These are outlined well in the official Pillow ImageDraw documentation. Nosotros won't be touching on most of them, but the anchor argument volition exist used later in this tutorial. For now, let'due south come across what our image looks like with the text added:
Notation: This quote comes from the AGILE manifesto which outlines the core philosophy of software development practices such equally SCRUM, FURPS, and SOLID blueprint principles.
Pace 3: Text Wrapping
The next few steps tin can be done in any sequence—they amount to the same effect. I'm starting with text-wrapping because it's the trickest and I like to get the tough jobs done first. For this task, we'll be utilizing some of the Python standard library'south text-manipulation tools. Namely, the textwrap
module's wrap
and fill
functions. For a detailed look at the textwrap
module read this article. For our purposes hither, just know the post-obit:
- wrap: takes a body of text and returns as a drove of lines less than or equal to in character count to that of a specified maximum.
- make full: Returns a cake of solid text having used the wrap function to split lines into widths of a character maximum.
We have roughly 41 characters displayed, including whitespace and special characters, in our image so far. I'm going for a little extra padding for visual effect, so I'grand going to wrap our text to a 35 grapheme maximum. To do this, I use the textrwap.fill()
method as follows:
import textwrap # Ascertain the text to be used on our prototype text = """Simplicity--the fine art of maximizing the amount of piece of work not done--is essential.""" # Create a new version of our text with maximum of 35 chars per line text = textwrap.fill up(text=text, width=35) >>> Simplicity--the art of maximizing the amount of work not done--is essential. # Get some line counts for fun "\n".join(f"{str(len(x))} - {10}" for x in text.splitlines()) >>> 33 - Simplicity--the art of maximizing 31 - the corporeality of work not done--is x - essential.
You'll notation that the textwrap.fill()
function splits on whole-words by default. This leaves u.s.a. with three lines of full widths, measured by character count, of 33, 31, and 10 respectively. Here's what our image looks like now:
This is definitely a step in the right direction merely still far from what I'one thousand after. At that place are several issues that still have to be ironed out here. The following are the most immediate:
- Adjusting for font and fontsize changes
- Text positioning
- Scaling the text to fit the image
- Text alignment (centered, left-aligned, justified, etc.)
Each of these will help ensure our last solution offers us control to create unlike presentations of text and also dynamic and flexible plenty to allow the exchange of unlike texts and fonts. Afterwards all, what good is our solution if it merely works for a single font, on a single image, using a unmarried string of text? Not much! Let's offset by addressing the issue of font sizes.
Pace four: Accounting for Font Size
Python'due south textwrap
module has already demonstrated its utility. However, it has the shortcoming of using character count in determining the width of line breaks. This makes consummate sense—given it isn't an imaging focused module. We're yet going to utilize this module but we'll have to convert our width parameter from pixel size to grapheme count dynamically by completing the following deportment
- Calculate an average character width given font and size
- Calculate the maximum number of characters, using our average width value, that tin fit across a specified pixel distance.
- Use that calculated value to wrap the text
I've wrapped all this up into a tidy function just for now, we'll intermission things down step-by-step to visualize the benefits of this stride:
from string import ascii_letters # Calculate the average length of a single character of our font. # Notation: this takes into business relationship the specific font and font size. avg_char_width = sum(font.getsize(char)[0] for char in ascii_letters) / len(ascii_letters) >>> 25.03846153846154 # Translate this boilerplate length into a character count # to fill 95% of our prototype'southward total width max_char_count = int( (image.size[0] * .95) / avg_char_width ) >>> 38 # Create a wrapped text object using scaled character count scaled_wrapped_text = textwrap.fill(text=text, width=) >>> Simplicity--the fine art of maximizing the amount of work not done--is essential.
This uses the string.ascii_letters
variable along with our ImageFont object to summate the average width, as a pixel value, of each letter in our font. We then catechumen this value into a maximum number of characters per line by dividing our image width (scaled past a cistron of .95 for visual aesthetics) by our average font graphic symbol length.
Nosotros then use the resulting value as the width argument for the textwrap.fill()
part'southward width argument (as character count) to create a wrapped text object that is scaled to the image. Here'due south our image at present:
Overall I think things are looking much better — we can employ a custom font, of a custom size, and not have to worry about our text inundation our paradigm horizontally. There are yet several needed tweaks including text justification and placement.
Pace five: Text Placement
Previous versions of Pillow made little accommodation for the command of where a TextDraw object was placed on an image. At some point [commendation needed] the Pillow library matured and Text Anchors were implemented. This allows one to specifiy standardized locations to which a text object is aligned to an paradigm.
There are several possible alignment options outlined in the official Text Anchors documentation. For our purposes here, I'll be positioning our text to the vertical and horizontal center using the mm parameter for the anchor argument. Having already scaled out text appropriately, the text can be centered equally such:
# Add together the 'mm' argument to the ballast parameter to eye horizontally and vertically draw.text(xy=(0, 0), text=text, font=font, fill='#000000', anchor='mm')
As you lot can see, I've added the mm argument equally the anchor parameter hither. Also, I changed the width of our text to be 61.8% of our paradigm'due south total width (ever drawn to the aesthetics of the golden ratio!).
Yikes. I made abigmistake hither by forgetting to update the position at which our text was existence added to the image. Up to this point, we've been placing our text starting at the tiptop-left corner (coordinates (0, 0) ) of our image. Our text istechnically centered now, but it's centered at the (0, 0) location which causes unwanted overflow. Let'due south use the coordinates of our image and run into what happens:
This is much more along the lines of what I was hoping to achieve. Our text is centered, scaled, and volition dynamically adjust should we choose to update the font or the font size. Let's update some parameters and double-cheque things work as expected.
Increased Font Size
Hither I've increased the font size from a small 42 to an imposing 72 suited for bold, captivating memes and banners:
Decreased Font Size & Longer Text
Hither I'll add together a significant number of additional characters—using a quote from Alan Turing—and also decrease the font size from the larger 72 to a meager 28.
Putting it All Together
Beneath is all the code that nosotros've put together so far. Notation that this code assumes an image named groundwork.jpg
and a valid font file named SourceCodePro-Bold.ttf
are present in the current working directory.
# Open epitome img = Image.open(fp='background.jpg', mode='r') # Load custom font font = ImageFont.truetype(font='SourceCodePro-Bold.ttf', size=72) # Create DrawText object depict = ImageDraw.Draw(im=img) # Define our text text = """Simplicity--the fine art of maximizing the amount of piece of work non done--is essential.""" # Summate the average length of a single graphic symbol of our font. # Note: this takes into account the specific font and font size. avg_char_width = sum(font.getsize(char)[0] for char in ascii_letters) / len(ascii_letters) # Translate this boilerplate length into a character count max_char_count = int(img.size[0] * .618 / avg_char_width) # Create a wrapped text object using scaled character count text = textwrap.fill up(text=text, width=max_char_count) # Add text to the image draw.text(xy=(img.size[0]/2, img.size[one] / ii), text=text, font=font, fill='#000000', ballast='mm') # view the result img.testify()
This volition result in the image that we saw terminal. I'm a fan of object-oriented programming styles and accept created a grade to handle all this with a little more stylistic grace. Information technology's a piece of work-in-progress and is available on Github here.
@TODO
Address the problems of vertical overflow
when in that location are too many lines of text to adequately fit on the background. There would be tradeoffs hither to remainder an increase in overall text width, a possible decrease in font size, and the possible example such that no applied adjustment to either would result in enough existent manor to conform a text. For instance, one couldn't fit the entirety of Python documentation onto a single 1024 10 1024 image and expect the text to be legible.
Proposed: force a maximum font size proportional to a fixed percentage of the image superlative.
Last Thoughts
Python + Pillow affords a powerful set of text-manipulating tools. With a fiddling bit of translation between the textwrap
and ImageDraw
features, one tin create visually stunning images with text overlay in Python. These tools are one of the many aspects of Python that make it i of the nearly popular programming languages in the world. The tools and approaches outlined here are far from robust, autumn short of completeness, and come with no warranty. I know I learned a lot while writing this article and hope that yous have likewise!
Source: https://www.alpharithms.com/fit-custom-font-wrapped-text-image-python-pillow-552321/