diff --git a/src/ilo/cog_utils.py b/src/ilo/cog_utils.py index a29836c..6adcb7f 100644 --- a/src/ilo/cog_utils.py +++ b/src/ilo/cog_utils.py @@ -52,10 +52,11 @@ def is_valid_color(value: str) -> bool: return True return False - def is_valid_bgstyle(style: BgStyle) -> bool: return style in VALID_STYLES +def is_valid_line_width(value: int) -> bool: + return(value > 1 and value < 10000) def load_file(file_path, file_name) -> List[str] | Dict: path = Path(file_path).parent / file_name diff --git a/src/ilo/cogs/sitelen/cog.py b/src/ilo/cogs/sitelen/cog.py index fd0f742..95613a6 100644 --- a/src/ilo/cogs/sitelen/cog.py +++ b/src/ilo/cogs/sitelen/cog.py @@ -78,6 +78,22 @@ def __init__(self, bot: Bot): validation=lambda x: isinstance(x, bool), ) ) + preferences.register( + Template( + locale=self.locale, + name="linewrap", + default=data.DEFAULT_LINE_WRAP, + validation=lambda x: isinstance(x, bool), + ) + ) + preferences.register( + Template( + locale=self.locale, + name="linewidth", + default=data.DEFAULT_LINE_WIDTH, + validation=utils.is_valid_line_width, + ) + ) locale = utils.Locale(__file__) @@ -91,6 +107,9 @@ def __init__(self, bot: Bot): @locale.option("sp-hide") # @locale.option("sp-proxy") @locale.option("sp-convert") + @locale.option("sp-linewrap") + @locale.option("sp-linewidth") + async def slash_sp( self, ctx: ApplicationContext, @@ -103,6 +122,8 @@ async def slash_sp( hide: bool = False, # proxy: bool = False, convert: bool = False, + linewrap: bool = False, + linewidth: int = 0 ): await self.sp( ctx, @@ -115,6 +136,8 @@ async def slash_sp( hide, # proxy, convert, + linewrap, + linewidth ) @locale.command("sitelenpona") @@ -139,6 +162,8 @@ async def slash_sitelenpona( hide: bool = False, # proxy: bool = False, convert: bool = False, + linewrap: bool = False, + linewidth: int = 0 ): await self.sp( ctx, @@ -151,6 +176,8 @@ async def slash_sitelenpona( hide, # proxy, convert, + linewrap, + linewidth ) @locale.command("ss") @@ -165,12 +192,15 @@ async def slash_ss( self, ctx: ApplicationContext, text: str, + font: str = "", fontsize: int = 0, color: str = "", bgstyle: str = "", spoiler: bool = False, hide: bool = False, # proxy: bool = False, + linewrap: bool = False, + linewidth: int = 0 ): await self.sp( ctx, @@ -182,6 +212,9 @@ async def slash_ss( spoiler, hide, # proxy, + False, + linewrap, + linewidth ) @locale.command("sitelensitelen") @@ -196,12 +229,15 @@ async def slash_sitelensitelen( self, ctx: ApplicationContext, text: str, + font: str = "", fontsize: int = 0, color: str = "", bgstyle: str = "", spoiler: bool = False, hide: bool = False, # proxy: bool = False, + linewrap: bool = False, + linewidth: int = 0 ): await self.sp( ctx, @@ -214,6 +250,8 @@ async def slash_sitelensitelen( hide, # proxy, False, + linewrap, + linewidth ) async def make_sp_reply( @@ -226,6 +264,8 @@ async def make_sp_reply( bgstyle: str = "", spoiler: bool = False, convert: bool = False, + linewrap: bool = False, + linewidth: int = 0 ) -> tuple[File, list[str]]: user_id = str(ctx.author.id) @@ -236,6 +276,8 @@ async def make_sp_reply( fontsize = await utils.handle_pref_error(ctx, user_id, "fontsize", fontsize) color = await utils.handle_pref_error(ctx, user_id, "color", color) bgstyle = await utils.handle_pref_error(ctx, user_id, "bgstyle", bgstyle) + linewrap = await utils.handle_pref_error(ctx, user_id, "linewrap", linewrap) + linewidth = await utils.handle_pref_error(ctx, user_id, "linewidth", linewidth) # TODO: get channel name or user display name? # we can't stop them from getting smooshed in the render process... text, refs = rm_refs(text) @@ -249,7 +291,7 @@ async def make_sp_reply( text = unescape_newline(text) # desktop alt text has no newlines image = io.BytesIO( - sitelen.display(text, font, fontsize, utils.rgb_tuple(color), bgstyle) + sitelen.display(text, font, fontsize, utils.rgb_tuple(color), bgstyle, linewrap, linewidth) ) filename = text_to_filename(text) + ".png" file = File( @@ -274,6 +316,8 @@ async def sp( spoiler: bool = False, hide: bool = False, convert: bool = False, + linewrap: bool = False, + linewidth: int = 0 ): user_id = str(ctx.author.id) proxy = await utils.handle_pref_error(ctx, user_id, "proxy", False) @@ -289,6 +333,8 @@ async def sp( bgstyle, spoiler, convert, + linewrap, + linewidth ) kwargs["file"] = file diff --git a/src/ilo/cogs/sitelen/locale.json b/src/ilo/cogs/sitelen/locale.json index faff2a9..31ccf8a 100644 --- a/src/ilo/cogs/sitelen/locale.json +++ b/src/ilo/cogs/sitelen/locale.json @@ -9,6 +9,8 @@ "sp-bgstyle": "Select background or outline for your background style.", "sp-convert": "Whether or not to convert text from and to ucsur.", "sp-proxy": "Whether to send an image via the bot or proxying you.", + "sp-linewrap": "Whether to wrap text when image becomes wider than specified line width", + "sp-linewidth": "The width in pixels after which a line gets wrapped if line wrap is enabled", "sitelenpona": "Get sitelen pona of a toki pona phrase", "sitelenpona-text": "The phrase you want to convert to sitelen pona.", @@ -20,6 +22,8 @@ "sitelenpona-bgstyle": "Select background or outline for your background style.", "sitelenpona-convert": "Whether or not to convert text from and to ucsur.", "sitelenpona-proxy": "Whether to send an image via the bot or proxying you.", + "sitelenpona-linewrap": "Whether to wrap text when image becomes wider than specified line width", + "sitelenpona-linewidth": "The width in pixels after which a line gets wrapped if line wrap is enabled", "ss": "Get the sitelen sitelen of a toki pona phrase", "ss-text": "The phrase you want to convert to sitelen sitelen.", @@ -29,6 +33,8 @@ "ss-hide": "Whether to show the image to only you.", "ss-bgstyle": "Select background or outline for your background style.", "ss-proxy": "Whether to send an image via the bot or proxying you.", + "ss-linewrap": "Whether to wrap text when image becomes wider than specified line width", + "ss-linewidth": "The width in pixels after which a line gets wrapped if line wrap is enabled", "sitelensitelen": "Get the sitelen sitelen of a toki pona phrase", "sitelensitelen-text": "The phrase you want to convert to sitelen sitelen.", @@ -38,6 +44,8 @@ "sitelensitelen-hide": "Whether to show the image to only you.", "sitelensitelen-bgstyle": "Select background or outline for your background style.", "sitelensitelen-proxy": "Whether to send an image via the bot or proxying you.", + "sitelensitelen-linewrap": "Whether to wrap text when image becomes wider than specified line width", + "sitelensitelen-linewidth": "The width in pixels after which a line gets wrapped if line wrap is enabled", "prefs-fontsize": "Set the font size that sitelen pona will be displayed as", "prefs-fontsize-fallback": "The fontsize ***{}*** is not in the range 14 to 500. Using the default ***{}***.", @@ -67,5 +75,17 @@ "prefs-proxy-fallback": "The proxy setting ***{}*** is invalid. Using the default ***{}***.", "prefs-proxy-pref-invalid": "The proxy setting ***{}*** is in your preferences but not valid. Using the default ***{}***.", "prefs-proxy-choice-invalid": "The proxy setting ***{}*** is invalid. Did not update preferences.", - "prefs-proxy-option": "Enable or disable proxying." + "prefs-proxy-option": "Enable or disable proxying.", + + "prefs-linewrap": "Set whether to wrap text when image becomes wider than specified line width", + "prefs-linewrap-fallback": "The line wrap setting ***{}*** is invalid. Using the default ***{}***.", + "prefs-linewrap-pref-invalid": "The line wrap setting ***{}*** is in your preferences but not valid. Using the default ***{}***.", + "prefs-linewrap-choice-invalid": "The line wrap setting ***{}*** is invalid. Did not update preferences.", + "prefs-linewrap-option": "Enable or disable line wrapping.", + + "prefs-linewidth": "Set the width in pixels after which a line gets wrapped if line wrap is enabled", + "prefs-linewidth-fallback": "The line width setting ***{}*** is invalid. Using the default ***{}***.", + "prefs-linewidth-pref-invalid": "The line width setting ***{}*** is in your preferences but not valid. Using the default ***{}***.", + "prefs-linewidth-choice-invalid": "The line width setting ***{}*** is invalid. Did not update preferences.", + "prefs-linewidth-option": "Set the width in pixels after which a line gets wrapped if line wrap is enabled" } diff --git a/src/ilo/data.py b/src/ilo/data.py index 68f5e0c..af55de8 100644 --- a/src/ilo/data.py +++ b/src/ilo/data.py @@ -103,6 +103,8 @@ class UsageCategory(Enum): DEFAULT_LANGUAGE = "en" DEFAULT_USAGE_CATEGORY = "common" DEFAULT_PROXY = False +DEFAULT_LINE_WRAP = False +DEFAULT_LINE_WIDTH = 1000 # TODO: # - rework all functions here to use the api diff --git a/src/ilo/sitelen.py b/src/ilo/sitelen.py index 52e392b..7c785d6 100644 --- a/src/ilo/sitelen.py +++ b/src/ilo/sitelen.py @@ -1,9 +1,11 @@ import io import logging +import re from PIL import Image, ImageDraw, ImageFont from ilo.cog_utils import BgStyle, Color, ColorAlpha +from ilo.ucsur import chars LOG = logging.getLogger("ilo") @@ -11,7 +13,6 @@ WHITE: ColorAlpha = (0xF0, 0xF0, 0xF0, 0xFF) TRANSPARENT: ColorAlpha = (0, 0, 0, 0) - def subpixel_luminance(num: int) -> float: srgb = num / 255 if srgb <= 0.04045: @@ -70,9 +71,33 @@ def passes_aa(color: Color, bg_color: Color, font_size: int) -> bool: # return BLACK, BLACK # return WHITE, WHITE +def wrap_text(text:str, font: ImageFont.FreeTypeFont, line_width: int): + lines = text.splitlines() + wrapped_text = "" + for line in lines: + if font.getlength(line) < line_width: + wrapped_text += line+"\n" + continue + + split_line = re.sub(rf"({"|".join(chars)})", r"\g<0> ", line) # insert space after every uscur character + split_line = split_line.split(" ") + current_line_segment = "" + for word in split_line: + newline = current_line_segment + word + if font.getlength(newline) < line_width: + if current_line_segment != "" and word not in chars: # add space not first word on line and if not uscur char + current_line_segment += " " + current_line_segment += word + else: + wrapped_text += current_line_segment + "\n" + current_line_segment = word + wrapped_text += current_line_segment + "\n" + if wrapped_text[-1] == "\n": + wrapped_text = wrapped_text[:-1] + return(wrapped_text) # by jan Tepo -def display(text: str, font_path: str, font_size: int, color: Color, bgstyle: BgStyle): +def display(text: str, font_path: str, font_size: int, color: Color, bgstyle: BgStyle, linewrap:bool, linewidth:int): STROKE_WIDTH = round((font_size / 133) * 5) LINE_SPACING = round((font_size / 2.5)) PAD = round(font_size / 25) @@ -81,6 +106,10 @@ def display(text: str, font_path: str, font_size: int, color: Color, bgstyle: Bg bg_color = stroke_color if bgstyle == "background" else TRANSPARENT font = ImageFont.truetype(font_path, font_size) + + if linewrap: + text = wrap_text(text, font, linewidth) + d = ImageDraw.Draw(Image.new("RGBA", (0, 0), (0, 0, 0, 0))) x, y, w, h = d.multiline_textbbox( (0, 0), @@ -90,6 +119,10 @@ def display(text: str, font_path: str, font_size: int, color: Color, bgstyle: Bg stroke_width=STROKE_WIDTH, font_size=font_size, ) + + if linewrap: + w = max(w, linewidth) # mobile sometimes expands images to the screen's width, so this helps keep the characters displayi ng at a constant size + image = Image.new( mode="RGBA", size=(w + PAD, h + PAD + 25),