Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 85 additions & 27 deletions handright/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,59 @@ def handwrite(
templates = (template,)
else:
templates = template
pages = _draft(text, templates, seed)
renderer = _Renderer(templates, seed)

total_pages = None
if any(t.get_all_text_sigma_sigma() > 1 for t in templates):
total_pages = _count_pages(text, templates, seed)

pages = _draft(text, templates, seed, total_pages)
renderer = _Renderer(templates, seed, total_pages)
return mapper(renderer, pages)


def _draft(text, templates, seed=None) -> Iterator[Page]:
def _count_pages(text, templates, seed=None) -> int:
text = _preprocess_text(text)
template_iter = itertools.cycle(templates)
num_iter = itertools.count()
rand = random.Random(x=seed)
start = 0
count = 0
while start < len(text):
template = next(template_iter)
page = Page(_INTERNAL_MODE, template.get_size(), _BLACK, next(num_iter))
page = DummyPage(template.get_size())
start = _draw_page(page, text, start, template, rand)
count += 1
return count


def _draft(text, templates, seed=None, total_pages=None) -> Iterator[Page]:
text = _preprocess_text(text)
template_iter = itertools.cycle(templates)
num_iter = itertools.count()
rand = random.Random(x=seed)
start = 0
while start < len(text):
template = next(template_iter)
num = next(num_iter)
page = Page(_INTERNAL_MODE, template.get_size(), _BLACK, num)
multiplier = _get_multiplier(num, total_pages, template.get_all_text_sigma_sigma())
start = _draw_page(page, text, start, template, rand, multiplier)
yield page


def _get_multiplier(page_num, total_pages, sigma_sigma):
if total_pages is None or total_pages <= 1 or sigma_sigma <= 1:
return 1.0

center = (total_pages - 1) / 2
sigma_curve = (total_pages - 1) / 6
if sigma_curve == 0:
return 1.0

delta = page_num - center
factor = math.exp(-(delta ** 2) / (2 * sigma_curve ** 2))
return 1 + (sigma_sigma - 1) * factor


def _preprocess_text(text: str) -> str:
return text.replace(_CRLF, _LF).replace(_CR, _LF)

Expand All @@ -89,7 +124,7 @@ def _check_template(page, tpl) -> None:


def _draw_page(
page, text, start: int, tpl: Template, rand: random.Random
page, text, start: int, tpl: Template, rand: random.Random, multiplier: float = 1.0
) -> int:
_check_template(page, tpl)

Expand All @@ -107,7 +142,7 @@ def _draw_page(
draw = page.draw()
y = top_margin + line_spacing - font_size
while y <= height - bottom_margin - font_size:
x = left_margin
x = gauss(rand, left_margin, tpl.get_left_margin_sigma() * multiplier)
while True:
if text[start] == _LF:
start += 1
Expand All @@ -121,9 +156,9 @@ def _draw_page(
and text[start] not in end_chars):
break
if Feature.GRID_LAYOUT in tpl.get_features():
x = _grid_layout(draw, x, y, text[start], tpl, rand)
x = _grid_layout(draw, x, y, text[start], tpl, rand, multiplier)
else:
x = _flow_layout(draw, x, y, text[start], tpl, rand)
x = _flow_layout(draw, x, y, text[start], tpl, rand, multiplier)
start += 1
if start == len(text):
return start
Expand All @@ -132,34 +167,34 @@ def _draw_page(


def _flow_layout(
draw, x, y, char, tpl: Template, rand: random.Random
draw, x, y, char, tpl: Template, rand: random.Random, multiplier: float = 1.0
) -> float:
xy = (round(x), round(gauss(rand, y, tpl.get_line_spacing_sigma())))
font = _get_font(tpl, rand)
xy = (round(x), round(gauss(rand, y, tpl.get_line_spacing_sigma() * multiplier)))
font = _get_font(tpl, rand, multiplier)
offset = _draw_char(draw, char, xy, font)
x += gauss(
rand,
tpl.get_word_spacing() + offset,
tpl.get_word_spacing_sigma()
tpl.get_word_spacing_sigma() * multiplier
)
return x


def _grid_layout(
draw, x, y, char, tpl: Template, rand: random.Random
draw, x, y, char, tpl: Template, rand: random.Random, multiplier: float = 1.0
) -> float:
xy = (round(gauss(rand, x, tpl.get_word_spacing_sigma())),
round(gauss(rand, y, tpl.get_line_spacing_sigma())))
font = _get_font(tpl, rand)
xy = (round(gauss(rand, x, tpl.get_word_spacing_sigma() * multiplier)),
round(gauss(rand, y, tpl.get_line_spacing_sigma() * multiplier)))
font = _get_font(tpl, rand, multiplier)
_ = _draw_char(draw, char, xy, font)
x += tpl.get_word_spacing() + tpl.get_font().size
return x


def _get_font(tpl: Template, rand: random.Random):
def _get_font(tpl: Template, rand: random.Random, multiplier: float = 1.0):
font = tpl.get_font()
actual_font_size = max(round(
gauss(rand, font.size, tpl.get_font_size_sigma())
gauss(rand, font.size, tpl.get_font_size_sigma() * multiplier)
), 0)
if actual_font_size != font.size:
return font.font_variant(size=actual_font_size)
Expand All @@ -182,14 +217,16 @@ class _Renderer(object):
"_templates",
"_rand",
"_hashed_seed",
"_total_pages",
)

def __init__(self, templates, seed=None) -> None:
def __init__(self, templates, seed=None, total_pages=None) -> None:
self._templates = _to_picklable(templates)
self._rand = random.Random()
self._hashed_seed = None
if seed is not None:
self._hashed_seed = hash(seed)
self._total_pages = total_pages

def __call__(self, page) -> PIL.Image.Image:
if self._hashed_seed is None:
Expand All @@ -206,7 +243,8 @@ def _perturb_and_merge(self, page) -> PIL.Image.Image:
if bbox is None:
return canvas
strokes = _extract_strokes(page.matrix(), bbox)
_draw_strokes(canvas.load(), strokes, template, self._rand)
multiplier = _get_multiplier(page.num, self._total_pages, template.get_all_text_sigma_sigma())
_draw_strokes(canvas.load(), strokes, template, self._rand, multiplier)
return canvas


Expand Down Expand Up @@ -261,7 +299,7 @@ def _extract_stroke(
stack.append((x + 1, y))


def _draw_strokes(bitmap, strokes, tpl, rand) -> None:
def _draw_strokes(bitmap, strokes, tpl, rand, multiplier=1.0) -> None:
stroke = []
min_x = _MAX_INT16_VALUE
min_y = _MAX_INT16_VALUE
Expand All @@ -270,7 +308,7 @@ def _draw_strokes(bitmap, strokes, tpl, rand) -> None:
for xy in strokes:
if xy == _STROKE_END:
center = ((min_x + max_x) / 2, (min_y + max_y) / 2)
_draw_stroke(bitmap, stroke, tpl, center, rand)
_draw_stroke(bitmap, stroke, tpl, center, rand, multiplier)
min_x = _MAX_INT16_VALUE
min_y = _MAX_INT16_VALUE
max_x = 0
Expand All @@ -290,11 +328,12 @@ def _draw_stroke(
stroke: Sequence[Tuple[int, int]],
tpl: Template,
center: Tuple[float, float],
rand
rand,
multiplier: float = 1.0
) -> None:
dx = gauss(rand, 0, tpl.get_perturb_x_sigma())
dy = gauss(rand, 0, tpl.get_perturb_y_sigma())
theta = gauss(rand, 0, tpl.get_perturb_theta_sigma())
dx = gauss(rand, 0, tpl.get_perturb_x_sigma() * multiplier)
dy = gauss(rand, 0, tpl.get_perturb_y_sigma() * multiplier)
theta = gauss(rand, 0, tpl.get_perturb_theta_sigma() * multiplier)
for x, y in stroke:
new_x, new_y = _rotate(center, x, y, theta)
new_x = round(new_x + dx)
Expand Down Expand Up @@ -324,3 +363,22 @@ def _xy(x: int, y: int) -> int:

def _x_y(xy: int) -> Tuple[int, int]:
return xy >> 16, xy & 0xFFFF


class DummyDraw(object):
def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs):
pass


class DummyPage(object):
def __init__(self, size):
self._size = size

def width(self):
return self._size[0]

def height(self):
return self._size[1]

def draw(self):
return DummyDraw()
44 changes: 39 additions & 5 deletions handright/_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ class Template(object):
"_line_spacing_sigma",
"_font_size_sigma",
"_word_spacing_sigma",
"_left_margin_sigma",
"_start_chars",
"_end_chars",
"_perturb_x_sigma",
"_perturb_y_sigma",
"_perturb_theta_sigma",
"_features",
"_all_text_sigma_sigma",
)

_DEFAULT_WORD_SPACING = 0
Expand All @@ -47,6 +49,8 @@ class Template(object):

_DEFAULT_PERTURB_THETA_SIGMA = 0.07

_DEFAULT_ALL_TEXT_SIGMA_SIGMA = 1.0

_DEFAULT_FEATURES = frozenset()

def __init__(
Expand All @@ -63,12 +67,14 @@ def __init__(
line_spacing_sigma: Optional[float] = None,
font_size_sigma: Optional[float] = None,
word_spacing_sigma: Optional[float] = None,
left_margin_sigma: Optional[float] = None,
start_chars: str = _DEFAULT_START_CHARS,
end_chars: str = _DEFAULT_END_CHARS,
perturb_x_sigma: Optional[float] = None,
perturb_y_sigma: Optional[float] = None,
perturb_theta_sigma: float = _DEFAULT_PERTURB_THETA_SIGMA,
features: Set = _DEFAULT_FEATURES,
all_text_sigma_sigma: float = _DEFAULT_ALL_TEXT_SIGMA_SIGMA,
):
"""Note that, all the Integer parameters are in pixels.

Expand All @@ -85,9 +91,9 @@ def __init__(
`word_spacing` can be less than `0`, but must be greater than
`-font.size // 2`.

`line_spacing_sigma`, `font_size_sigma` and `word_spacing_sigma` are the
sigmas of the gauss distributions of line spacing, font size and word
spacing, respectively.
`line_spacing_sigma`, `font_size_sigma`, `word_spacing_sigma` and
`left_margin_sigma` are the sigmas of the gauss distributions of line
spacing, font size, word spacing and left margin, respectively.

`start_chars` is the collection of Chars which should not be placed in
the end of a line. `end_chars`, by contrast, is the collection of Chars
Expand All @@ -97,6 +103,9 @@ def __init__(
sigmas of the gauss distributions of the horizontal position, the
vertical position and the rotation of strokes, respectively.

`all_text_sigma_sigma` is the overall sigma multiplier for all
perturbation parameters. It must be greater than or equal to 1.

**EXPERIMENT**
Use `features` to turn on the extra features, see `handright.Feature`.
"""
Expand All @@ -112,12 +121,14 @@ def __init__(
self.set_line_spacing_sigma(line_spacing_sigma)
self.set_font_size_sigma(font_size_sigma)
self.set_word_spacing_sigma(word_spacing_sigma)
self.set_left_margin_sigma(left_margin_sigma)
self.set_start_chars(start_chars)
self.set_end_chars(end_chars)
self.set_perturb_x_sigma(perturb_x_sigma)
self.set_perturb_y_sigma(perturb_y_sigma)
self.set_perturb_theta_sigma(perturb_theta_sigma)
self.set_features(features)
self.set_all_text_sigma_sigma(all_text_sigma_sigma)

def __eq__(self, other) -> bool:
return (isinstance(other, Template)
Expand All @@ -134,11 +145,13 @@ def __eq__(self, other) -> bool:
and self._word_spacing == other._word_spacing
and self._features == other._features
and self._word_spacing_sigma == other._word_spacing_sigma
and self._left_margin_sigma == other._left_margin_sigma
and self._start_chars == other._start_chars
and self._end_chars == other._end_chars
and self._perturb_x_sigma == other._perturb_x_sigma
and self._perturb_y_sigma == other._perturb_y_sigma
and self._perturb_theta_sigma == other._perturb_theta_sigma)
and self._perturb_theta_sigma == other._perturb_theta_sigma
and self._all_text_sigma_sigma == other._all_text_sigma_sigma)

def set_background(self, background: PIL.Image.Image) -> None:
self._background = background
Expand Down Expand Up @@ -186,6 +199,11 @@ def set_word_spacing(
def set_features(self, features: Set = _DEFAULT_FEATURES) -> None:
self._features = features

def set_all_text_sigma_sigma(
self, all_text_sigma_sigma: float = _DEFAULT_ALL_TEXT_SIGMA_SIGMA
) -> None:
self._all_text_sigma_sigma = all_text_sigma_sigma

def set_line_spacing_sigma(
self, line_spacing_sigma: Optional[float] = None
) -> None:
Expand All @@ -210,6 +228,14 @@ def set_word_spacing_sigma(
else:
self._word_spacing_sigma = word_spacing_sigma

def set_left_margin_sigma(
self, left_margin_sigma: Optional[float] = None
) -> None:
if left_margin_sigma is None:
self._left_margin_sigma = self._font.size / 32
else:
self._left_margin_sigma = left_margin_sigma

def set_start_chars(self, start_chars: str = _DEFAULT_START_CHARS) -> None:
self._start_chars = start_chars

Expand Down Expand Up @@ -267,6 +293,9 @@ def get_word_spacing(self) -> int:
def get_features(self) -> Set:
return self._features

def get_all_text_sigma_sigma(self) -> float:
return self._all_text_sigma_sigma

def get_line_spacing_sigma(self) -> float:
return self._line_spacing_sigma

Expand All @@ -276,6 +305,9 @@ def get_font_size_sigma(self) -> float:
def get_word_spacing_sigma(self) -> float:
return self._word_spacing_sigma

def get_left_margin_sigma(self) -> float:
return self._left_margin_sigma

def get_start_chars(self) -> str:
return self._start_chars

Expand Down Expand Up @@ -316,11 +348,13 @@ def __repr__(self) -> str:
"line_spacing_sigma={self._line_spacing_sigma}, "
"font_size_sigma={self._font_size_sigma}, "
"word_spacing_sigma={self._word_spacing_sigma}, "
"left_margin_sigma={self._left_margin_sigma}, "
"start_chars={self._start_chars}, "
"end_chars={self._end_chars}, "
"perturb_x_sigma={self._perturb_x_sigma}, "
"perturb_y_sigma={self._perturb_y_sigma}, "
"perturb_theta_sigma={self._perturb_theta_sigma})"
"perturb_theta_sigma={self._perturb_theta_sigma}, "
"all_text_sigma_sigma={self._all_text_sigma_sigma})"
).format(class_name=class_name, self=self)


Expand Down