|
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: |
|
|
|
return CJK_V2H[cdpt], 0 |
|
else: |
|
return cdpt, 0 |
|
elif cdpt in CJK_H2V: |
|
if direction == 1: |
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, :]) |
|
|
|
|
|
|
|
return bg |
|
|
|
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): |
|
|
|
|
|
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) |
|
|
|
|
|
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 calc_vertical(font_size: int, text: str, max_height: int): |
|
line_text_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 |
|
|
|
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_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)) |
|
|
|
line_text_list.append(line_str) |
|
line_height_list.append(line_height) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if border_size > 0: |
|
pen_border = (max(pen[0] - border_size, 0), max(pen[1] - border_size, 0)) |
|
|
|
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)) |
|
|
|
|
|
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) |
|
|
|
|
|
canvas_text = np.zeros((canvas_y, canvas_x), dtype=np.uint8) |
|
canvas_border = canvas_text.copy() |
|
|
|
|
|
pen_orig = [canvas_text.shape[1] - (font_size + bg_size), font_size + bg_size] |
|
|
|
|
|
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 |
|
|
|
|
|
canvas_border = np.clip(canvas_border, 0, 255) |
|
line_box = add_color(canvas_text, fg, canvas_border, bg) |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
if bitmap.rows * bitmap.width == 0 or len(bitmap.buffer) != bitmap.rows * bitmap.width: |
|
|
|
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, '-') |
|
|
|
|
|
words = re.split(r'\s+', text) |
|
word_widths = [] |
|
for i, word in enumerate(words): |
|
word_widths.append(get_string_width(font_size, word)) |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
first_word = True |
|
while len(line_words2) != 0: |
|
word_idx = line_words2[0] |
|
|
|
|
|
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: |
|
|
|
if current_width > 0: |
|
|
|
|
|
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: |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
elif line_idx >= len(line_words_list) - 1 or line_words_list[line_idx + 1] != merged_word_idx: |
|
line_idx += 1 |
|
|
|
|
|
|
|
|
|
|
|
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 += '-' |
|
|
|
line_width_list[i] += hyphen_offset_x |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if border_size > 0: |
|
pen_border = (max(pen[0] - border_size, 0), max(pen[1] - border_size, 0)) |
|
|
|
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)) |
|
|
|
|
|
|
|
line_text_list, line_width_list = calc_horizontal(font_size, text, width, height, lang, hyphenate) |
|
|
|
|
|
|
|
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_orig = [font_size + bg_size, font_size + bg_size] |
|
if reversed_direction: |
|
|
|
|
|
pen_orig[0] = canvas_w - bg_size - 10 |
|
|
|
|
|
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]) |
|
|
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
canvas_border = np.clip(canvas_border, 0, 255) |
|
line_box = add_color(canvas_text, fg, canvas_border, bg) |
|
|
|
|
|
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 test(): |
|
|
|
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() |
|
|