Only call text_layout once in getmask2 by radarhere · Pull Request #7206 · python-pillow/Pillow
Helps #6618
Backstory
The following is an earlier version of ImageFont's getmask2(), as described in the quoted comment.
| if fill is _UNSPECIFIED: | |
| fill = Image.core.fill | |
| else: | |
| deprecate("fill", 10) | |
| size, offset = self.font.getsize( | |
| text, mode, direction, features, language, anchor | |
| ) | |
| if start is None: | |
| start = (0, 0) | |
| size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) | |
| offset = offset[0] - stroke_width, offset[1] - stroke_width | |
| Image._decompression_bomb_check(size) | |
| im = fill("RGBA" if mode == "RGBA" else "L", size, 0) | |
| if min(size): | |
| self.font.render( | |
| text, | |
| im.id, | |
| mode, | |
| direction, | |
| features, | |
| language, | |
| stroke_width, | |
| ink, | |
| start[0], | |
| start[1], | |
| ) |
The function getmask2 performs the following steps:
- calls getsize to get the size of the text
- calls Image._decompression_bomb_check to compare size with MAX_IMAGE_PIXELS
- calls the fill function passed as argument to create a blank image
- calls render to draw text into the blank image
After Pillow 10 the deprecated fill parameter will be replaced by a direct call to the internal function. After this, the only Python function to be called between the two C functions is the decompression bomb check. If this was moved into C, the two functions could be combined to remove the duplicate call to text_layout.
#7059 removed the deprecated fill parameter. So here is the current version of ImageFont's getmask2().
| size, offset = self.font.getsize( | |
| text, mode, direction, features, language, anchor | |
| ) | |
| if start is None: | |
| start = (0, 0) | |
| size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) | |
| offset = offset[0] - stroke_width, offset[1] - stroke_width | |
| Image._decompression_bomb_check(size) | |
| im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) | |
| if min(size): | |
| self.font.render( | |
| text, | |
| im.id, | |
| mode, | |
| direction, | |
| features, | |
| language, | |
| stroke_width, | |
| ink, | |
| start[0], | |
| start[1], | |
| ) |
This is the only place that font.render is called.
Change
So I added a change to make font.getsize a builtin part of font.render, meaning that text_layout is not called once by each, but only once.
From my tests, this causes getmask2 to be 10% faster.
There is an awkward part of this change though - the _imagingft extension is not connected to the C code for creating new images. I couldn't call ImagingNewDirty and ImagingFill. Despite the quoted comment's expectation that this could all be done in C, it didn't realise that our C code is actually split up into these extensions. Instead, I've passed Image.core.fill into C, to then be called by PyObject_CallFunction.