def text_wrap(text: str, font: ImageFont.FreeTypeFont, max_width: int) -> str: """ Text wrapper @param text: Text to wrap @type text: str @param font: Font that the text will be displayed with @type font: ImageFont.FreeTypeFont @param max_width: The maximal width of the text @type max_width: int @return: Wrapped text @rtype: str """ lines: list[str] = [] # TODO: implement emoji # If the text width is smaller than the image width, then no need to split # just add it to the line list and return if font.getsize(text)[0] <= max_width: return text # split the line by spaces to get words words: list[str] = text.split(' ') i = 0 # append every word to a line while its width is shorter than the image width while i < len(words): line = '' while i < len(words) and font.getsize(line + words[i])[0] <= max_width: line = line + words[i] + " " i += 1 if not line: line = words[i] i += 1 lines.append(line) return '\n'.join(lines)
def __text_wrap(self, text: str, font: PIL_ImgFont.FreeTypeFont, max_width: int) -> List[str]: """ Wrap the into multiple lines """ lines = [] # If the width of the text is smaller than image width # we don't need to split it, just add it to the lines array # and return if font.getsize(text)[0] <= max_width: lines.append(text) else: # split the line by spaces to get words words = text.split(' ') i = 0 while i < len(words): line = '' while i < len(words) and font.getsize( line + words[i])[0] <= max_width: line = line + words[i] + " " i += 1 if not line: line = words[i] i += 1 lines.append(line) return lines
def draw_text(self, image: PillowImage, text: List[str], font: PillowImageFont): draw = ImageDraw.Draw(image) for line_index, line in enumerate(text): font_width, _ = font.getsize(line) _, height_offset = font.getoffset(line) x = self._get_x(font_width) y = self._get_y(line_index, self.line_height, height_offset) for c in line: if not is_emoji(c) and c.isprintable(): draw.text((x, y), c, font=font, fill=self.font_color) x += math.ceil(draw.textsize(c, font)[0]) elif self.get_emoji_content: emoji_content = self.get_emoji_content(c) if emoji_content: with tempfile.NamedTemporaryFile(mode='wb') as f: temp_filename = f.name f.write(emoji_content) emoji_img = Image.open(temp_filename) resized_emoji_img = emoji_img.resize( (font.size, font.size)) image.paste(resized_emoji_img, (x, y), resized_emoji_img) x += math.ceil(resized_emoji_img.width) return image
def __get_x_centered(text_line: str, width_avail: int, font: ImageFont.FreeTypeFont) -> int: """Get the X coordinate at which to draw a text line horizontally-centered. Parameters ---------- text_line : str Text line to draw. width_avail : int Width available to draw (essentially the image's width). font : ImageFont.FreeTypeFont Font used to draw. Returns ------- int The X coordinate at which to draw the text line horizontally-centered. """ # https://stackoverflow.com/a/46220683/9263761 ascent, descent = font.getmetrics() # Width needed to draw the line width_text = font.getmask(text_line).getbbox()[2] + font.font.getsize( text_line)[1][0] # Calculate the centered X coordinate x = (width_avail - width_text) // 2 # Return the first Y coordinate and a list with the height of each line return x
def draw_text(self, color: Color, size, font: ImageFont.FreeTypeFont): im = Image.new('RGB', size, color.rgb) draw = ImageDraw.Draw(im) name = str(color) text_size = font.getsize(name) text_color = self.text_color(color) if text_size[0] > size[0]: all_lines = [] lines = split_string(name, maxlen=len(name) // (text_size[0] / size[0])) margin = 2 total_y = 0 for line in lines: line = line.strip() if not line: continue if text_size[1] + total_y > size[1]: break x = (size[0] - font.getsize(line)[0]) // 2 all_lines.append((line, x)) total_y += margin + text_size[1] y = (size[1] - total_y) // 2 for line, x in all_lines: draw.text((x, y), line, font=font, fill=text_color) y += margin + text_size[1] else: x = (size[0] - text_size[0]) // 2 y = (size[1] - text_size[1]) // 2 draw.text((x, y), name, font=font, fill=text_color) return im
def wrap_new(canv: ImageDraw.Draw, box: Tuple[Tuple[int, int], Tuple[int, int]], text: str, *, font: ImageFont.FreeTypeFont): _, h = font.getsize('yA') max_width = box[1][0] - box[0][0] max_height = box[1][1] def write(x: int, y: int, line: List[str]): text_ = ' '.join(line) font_ = auto_font(font, text_, max_width) w, h = font_.getsize(text_) xy = (x + center(w, max_width), y) canv.text(xy, text_, fill='black', font=font_) x, y = box[0] line = [] for word in text.split(): w, _ = font.getsize(' '.join(line + [word])) if w > max_width: write(x, y, line) y += h if y > max_height: return line = [word] else: line.append(word) if line: write(x, y, line)
def _get_character_pixel_width(font: ImageFont.FreeTypeFont, char: str) -> int: """Use getlength over using getsize for character pixel width, if available in PIL.""" try: write_pos = int(font.getlength(char)) except AttributeError: write_pos = font.getsize(char)[0] return write_pos
def renderPick(self, font: ImageFont.FreeTypeFont) -> Image: """ Render Pick 2, or draw 2/pick 3 """ # Figure out the size of the image pickIm = self.renderCircle(self.blanks, font) w, h = pickIm.size pickT = "PICK " pw, ph = font.getsize(pickT) w += pw if self.blanks > 2: h = round(h * 1.75) drawIm = self.renderCircle(self.blanks - 1, font) drawT = "DRAW " dw, dh = font.getsize(drawT) h += dh w = max(w, drawIm.width + dw) else: drawIm = None im = Image.new("RGB", (w, h), (0, 0, 0)) draw = ImageDraw.Draw(im) # Draw the Pick nw, nh = pickIm.size im.paste(pickIm, (w - nw, h - nh)) draw.text((w - nw - pw, h - nh // 2 - ph // 2), pickT, fill=(255, 255, 255), font=font) if drawIm: # Draw the Draw nw, nh = drawIm.size im.paste(drawIm, (w - nw, 0)) draw.text((w - nw - dw, nh // 2 - dh // 2), drawT, fill=(255, 255, 255), font=font) # Close the temporary images pickIm.close() if drawIm: drawIm.close() newIm = im.resize((round(w * 0.75), round(h * 0.75)), resample=Image.LANCZOS) im.close() return newIm
def _get_metrics_map( text: str, font: ImageFont.FreeTypeFont, with_vertical_metrics: bool = True ) -> typing.Union[typing.Dict[str, typing.Tuple[int, int]], typing.Dict[ str, typing.Tuple[int, int, int, int]], ]: return { char: (*size, size[1] - height, size[1]) if with_vertical_metrics and (height := font.getmask(char).size[1]) is not None else size for char in set(text) if (size := font.getsize(char)) }
def draw_text(self, image: PillowImage, text: List[str], font: PillowImageFont): draw = ImageDraw.Draw(image) for line_index, line in enumerate(text): font_width, _ = font.getsize(line) _, height_offset = font.getoffset(line) x = self._get_x(font_width) y = self._get_y(line_index, self.line_height, height_offset) draw.text((x, y), line, font=font, fill=self.font_color) return image
def renderLine(self, raw: str, image: Image, y: int, height: int, font: ImageFont.FreeTypeFont) -> ImageDraw: """ Render a single line """ # Get the number of blanks blanks = raw.count("_blank_") no_line = raw.replace("_blank_", "") ascent, descent = font.getmetrics() w, h = font.getsize(no_line) (width, baseline), (offset_x, offset_y) = font.font.getsize(no_line) if height < h: line = ascent else: line = height - descent / 2 y = y - height + h ascent, descent = font.getmetrics() draw = ImageDraw.Draw(image) # draw.line((0, y, w, y), fill=(255, 0, 0)) # draw.line((0, y + height, w, y + height), fill=(0, 255, 0)) # draw.line((0, y + h, w, y + h), fill=(0, 0, 255)) if blanks > 0: # Get the size of each blank. blank_size = (image.size[0] - w) / blanks groups = raw.split("_blank_") # Render each group x = 0 for group in groups: # Draw the text draw.text((x, y), group, self.color, font) w, _ = font.getsize(group) x += w if blanks > 0: # Draw the line draw.line( ((x + 5, y + line), (x + blank_size - 5, y + line)), fill=self.color, width=2) x += blank_size blanks -= 1 return draw.text((0, y), raw, self.color, font)
def draw_right_text(text: str, draw: ImageDraw, font: FreeTypeFont, f_width: int, x: int, y: int, color: Tuple[int, int, int], outline_percentage, outline_color, fontsize) -> Tuple[int, int]: text_width = font.getsize(text)[0] off_x = f_width - text_width draw.text((x + off_x, y), text, color, font, stroke_width=round(outline_percentage * 0.01 * fontsize), stroke_fill=outline_color) return font.getsize(text)
def wrap_line_with_font(inputstr: str, max_width: int, font: ImageFont.FreeTypeFont) -> str: if font.getsize(inputstr)[0] <= max_width: return inputstr tokens = inputstr.split(" ") output = "" segments = [] for token in tokens: combo = output + " " + token if font.getsize(combo)[0] > max_width: segments.append(output + "\n") output = token else: output = combo return "".join(segments + [output]).strip()
def renderText(self, font: ImageFont.FreeTypeFont, width: int = 0, maxwidth: int = 0) -> Image: """ Render the text of the card """ lines = list() # split the words that are too long for match in re.finditer(r"(?:\\n|\s*)(.*?(?=\\n)|.{0,20}(?=\s|$))", self.raw): if match[1]: lines.append(match[1]) ws, hs = zip(*[font.getsize(l) for l in lines]) mult = 1.5 if not width: width = max(ws) padding = (maxwidth - width) // 2 h = round(sum(hs) / len(hs)) height = round(h * (len(hs) - 1) * mult + hs[-1]) im = Image.new("RGB", (width, height + padding), self.background) y = padding for l in lines: self.renderLine(l, im, y, h, font) y += round(h * mult) return im
def __calculate_text_width(tweet_info: TweetInfo, graphic_settings: GraphicSettings, font: ImageFont.FreeTypeFont) -> float: """Calculate the width of the tweet content. The width is given as the width of the largest tweet text line. Parameters ---------- tweet_info : TweetInfo Dictionary with all the tweet's information. graphic_settings : GraphicSettings Dictionary with the graphic's settings. font : ImageFont.FreeTypeFont Font to be used for the text. Returns ------- float Width needed to draw the text. """ # Break the text into multiple lines based on the character limit text_wrapped = wrap(tweet_info["tweet_text"], graphic_settings["wrap_limit"]) # The text's width is set by the largest text line width_text = max([ font.getmask(text_line).getbbox()[2] + font.font.getsize(text_line)[1][0] for text_line in text_wrapped ]) return width_text
def __calculate_username_width(user_name: str, user_pic: str, font: ImageFont.FreeTypeFont) -> float: """Calculate the width of the username. The width is given as the width of the largest username line (in case it is broken up into multiple lines). user_name : str User name. user_pic : str Path to the profile picture. font : ImageFont.FreeTypeFont Font to be used for the username (header). Returns ------- float Width needed to draw the username. """ # Calculate username character limit per line based on the presence of\ # the profile picture username_char_limit = 19 if user_pic != "" else 38 # Break the text into multiple lines based on the character limit username_wrapped = wrap(user_name, username_char_limit) # The username's width is set by the largest username text line width_username = max([ font.getmask(text_line).getbbox()[2] + font.font.getsize(text_line)[1][0] for text_line in username_wrapped ]) return width_username
def get_text_pos(size: Tuple[int, int], text: Text, font: ImageFont.FreeTypeFont, relative: bool = False) -> Tuple[int, int]: min_x = round(text.x_range[0] * size[0]) max_x = round(text.x_range[1] * size[0]) min_y = round(text.y_range[0] * size[1]) max_y = round(text.y_range[1] * size[1]) text_size = font.getsize_multiline(text.text, stroke_width=text.stroke_width * font.size) if int(text.position.value) // 3 == 0: pos_y = min_y elif int(text.position.value) // 3 == 1: pos_y = round((min_y + max_y) / 2 - text_size[1] / 2) else: pos_y = max_y - text_size[1] if int(text.position.value) % 3 == 0: pos_x = min_x elif int(text.position.value) % 3 == 1: pos_x = round((min_x + max_x) / 2 - text_size[0] / 2) else: pos_x = max_x - text_size[0] if relative: return pos_x - min_x, pos_y - min_y else: return pos_x, pos_y
def _get_multiline_text(text, font: PillowImageFont, width: int) -> List[str]: font_width, _ = font.getsize(text) line_length = int((width / (font_width / len(text)))) text_lines = wrap(text, line_length) return text_lines
def center_text(text: str, x_pos: int, y_pos: int, font: ImageFont.FreeTypeFont) -> Tuple[int, int]: text_x, text_y = font.getsize(text) x = (x_pos - text_x//2) # / 2 y = (y_pos - text_y//2) # / 2 return x, y
def getoffset(font: FreeTypeFont, text_str: str) -> Tuple[int, int]: """get the offset of text""" if USE_PIL: offset = font.getoffset(text_str) else: # FOR NOW...because I don't think this is truly necessary offset = (0, 0) return offset
def get_final_text_lines(text: str, text_width: int, font: ImageFont.FreeTypeFont) -> int: lines = text.split("\n") line_count = 0 for line in lines: if not line: line_count += 1 continue line_count += int(math.ceil(float(font.getsize(line)[0]) / float(text_width))) return line_count + 1
def __get_y_and_heights(text_wrapped: List[str], height_avail: int, margin: float, font: ImageFont.FreeTypeFont) -> Tuple[int, List[int]]: """Calculate the height needed to draw all text lines, vertically centered. Return the vertical coordinate to draw the first line at and a list of the heights for each line (margin included). Parameters ---------- text_wrapped : List[str] Lines of text to draw. height_avail : int Height available to draw in (essentially the image's height). margin : float Vertical margin between text lines. font : ImageFont.FreeTypeFont Font used to draw. Returns ------- Tuple[int, List[int]] The vertical coordinate of the first text line and a list with the heights of all lines. """ # https://stackoverflow.com/a/46220683/9263761 ascent, descent = font.getmetrics() # Calculate the height needed to draw each line of text height_lines = [ font.getmask(text_line).getbbox()[3] + font.font.getsize(text_line)[1][1] + margin for text_line in text_wrapped ] # The last line doesn't have a bottom margin height_lines[-1] -= margin # Total height needed height_text = sum(height_lines) # Calculate the Y coordinate at which to draw the first line of text y = (height_avail - height_text) // 2 # Return the first Y coordinate and a list with the height of each line return (y, height_lines)
def wrap_text(text: str, font: FreeTypeFont, max_width: float) -> List[str]: line = "" lines = [] for t in text: if t == "\n": lines.append(line) line = "" elif font.getsize(line + t)[0] > max_width: lines.append(line) line = t else: line += t lines.append(line) return lines
def does_label_needs_to_be_wrapped(font: ImageFont.FreeTypeFont, label_text: str, label_width: int) -> Tuple[bool, int]: """ :param font: :param label_text: single-line text to be checked if it needs to wrapped :param label_width: :return: True if text needs to be wrapped else False """ w, h = font.getsize(label_text) per_char_pixels = w / len(label_text) can_fit_n_chars_in_single_line = label_width // per_char_pixels return (len(label_text) > can_fit_n_chars_in_single_line, can_fit_n_chars_in_single_line)
def getsize(font: FreeTypeFont, text_str: str) -> Tuple[int, int]: """get the size of text""" if USE_PIL: # original behavior # size = font.getsize(text_str) # new behavior width, _ = font.getsize(text_str) ascent, descent = font.getmetrics() height = ascent + descent size = (width, height) else: info = text_scala.get_info(text_str, _font_to_tuple(font), 0, BORDER_DEFAULT) # Important note! Info height and width may not include some antialiasing pixels! # import numpy as np # column_sums = np.sum(img, axis=0) # present = np.where(column_sums > 0)[0] # calculated_width = (present[-1] - present[0]) + 1 # print("metrics width:", info["width"]) # print("calculated width:", calculated_width) size = (info["width"], info["height"]) return size
def wrap_text(text: str, font: FreeTypeFont, max_width: float, stroke_width: int = 0) -> List[str]: line = '' lines = [] for t in text: if t == '\n': lines.append(line) line = '' elif font.getsize(line + t, stroke_width=stroke_width)[0] > max_width: lines.append(line) line = t else: line += t lines.append(line) return lines
def __calculate_header_width(tweet_info: TweetInfo, graphic_settings: GraphicSettings, font_header: ImageFont.FreeTypeFont) -> float: """Calculate the width of the header. Parameters ---------- tweet_info : TweetInfo Dictionary with all the tweet's information. graphic_settings : GraphicSettings Dictionary with the graphic's settings. font : ImageFont.FreeTypeFont Font to be used for the text. Returns ------- float Width needed to draw the header (pixels). """ # Calculate the header of the profile picture profile_pic_width = graphic_settings["profile_pic_size"][0] # Calculate the user name width username_width = __calculate_username_width(tweet_info["user_name"], tweet_info["user_pic"], font_header) # Calculate the user tag width user_tag = tweet_info["user_tag"] usertag_width = font_header.getmask(user_tag).getbbox()[2] +\ font_header.font.getsize(user_tag)[1][0] # The width of the header's text is set by the largest of the user\ # name and user tag if username_width > usertag_width: text_width = username_width else: text_width = usertag_width # The header width is given as the sum of the profile picture and\ # the header text width width_header = profile_pic_width + \ graphic_settings["margin_bottom"] + text_width return width_header
def __calculate_header_height( tweet_info: TweetInfo, graphic_settings: GraphicSettings, font: ImageFont.FreeTypeFont, ) -> float: """Calculate the header height: username, user tag and profile picture. Parameters ---------- tweet_info : TweetInfo Dictionary with all the tweet's information. graphic_settings : GraphicSettings Dictionary with the graphic's settings. font : ImageFont.FreeTypeFont Font to be used for the header. Returns ------- float Height needed for the header (pixels). """ user_pic = tweet_info["user_pic"] height_margin = graphic_settings["margin_bottom"] profile_pic_size = graphic_settings["profile_pic_size"] user_name = tweet_info["user_name"] user_tag = tweet_info["user_tag"] # Calculate the height of the header's text: user name and user tag height_user_name = __calculate_username_height(user_name, user_pic, height_margin, font) height_usertag = font.getmask(user_tag).getbbox()[3] + font.font.getsize( user_tag)[1][1] height_header_text = height_user_name + height_usertag + height_margin # If the header's text is taller than the profile picture, than that's\ # the header height if (height_header_text + height_margin) > profile_pic_size[1]: height_header = height_header_text # Otherwise, the profile picture sets the header height else: height_header = profile_pic_size[1] return height_header
def renderCircle(self, txt: str, font: ImageFont.FreeTypeFont) -> Image: """ Render text inside a circle """ txt = str(txt) # Get the size of the circle w, h = font.getsize(txt) diam = round(math.sqrt(w**2 + h**2) * 1.25) im = Image.new("RGB", (diam, diam), (0, 0, 0)) draw = ImageDraw.Draw(im) draw.ellipse((0, 0, diam, diam), fill=(255, 255, 255)) draw.text((diam // 2 - w // 2, diam // 2 - h // 2), txt, fill=(0, 0, 0), font=font) return im
def circle_text(img, text, base_circle, rotation, font: ImageFont.FreeTypeFont, color, angle=None, upscale=4): if angle is None: angle = 360 / len(text) angle_rad = angle / 360 * 2 * math.pi bigger_size = (img.size[0] * upscale, img.size[1] * upscale) bigger_font = font.font_variant(size=font.size * upscale) cx, cy, r = _extract_circle(base_circle) ucx, ucy = cx * upscale, cy * upscale rad = r * upscale + bigger_font.size / 2 img2 = Image.new("RGBA", bigger_size, (0, 0, 0, 0)) for i, c in enumerate(text): angled = angled_text(c, fill=color, font=bigger_font, angle=-90 - angle * i) text_origin = (ucx + rad * math.cos(angle_rad * i), ucy + rad * math.sin(angle_rad * i)) img2.paste(angled, box=(int(text_origin[0] - angled.width / 2), int(text_origin[1] - angled.height / 2))) img2 = img2.rotate(rotation + 90, center=(ucx, ucy)).resize(img.size, resample=Image.LANCZOS) img.paste(img2, mask=img2) return r + font.size + 4