diff --git a/.gitignore b/.gitignore index a0d1c9a..7f99b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ __pycache__/ # mypy .mypy_cache + +.pypirc diff --git a/handright/__init__.py b/handright/__init__.py index 11e2d67..0401fec 100644 --- a/handright/__init__.py +++ b/handright/__init__.py @@ -26,7 +26,7 @@ from handright._exceptions import Error, LayoutError, BackgroundTooLargeError from handright._template import Template, Feature -__version__ = "8.2.0" +__version__ = "8.7.0" __all__ = ( "handwrite", @@ -34,5 +34,5 @@ "Feature", "Error", "LayoutError", - "BackgroundTooLargeError" + "BackgroundTooLargeError", ) diff --git a/handright/_core.py b/handright/_core.py index 0a0cb77..67122c0 100644 --- a/handright/_core.py +++ b/handright/_core.py @@ -22,10 +22,10 @@ def handwrite( - text: str, - template: Union[Template, Sequence[Template]], - seed: Hashable = None, - mapper: Callable[[Callable, Iterable], Iterable] = map, + text: str, + template: Union[Template, Sequence[Template]], + seed: Hashable = None, + mapper: Callable[[Callable, Iterable], Iterable] = map, ) -> Iterable[PIL.Image.Image]: """Handwrite `text` with the configurations in `template`, and return an Iterable of Pillow's Images. @@ -72,15 +72,17 @@ def _preprocess_text(text: str) -> str: def _check_template(page, tpl) -> None: - if page.height() < (tpl.get_top_margin() + tpl.get_line_spacing() - + tpl.get_bottom_margin()): + if page.height() < ( + tpl.get_top_margin() + tpl.get_line_spacing() + tpl.get_bottom_margin() + ): msg = "for (height < top_margin + line_spacing + bottom_margin)" raise LayoutError(msg) if tpl.get_font().size > tpl.get_line_spacing(): msg = "for (font.size > line_spacing)" raise LayoutError(msg) - if page.width() < (tpl.get_left_margin() + tpl.get_font().size - + tpl.get_right_margin()): + if page.width() < ( + tpl.get_left_margin() + tpl.get_font().size + tpl.get_right_margin() + ): msg = "for (width < left_margin + font.size + right_margin)" raise LayoutError(msg) if tpl.get_word_spacing() <= -tpl.get_font().size // 2: @@ -88,9 +90,7 @@ def _check_template(page, tpl) -> None: raise LayoutError(msg) -def _draw_page( - page, text, start: int, tpl: Template, rand: random.Random -) -> int: +def _draw_page(page, text, start: int, tpl: Template, rand: random.Random) -> int: _check_template(page, tpl) width = page.width() @@ -114,11 +114,9 @@ def _draw_page( if start == len(text): return start break - if (x > width - right_margin - 2 * font_size - and text[start] in start_chars): + if x > width - right_margin - 2 * font_size and text[start] in start_chars: break - if (x > width - right_margin - font_size - and text[start] not in end_chars): + if x > width - right_margin - font_size 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) @@ -131,25 +129,19 @@ def _draw_page( return start -def _flow_layout( - draw, x, y, char, tpl: Template, rand: random.Random -) -> float: +def _flow_layout(draw, x, y, char, tpl: Template, rand: random.Random) -> float: xy = (round(x), round(gauss(rand, y, tpl.get_line_spacing_sigma()))) font = _get_font(tpl, rand) offset = _draw_char(draw, char, xy, font) - x += gauss( - rand, - tpl.get_word_spacing() + offset, - tpl.get_word_spacing_sigma() - ) + x += gauss(rand, tpl.get_word_spacing() + offset, tpl.get_word_spacing_sigma()) return x -def _grid_layout( - draw, x, y, char, tpl: Template, rand: random.Random -) -> float: - xy = (round(gauss(rand, x, tpl.get_word_spacing_sigma())), - round(gauss(rand, y, tpl.get_line_spacing_sigma()))) +def _grid_layout(draw, x, y, char, tpl: Template, rand: random.Random) -> 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) _ = _draw_char(draw, char, xy, font) x += tpl.get_word_spacing() + tpl.get_font().size @@ -158,9 +150,7 @@ def _grid_layout( def _get_font(tpl: Template, rand: random.Random): font = tpl.get_font() - actual_font_size = max(round( - gauss(rand, font.size, tpl.get_font_size_sigma()) - ), 0) + actual_font_size = max(round(gauss(rand, font.size, tpl.get_font_size_sigma())), 0) if actual_font_size != font.size: return font.font_variant(size=actual_font_size) return font @@ -230,10 +220,7 @@ def _extract_strokes(bitmap, bbox: Tuple[int, int, int, int]): _MAX_INT16_VALUE - 1 ) raise BackgroundTooLargeError(msg) - strokes = NumericOrderedSet( - _UNSIGNED_INT32_TYPECODE, - privileged=_STROKE_END - ) + strokes = NumericOrderedSet(_UNSIGNED_INT32_TYPECODE, privileged=_STROKE_END) for y in range(upper, lower): for x in range(left, right): if bitmap[x, y] and strokes.add(_xy(x, y)): @@ -243,12 +230,14 @@ def _extract_strokes(bitmap, bbox: Tuple[int, int, int, int]): def _extract_stroke( - bitmap, start: Tuple[int, int], strokes, bbox: Tuple[int, int, int, int] + bitmap, start: Tuple[int, int], strokes, bbox: Tuple[int, int, int, int] ) -> None: """Helper function of _extract_strokes() which uses depth first search to find the pixels of a glyph.""" left, upper, right, lower = bbox - stack = [start, ] + stack = [ + start, + ] while stack: x, y = stack.pop() if y - 1 >= upper and bitmap[x, y - 1] and strokes.add(_xy(x, y - 1)): @@ -286,35 +275,68 @@ def _draw_strokes(bitmap, strokes, tpl, rand) -> None: def _draw_stroke( - bitmap, - stroke: Sequence[Tuple[int, int]], - tpl: Template, - center: Tuple[float, float], - rand + bitmap, + stroke: Sequence[Tuple[int, int]], + tpl: Template, + center: Tuple[float, float], + rand, ) -> 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()) + + ink_depth_sigma = tpl.get_ink_depth_sigma() + original_fill = tpl.get_fill() + # 添加随机扰动 + ink_depth_rand = gauss(rand, 0, ink_depth_sigma) + if isinstance(original_fill, int): + # 如果 original_fill 是一个整数 + rand_fill = max(0, min(100, int(original_fill + ink_depth_rand))) + elif isinstance(original_fill, tuple): + if len(original_fill) == 3: + # 如果 original_fill 是一个三元组(假设是 RGB 值) + rand_fill = tuple( + max(0, min(255, int(channel + ink_depth_rand))) + for channel in original_fill + ) + elif len(original_fill) == 4: + # 如果 original_fill 是一个四元组(假设是 RGBA 值) + # 保持 Alpha 通道不变 + rand_fill = tuple( + ( + max(0, min(255, int(channel + ink_depth_rand))) + if i < 3 + else original_fill[3] + ) + for i, channel in enumerate(original_fill) + ) + + # 打印结果以验证 + # print('rand_fill', rand_fill) for x, y in stroke: new_x, new_y = _rotate(center, x, y, theta) new_x = round(new_x + dx) new_y = round(new_y + dy) width, height = tpl.get_size() if 0 <= new_x < width and 0 <= new_y < height: - bitmap[new_x, new_y] = tpl.get_fill() + bitmap[new_x, new_y] = rand_fill def _rotate( - center: Tuple[float, float], x: float, y: float, theta: float + center: Tuple[float, float], x: float, y: float, theta: float ) -> Tuple[float, float]: if theta == 0: return x, y - new_x = ((x - center[0]) * math.cos(theta) - + (y - center[1]) * math.sin(theta) - + center[0]) - new_y = ((y - center[1]) * math.cos(theta) - - (x - center[0]) * math.sin(theta) - + center[1]) + new_x = ( + (x - center[0]) * math.cos(theta) + + (y - center[1]) * math.sin(theta) + + center[0] + ) + new_y = ( + (y - center[1]) * math.cos(theta) + - (x - center[0]) * math.sin(theta) + + center[1] + ) return new_x, new_y diff --git a/handright/_template.py b/handright/_template.py index 7d97697..a7755b5 100644 --- a/handright/_template.py +++ b/handright/_template.py @@ -32,6 +32,7 @@ class Template(object): "_perturb_x_sigma", "_perturb_y_sigma", "_perturb_theta_sigma", + '_ink_depth_sigma', "_features", ) @@ -68,6 +69,7 @@ def __init__( perturb_x_sigma: Optional[float] = None, perturb_y_sigma: Optional[float] = None, perturb_theta_sigma: float = _DEFAULT_PERTURB_THETA_SIGMA, + ink_depth_sigma: Optional[float]=None, features: Set = _DEFAULT_FEATURES, ): """Note that, all the Integer parameters are in pixels. @@ -117,6 +119,7 @@ def __init__( 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_ink_depth_sigma(ink_depth_sigma) self.set_features(features) def __eq__(self, other) -> bool: @@ -237,6 +240,14 @@ def set_perturb_theta_sigma( ) -> None: self._perturb_theta_sigma = perturb_theta_sigma + def set_ink_depth_sigma( + self, ink_depth_sigma: Optional[float] = None + ) -> None: + if ink_depth_sigma is None: + self._ink_depth_sigma = self._font.size / 1.5 + else: + self._ink_depth_sigma = ink_depth_sigma + def get_background(self) -> PIL.Image.Image: return self._background @@ -291,6 +302,9 @@ def get_perturb_y_sigma(self) -> float: def get_perturb_theta_sigma(self) -> float: return self._perturb_theta_sigma + def get_ink_depth_sigma(self): + return self._ink_depth_sigma + def get_size(self) -> Tuple[int, int]: return self.get_background().size diff --git a/tests/backgrounds/even-odd-letter/icon.png b/tests/backgrounds/even-odd-letter/icon.png new file mode 100644 index 0000000..f4ba170 Binary files /dev/null and b/tests/backgrounds/even-odd-letter/icon.png differ diff --git a/tests/watch.py b/tests/watch.py index 2050757..fc685b0 100644 --- a/tests/watch.py +++ b/tests/watch.py @@ -11,17 +11,17 @@ def main(): print("Test by naked eyes:") - _watch_gird_layout() - _like_it() + # _watch_gird_layout() + # _like_it() _watch_flow_layout() - _like_it() + # _like_it() def _watch_flow_layout(): path = "backgrounds/even-odd-letter/" - image1 = PIL.Image.open(abs_path(path + "村庄信笺纸.jpg")) + image1 = PIL.Image.open(abs_path(path + "icon.png")) image2 = PIL.Image.open(abs_path(path + "树信笺纸.jpg")) - assert image1.mode == "RGB" + # assert image1.mode == "RGB" assert image2.mode == "RGB" template1 = Template(