Sunday01's picture
up
9dce458
import os
import re
import cv2
import numpy as np
import freetype
import functools
from pathlib import Path
from typing import Tuple, Optional, List
from hyphen import Hyphenator
from hyphen.dictools import LANGUAGES as HYPHENATOR_LANGUAGES
from langcodes import standardize_tag
from ..utils import BASE_PATH, is_punctuation, is_whitespace
try:
HYPHENATOR_LANGUAGES.remove('fr')
HYPHENATOR_LANGUAGES.append('fr_FR')
except Exception:
pass
CJK_H2V = {
"‥": "︰",
"—": "︱",
"―": "|",
"–": "︲",
"_": "︳",
"_": "︴",
"(": "︵",
")": "︶",
"(": "︵",
")": "︶",
"{": "︷",
"}": "︸",
"〔": "︹",
"〕": "︺",
"【": "︻",
"】": "︼",
"《": "︽",
"》": "︾",
"〈": "︿",
"〉": "﹀",
"「": "﹁",
"」": "﹂",
"『": "﹃",
"』": "﹄",
"﹑": "﹅",
"﹆": "﹆",
"[": "﹇",
"]": "﹈",
"﹉": "﹉",
"﹊": "﹊",
"﹋": "﹋",
"﹌": "﹌",
"﹍": "﹍",
"﹎": "﹎",
"﹏": "﹏",
"…": "⋮",
}
CJK_V2H = {
**dict(zip(CJK_H2V.items(), CJK_H2V.keys())),
}
def CJK_Compatibility_Forms_translate(cdpt: str, direction: int):
"""direction: 0 - horizontal, 1 - vertical"""
if cdpt == 'ー' and direction == 1:
return 'ー', 90
if cdpt in CJK_V2H:
if direction == 0:
# translate
return CJK_V2H[cdpt], 0
else:
return cdpt, 0
elif cdpt in CJK_H2V:
if direction == 1:
# translate
return CJK_H2V[cdpt], 0
else:
return cdpt, 0
return cdpt, 0
def compact_special_symbols(text: str) -> str :
text = text.replace('...', '…')
return text
def rotate_image(image, angle):
if angle == 0:
return image, (0, 0)
image_exp = np.zeros((round(image.shape[0] * 1.5), round(image.shape[1] * 1.5), image.shape[2]), dtype = np.uint8)
diff_i = (image_exp.shape[0] - image.shape[0]) // 2
diff_j = (image_exp.shape[1] - image.shape[1]) // 2
image_exp[diff_i:diff_i+image.shape[0], diff_j:diff_j+image.shape[1]] = image
# from https://stackoverflow.com/questions/9041681/opencv-python-rotate-image-by-x-degrees-around-specific-point
image_center = tuple(np.array(image_exp.shape[1::-1]) / 2)
rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0)
result = cv2.warpAffine(image_exp, rot_mat, image_exp.shape[1::-1], flags=cv2.INTER_LINEAR)
if angle == 90:
return result, (0, 0)
return result, (diff_i, diff_j)
def add_color(bw_char_map, color, stroke_char_map, stroke_color):
if bw_char_map.size == 0:
fg = np.zeros((bw_char_map.shape[0], bw_char_map.shape[1], 4), dtype = np.uint8)
return fg
# print(bw_char_map.shape, stroke_char_map.shape)
# import matplotlib.pyplot as plt
# x1, y1, w1, h1 = cv2.boundingRect(bw_char_map)
# x2, y2, w2, h2 = cv2.boundingRect(stroke_char_map)
# fig, ax = plt.subplots(1, 2)
# ax[0].imshow(bw_char_map)
# ax[1].imshow(stroke_char_map)
# # draw bounding boxes
# rect1 = plt.Rectangle((x1, y1), w1, h1, fill=False, color='red')
# rect2 = plt.Rectangle((x2, y2), w2, h2, fill=False, color='blue')
# ax[0].add_patch(rect1)
# ax[0].add_patch(rect2)
# rect1 = plt.Rectangle((x1, y1), w1, h1, fill=False, color='red')
# rect2 = plt.Rectangle((x2, y2), w2, h2, fill=False, color='blue')
# ax[1].add_patch(rect1)
# ax[1].add_patch(rect2)
# plt.show()
# since bg rect is always larger than fg rect, we can just use the bg rect
if stroke_color is None :
x, y, w, h = cv2.boundingRect(bw_char_map)
else :
x, y, w, h = cv2.boundingRect(stroke_char_map)
fg = np.zeros((h, w, 4), dtype = np.uint8)
fg[:,:,0] = color[0]
fg[:,:,1] = color[1]
fg[:,:,2] = color[2]
fg[:,:,3] = bw_char_map[y:y+h, x:x+w]
if stroke_color is None :
stroke_color = color
bg = np.zeros((stroke_char_map.shape[0], stroke_char_map.shape[1], 4), dtype = np.uint8)
bg[:,:,0] = stroke_color[0]
bg[:,:,1] = stroke_color[1]
bg[:,:,2] = stroke_color[2]
bg[:,:,3] = stroke_char_map
fg_alpha = fg[:, :, 3] / 255.0
bg_alpha = 1.0 - fg_alpha
bg[y:y+h, x:x+w, :] = (fg_alpha[:, :, np.newaxis] * fg[:, :, :] + bg_alpha[:, :, np.newaxis] * bg[y:y+h, x:x+w, :])
#alpha_char_map = cv2.add(bw_char_map, stroke_char_map)
#alpha_char_map[alpha_char_map > 0] = 255
return bg#, alpha_char_map
FALLBACK_FONTS = [
os.path.join(BASE_PATH, 'fonts/Arial-Unicode-Regular.ttf'),
os.path.join(BASE_PATH, 'fonts/msyh.ttc'),
os.path.join(BASE_PATH, 'fonts/msgothic.ttc'),
]
FONT_SELECTION: List[freetype.Face] = []
font_cache = {}
def get_cached_font(path: str) -> freetype.Face:
path = path.replace('\\', '/')
if not font_cache.get(path):
# To circumvent a bug with non ascii paths in windows use memory fonts
# https://github.com/rougier/freetype-py/issues/157#issuecomment-1683713726
font_cache[path] = freetype.Face(Path(path).open('rb'))
return font_cache[path]
def set_font(font_path: str):
global FONT_SELECTION
if font_path:
selection = [font_path] + FALLBACK_FONTS
else:
selection = FALLBACK_FONTS
FONT_SELECTION = [get_cached_font(p) for p in selection]
class namespace:
pass
class Glyph:
def __init__(self, glyph):
self.bitmap = namespace()
self.bitmap.buffer = glyph.bitmap.buffer
self.bitmap.rows = glyph.bitmap.rows
self.bitmap.width = glyph.bitmap.width
self.advance = namespace()
self.advance.x = glyph.advance.x
self.advance.y = glyph.advance.y
self.bitmap_left = glyph.bitmap_left
self.bitmap_top = glyph.bitmap_top
self.metrics = namespace()
self.metrics.vertBearingX = glyph.metrics.vertBearingX
self.metrics.vertBearingY = glyph.metrics.vertBearingY
self.metrics.horiBearingX = glyph.metrics.horiBearingX
self.metrics.horiBearingY = glyph.metrics.horiBearingY
self.metrics.horiAdvance = glyph.metrics.horiAdvance
self.metrics.vertAdvance = glyph.metrics.vertAdvance
@functools.lru_cache(maxsize = 1024, typed = True)
def get_char_glyph(cdpt: str, font_size: int, direction: int) -> Glyph:
global FONT_SELECTION
for i, face in enumerate(FONT_SELECTION):
if face.get_char_index(cdpt) == 0 and i != len(FONT_SELECTION) - 1:
continue
if direction == 0:
face.set_pixel_sizes(0, font_size)
elif direction == 1:
face.set_pixel_sizes(font_size, 0)
face.load_char(cdpt)
return Glyph(face.glyph)
#@functools.lru_cache(maxsize = 1024, typed = True)
def get_char_border(cdpt: str, font_size: int, direction: int):
global FONT_SELECTION
for i, face in enumerate(FONT_SELECTION):
if face.get_char_index(cdpt) == 0 and i != len(FONT_SELECTION) - 1:
continue
if direction == 0:
face.set_pixel_sizes(0, font_size)
elif direction == 1:
face.set_pixel_sizes(font_size, 0)
face.load_char(cdpt, freetype.FT_LOAD_DEFAULT | freetype.FT_LOAD_NO_BITMAP)
slot_border = face.glyph
return slot_border.get_glyph()
# def get_char_kerning(cdpt, prev, font_size: int, direction: int):
# global FONT_SELECTION
# for i, face in enumerate(FONT_SELECTION):
# if face.get_char_index(cdpt) == 0 and i != len(FONT_SELECTION) - 1:
# continue
# if direction == 0:
# face.set_pixel_sizes(0, font_size)
# elif direction == 1:
# face.set_pixel_sizes(font_size, 0)
# face.load_char(cdpt, freetype.FT_LOAD_DEFAULT | freetype.FT_LOAD_NO_BITMAP)
# #print("VV", prev, cdpt, face.get_char_index(prev), face.get_char_index(cdpt))
# print("VR", face.has_kerning)
# return face.get_kerning(face.get_char_index(prev), face.get_char_index(cdpt))
def calc_vertical(font_size: int, text: str, max_height: int):
line_text_list = []
# line_width_list = []
line_height_list = []
line_str = ""
line_height = 0
line_width_left = 0
line_width_right = 0
for i, cdpt in enumerate(text):
if line_height == 0 and cdpt == ' ':
continue
cdpt, rot_degree = CJK_Compatibility_Forms_translate(cdpt, 1)
ckpt = get_char_glyph(cdpt, font_size, 1)
bitmap = ckpt.bitmap
# spaces, etc
if bitmap.rows * bitmap.width == 0 or len(bitmap.buffer) != bitmap.rows * bitmap.width:
char_offset_y = ckpt.metrics.vertBearingY >> 6
else:
char_offset_y = ckpt.metrics.vertAdvance >> 6
char_width = bitmap.width
char_bearing_x = ckpt.metrics.vertBearingX >> 6
if line_height + char_offset_y > max_height:
line_text_list.append(line_str)
line_height_list.append(line_height)
# line_width_list.append(line_width_left + line_width_right)
line_str = ""
line_height = 0
line_width_left = 0
line_width_right = 0
line_height += char_offset_y
line_str += cdpt
line_width_left = max(line_width_left, abs(char_bearing_x))
line_width_right = max(line_width_right, char_width - abs(char_bearing_x))
# last char
line_text_list.append(line_str)
line_height_list.append(line_height)
# line_width_list.append(line_width_left + line_width_right)
# box_calc_x = sum(line_width_list) + (len(line_width_list) - 1) * spacing_x
# box_calc_y = max(line_height_list)
return line_text_list, line_height_list
def put_char_vertical(font_size: int, cdpt: str, pen_l: Tuple[int, int], canvas_text: np.ndarray, canvas_border: np.ndarray, border_size: int):
pen = pen_l.copy()
is_pun = is_punctuation(cdpt)
cdpt, rot_degree = CJK_Compatibility_Forms_translate(cdpt, 1)
slot = get_char_glyph(cdpt, font_size, 1)
bitmap = slot.bitmap
if bitmap.rows * bitmap.width == 0 or len(bitmap.buffer) != bitmap.rows * bitmap.width:
char_offset_y = slot.metrics.vertBearingY >> 6
return char_offset_y
char_offset_y = slot.metrics.vertAdvance >> 6
bitmap_char = np.array(bitmap.buffer, dtype = np.uint8).reshape((bitmap.rows,bitmap.width))
pen[0] += slot.metrics.vertBearingX >> 6
pen[1] += slot.metrics.vertBearingY >> 6
canvas_text[pen[1]:pen[1]+bitmap.rows, pen[0]:pen[0]+bitmap.width] = bitmap_char
#print(pen_l, pen, slot.metrics.vertBearingX >> 6, bitmap.width)
#border
if border_size > 0:
pen_border = (max(pen[0] - border_size, 0), max(pen[1] - border_size, 0))
#slot_border =
glyph_border = get_char_border(cdpt, font_size, 1)
stroker = freetype.Stroker()
stroker.set(64 * max(int(0.07 * font_size), 1), freetype.FT_STROKER_LINEJOIN_ROUND, freetype.FT_STROKER_LINEJOIN_ROUND, 0)
glyph_border.stroke(stroker, destroy=True)
blyph = glyph_border.to_bitmap(freetype.FT_RENDER_MODE_NORMAL, freetype.Vector(0,0), True)
bitmap_b = blyph.bitmap
bitmap_border = np.array(bitmap_b.buffer, dtype = np.uint8).reshape(bitmap_b.rows, bitmap_b.width)
canvas_border[pen_border[1]:pen_border[1]+bitmap_b.rows, pen_border[0]:pen_border[0]+bitmap_b.width] = cv2.add(canvas_border[pen_border[1]:pen_border[1]+bitmap_b.rows, pen_border[0]:pen_border[0]+bitmap_b.width], bitmap_border)
return char_offset_y
def put_text_vertical(font_size: int, text: str, h: int, alignment: str, fg: Tuple[int, int, int], bg: Optional[Tuple[int, int, int]], line_spacing: int):
text = compact_special_symbols(text)
if not text :
return
bg_size = int(max(font_size * 0.07, 1)) if bg is not None else 0
spacing_x = int(font_size * (line_spacing or 0.2))
# make large canvas
num_char_y = h // font_size
num_char_x = len(text) // num_char_y + 1
canvas_x = font_size * num_char_x + spacing_x * (num_char_x - 1) + (font_size + bg_size) * 2
canvas_y = font_size * num_char_y + (font_size + bg_size) * 2
line_text_list, line_height_list = calc_vertical(font_size, text, h)
# print(line_text_list, line_height_list)
canvas_text = np.zeros((canvas_y, canvas_x), dtype=np.uint8)
canvas_border = canvas_text.copy()
# pen (x, y)
pen_orig = [canvas_text.shape[1] - (font_size + bg_size), font_size + bg_size]
# write stuff
for line_text, line_height in zip(line_text_list, line_height_list):
pen_line = pen_orig.copy()
if alignment == 'center':
pen_line[1] += (max(line_height_list) - line_height) // 2
elif alignment == 'right':
pen_line[1] += max(line_height_list) - line_height
for c in line_text:
offset_y = put_char_vertical(font_size, c, pen_line, canvas_text, canvas_border, border_size=bg_size)
pen_line[1] += offset_y
pen_orig[0] -= spacing_x + font_size
# colorize
canvas_border = np.clip(canvas_border, 0, 255)
line_box = add_color(canvas_text, fg, canvas_border, bg)
# rect
if bg is None :
x, y, w, h = cv2.boundingRect(canvas_text)
else :
x, y, w, h = cv2.boundingRect(canvas_border)
return line_box[y:y+h, x:x+w]
def select_hyphenator(lang: str):
lang = standardize_tag(lang)
if lang not in HYPHENATOR_LANGUAGES:
for avail_lang in reversed(HYPHENATOR_LANGUAGES):
if avail_lang.startswith(lang):
lang = avail_lang
break
else:
return None
try:
return Hyphenator(lang)
except Exception:
return None
# @functools.lru_cache(maxsize = 1024, typed = True)
def get_char_offset_x(font_size: int, cdpt: str):
c, rot_degree = CJK_Compatibility_Forms_translate(cdpt, 0)
glyph = get_char_glyph(c, font_size, 0)
bitmap = glyph.bitmap
# Extract length
if bitmap.rows * bitmap.width == 0 or len(bitmap.buffer) != bitmap.rows * bitmap.width:
# spaces, etc
char_offset_x = glyph.advance.x >> 6
else:
char_offset_x = glyph.metrics.horiAdvance >> 6
return char_offset_x
def get_string_width(font_size: int, text: str):
return sum([get_char_offset_x(font_size, c) for c in text])
def calc_horizontal(font_size: int, text: str, max_width: int, max_height: int, language: str = 'en_US', hyphenate: bool = True) -> Tuple[List[str], List[int]]:
"""
Splits up a string of text into lines. Returns list of lines and their widths.
Will go over max_height if too much text is present.
"""
max_width = max(max_width, 2 * font_size)
whitespace_offset_x = get_char_offset_x(font_size, ' ')
hyphen_offset_x = get_char_offset_x(font_size, '-')
# Split text into words and precalculate each word width
words = re.split(r'\s+', text)
word_widths = []
for i, word in enumerate(words):
word_widths.append(get_string_width(font_size, word))
# Try to increase width usage if a height overflow is unavoidable
while True:
max_lines = max_height // font_size + 1
expected_size = sum(word_widths) + max((len(word_widths) - 1) * whitespace_offset_x - (max_lines - 1) * hyphen_offset_x, 0)
max_size = max_width * max_lines
if max_size < expected_size:
multiplier = np.sqrt(expected_size / max_size)
max_width *= max(multiplier, 1.05)
max_height *= multiplier
else:
break
# Split words into syllables
syllables = []
hyphenator = select_hyphenator(language)
for i, word in enumerate(words):
new_syls = []
if hyphenator and len(word) <= 100:
try:
new_syls = hyphenator.syllables(word)
except Exception:
new_syls = []
if len(new_syls) == 0:
if len(word) <= 3:
new_syls = [word]
else:
new_syls = list(word)
# # Make sure no syllable goes over max_width
# for syl in syllables[-1]:
# w = get_string_width(font_size, syl)
# if w > max_width:
# max_width = w
# Split up syllables that are too large
normalized_syls = []
for syl in new_syls:
syl_width = get_string_width(font_size, syl)
if syl_width > max_width:
normalized_syls.extend(list(syl))
else:
normalized_syls.append(syl)
syllables.append(normalized_syls)
line_words_list = []
line_width_list = []
hyphenation_idx_list = []
line_words = []
line_width = 0
hyphenation_idx = 0
def break_line():
nonlocal line_words, line_width, hyphenation_idx
line_words_list.append(line_words)
line_width_list.append(line_width)
hyphenation_idx_list.append(hyphenation_idx)
line_words = []
line_width = 0
hyphenation_idx = 0
def get_present_syllables_range(line_idx, word_pos):
while word_pos < 0:
word_pos += len(line_words_list[line_idx])
word_idx = line_words_list[line_idx][word_pos]
syl_start_idx = 0
syl_end_idx = len(syllables[word_idx])
if line_idx > 0 and word_pos == 0 and line_words_list[line_idx - 1][-1] == word_idx:
syl_start_idx = hyphenation_idx_list[line_idx - 1]
if line_idx < len(line_words_list) - 1 and word_pos == len(line_words_list[line_idx]) - 1 \
and line_words_list[line_idx + 1][0] == word_idx:
syl_end_idx = hyphenation_idx_list[line_idx]
return syl_start_idx, syl_end_idx
def get_present_syllables(line_idx, word_pos):
syl_start_idx, syl_end_idx = get_present_syllables_range(line_idx, word_pos)
return syllables[line_words_list[line_idx][word_pos]][syl_start_idx:syl_end_idx]
# Step 1:
# Arrange words without hyphenating unless necessary
i = 0
while True:
if i >= len(words):
if line_width > 0:
break_line()
break
current_width = whitespace_offset_x if line_width > 0 else 0
if line_width + current_width + word_widths[i] <= max_width + hyphen_offset_x:
line_words.append(i)
line_width += current_width + word_widths[i]
i += 1
elif word_widths[i] > max_width:
# We know no syllable can be larger than max_width
j = 0
hyphenation_idx = 0
while j < len(syllables[i]):
syl = syllables[i][j]
syl_width = get_string_width(font_size, syl)
if line_width + current_width + syl_width <= max_width:
current_width += syl_width
j += 1
hyphenation_idx = j
else:
if hyphenation_idx > 0:
line_words.append(i)
line_width += current_width
current_width = 0
break_line()
line_words.append(i)
line_width += current_width
i += 1
else:
break_line()
# Step 2:
# Compare two adjacent lines and try to hyphenate backwards
# Avoid hyphenation if max_lines isn't fully used
if hyphenate and len(line_words_list) > max_lines:
line_idx = 0
while line_idx < len(line_words_list) - 1:
line_words1 = line_words_list[line_idx]
line_words2 = line_words_list[line_idx + 1]
left_space = max_width - line_width_list[line_idx]
# Move syllables from below line to above
first_word = True
while len(line_words2) != 0:
word_idx = line_words2[0]
# A bit messy but were basically trying to only use the syllables on the current line
if first_word and word_idx == line_words1[-1]:
syl_start_idx = hyphenation_idx_list[line_idx]
if line_idx < len(line_width_list) - 2 and word_idx == line_words_list[line_idx + 2][0]:
syl_end_idx = hyphenation_idx_list[line_idx + 1]
else:
syl_end_idx = len(syllables[word_idx])
else:
left_space -= whitespace_offset_x
syl_start_idx = 0
syl_end_idx = len(syllables[word_idx]) if len(line_words2) > 1 else hyphenation_idx_list[line_idx + 1]
first_word = False
current_width = 0
for i in range(syl_start_idx, syl_end_idx):
syl = syllables[word_idx][i]
syl_width = get_string_width(font_size, syl)
if left_space > current_width + syl_width:
current_width += syl_width
else:
# Splitting up word
if current_width > 0:
# We dont want very small splits
# if
left_space -= current_width
line_width_list[line_idx] = max_width - left_space
hyphenation_idx_list[line_idx] = i
line_words1.append(word_idx)
break
else:
# Whole word was brought to above line
left_space -= current_width
line_width_list[line_idx] = max_width - left_space
line_words1.append(word_idx)
line_words2.pop(0)
continue
break
if len(line_words2) == 0:
line_words_list.pop(line_idx + 1)
line_width_list.pop(line_idx + 1)
hyphenation_idx_list.pop(line_idx)
else:
line_idx += 1
# Step 3
# Move single char syllables on the left up and those on the right down
line_idx = 0
while line_idx < len(line_words_list) - 1:
line_words1 = line_words_list[line_idx]
line_words2 = line_words_list[line_idx + 1]
merged_word_idx = -1
if line_words1[-1] == line_words2[0]:
word1_text = ''.join(get_present_syllables(line_idx, -1))
word2_text = ''.join(get_present_syllables(line_idx + 1, 0))
word1_width = get_string_width(font_size, word1_text)
word2_width = get_string_width(font_size, word2_text)
if len(word2_text) == 1 or word2_width < font_size:
merged_word_idx = line_words1[-1]
line_words2.pop(0)
line_width_list[line_idx] += word2_width
line_width_list[line_idx + 1] -= word2_width + whitespace_offset_x
elif len(word1_text) == 1 or word1_width < font_size:
merged_word_idx = line_words1[-1]
line_words1.pop(-1)
line_width_list[line_idx] -= word1_width + whitespace_offset_x
line_width_list[line_idx + 1] += word1_width
if len(line_words1) == 0:
line_words_list.pop(line_idx)
line_width_list.pop(line_idx)
hyphenation_idx_list.pop(line_idx)
elif len(line_words2) == 0:
line_words_list.pop(line_idx + 1)
line_width_list.pop(line_idx + 1)
hyphenation_idx_list.pop(line_idx)
# We dont want all single letters to be merged
elif line_idx >= len(line_words_list) - 1 or line_words_list[line_idx + 1] != merged_word_idx:
line_idx += 1
# Step 4
# Assemble line_text_list
use_hyphen_chars = hyphenate and hyphenator and max_width > 1.5 * font_size and len(words) > 1
line_text_list = []
for i, line in enumerate(line_words_list):
line_text = ''
for j, word_idx in enumerate(line):
syl_start_idx, syl_end_idx = get_present_syllables_range(i, j)
current_syllables = syllables[word_idx][syl_start_idx:syl_end_idx]
line_text += ''.join(current_syllables)
if len(line_text) == 0:
continue
if j == 0 and i > 0 and line_text_list[-1][-1] == '-' and line_text[0] == '-':
line_text = line_text[1:]
line_width_list[i] -= hyphen_offset_x
if j < len(line) - 1 and len(line_text) > 0:
line_text += ' '
elif use_hyphen_chars and syl_end_idx != len(syllables[word_idx]) and len(words[word_idx]) > 3 and line_text[-1] != '-' \
and not (syl_end_idx < len(syllables[word_idx]) and not re.search(r'\w', syllables[word_idx][syl_end_idx][0])):
line_text += '-'
# hyphen_offset was ignored in previous steps
line_width_list[i] += hyphen_offset_x
# print(line_text, get_string_width(font_size, line_text), line_width_list[i])
# assert(line_width_list[i] == get_string_width(font_size, line_text))
# Shouldn't be needed but there is apparently still a bug somewhere (See #458)
line_width_list[i] = get_string_width(font_size, line_text)
line_text_list.append(line_text)
return line_text_list, line_width_list
def put_char_horizontal(font_size: int, cdpt: str, pen_l: Tuple[int, int], canvas_text: np.ndarray, canvas_border: np.ndarray, border_size: int):
pen = pen_l.copy()
cdpt, rot_degree = CJK_Compatibility_Forms_translate(cdpt, 0)
slot = get_char_glyph(cdpt, font_size, 0)
bitmap = slot.bitmap
char_offset_x = slot.advance.x >> 6
bitmap_char = np.array(bitmap.buffer, dtype = np.uint8).reshape((bitmap.rows,bitmap.width))
if bitmap.rows * bitmap.width == 0 or len(bitmap.buffer) != bitmap.rows * bitmap.width:
return char_offset_x
pen[0] += slot.bitmap_left
pen[1] = max(pen[1] - slot.bitmap_top, 0)
canvas_text[pen[1]:pen[1]+bitmap.rows, pen[0]:pen[0]+bitmap.width] = bitmap_char
#print(pen_l, pen, slot.metrics.vertBearingX >> 6, bitmap.width)
#border
if border_size > 0:
pen_border = (max(pen[0] - border_size, 0), max(pen[1] - border_size, 0))
#slot_border =
glyph_border = get_char_border(cdpt, font_size, 1)
stroker = freetype.Stroker()
stroker.set(64 * max(int(0.07 * font_size), 1), freetype.FT_STROKER_LINEJOIN_ROUND, freetype.FT_STROKER_LINEJOIN_ROUND, 0)
glyph_border.stroke(stroker, destroy=True)
blyph = glyph_border.to_bitmap(freetype.FT_RENDER_MODE_NORMAL, freetype.Vector(0,0), True)
bitmap_b = blyph.bitmap
bitmap_border = np.array(bitmap_b.buffer, dtype = np.uint8).reshape(bitmap_b.rows,bitmap_b.width)
canvas_border[pen_border[1]:pen_border[1]+bitmap_b.rows, pen_border[0]:pen_border[0]+bitmap_b.width] = cv2.add(canvas_border[pen_border[1]:pen_border[1]+bitmap_b.rows, pen_border[0]:pen_border[0]+bitmap_b.width], bitmap_border)
return char_offset_x
def put_text_horizontal(font_size: int, text: str, width: int, height: int, alignment: str,
reversed_direction: bool, fg: Tuple[int, int, int], bg: Tuple[int, int, int],
lang: str = 'en_US', hyphenate: bool = True, line_spacing: int = 0):
text = compact_special_symbols(text)
if not text :
return
bg_size = int(max(font_size * 0.07, 1)) if bg is not None else 0
spacing_y = int(font_size * (line_spacing or 0.01))
# calc
# print(width)
line_text_list, line_width_list = calc_horizontal(font_size, text, width, height, lang, hyphenate)
# print(line_text_list, line_width_list)
# make large canvas
canvas_w = max(line_width_list) + (font_size + bg_size) * 2
canvas_h = font_size * len(line_width_list) + spacing_y * (len(line_width_list) - 1) + (font_size + bg_size) * 2
canvas_text = np.zeros((canvas_h, canvas_w), dtype=np.uint8)
canvas_border = canvas_text.copy()
# pen (x, y)
pen_orig = [font_size + bg_size, font_size + bg_size]
if reversed_direction:
# right to left languages have to be rendered in the correct order (starting from right)
# so that the white outline of characters dont go over black parts of neighbouring characters
pen_orig[0] = canvas_w - bg_size - 10
# write stuff
for line_text, line_width in zip(line_text_list, line_width_list):
pen_line = pen_orig.copy()
if alignment == 'center':
pen_line[0] += (max(line_width_list) - line_width) // 2 * (-1 if reversed_direction else 1)
elif alignment == 'right' and not reversed_direction:
pen_line[0] += max(line_width_list) - line_width
elif alignment == 'left' and reversed_direction:
pen_line[0] -= max(line_width_list) - line_width
pen_line[0] = max(line_width, pen_line[0])
# print((line_width, pen_line[0], canvas_w))
# print(0, pen_line, line_text)
for c in line_text:
if reversed_direction:
cdpt, rot_degree = CJK_Compatibility_Forms_translate(c, 0)
glyph = get_char_glyph(cdpt, font_size, 0)
offset_x = glyph.metrics.horiAdvance >> 6
pen_line[0] -= offset_x
# print(1, pen_line, c)
offset_x = put_char_horizontal(font_size, c, pen_line, canvas_text, canvas_border, border_size=bg_size)
if not reversed_direction:
pen_line[0] += offset_x
pen_orig[1] += spacing_y + font_size
# colorize
canvas_border = np.clip(canvas_border, 0, 255)
line_box = add_color(canvas_text, fg, canvas_border, bg)
# rect
if bg is None :
x, y, w, h = cv2.boundingRect(canvas_text)
else :
x, y, w, h = cv2.boundingRect(canvas_border)
return line_box[y:y+height, x:x+width]
# def put_text(img: np.ndarray, text: str, line_count: int, x: int, y: int, w: int, h: int, fg: Tuple[int, int, int], bg: Optional[Tuple[int, int, int]]):
# pass
def test():
#canvas = put_text_vertical(64, 1.0, '因为不同‼ [这"真的是普]通的》肉!那个“姑娘”的恶作剧!是吗?咲夜⁉。', 700, (0, 0, 0), (255, 128, 128))
canvas = put_text_horizontal(64, 1.0, '因为不同‼ [这"真的是普]通的》肉!那个“姑娘”的恶作剧!是吗?咲夜⁉', 400, (0, 0, 0), (255, 128, 128))
cv2.imwrite('text_render_combined.png', canvas)
if __name__ == '__main__':
test()