diff --git a/tagstudio/src/core/constants.py b/tagstudio/src/core/constants.py index 6f67bb2e0..2bd1fc379 100644 --- a/tagstudio/src/core/constants.py +++ b/tagstudio/src/core/constants.py @@ -7,6 +7,11 @@ COLLAGE_FOLDER_NAME: str = "collages" LIBRARY_FILENAME: str = "ts_library.json" +FONT_SAMPLE_TEXT: str = ( + """ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?@$%(){}[]""" +) +FONT_SAMPLE_SIZES: list[int] = [10, 15, 20] + # TODO: Turn this whitelist into a user-configurable blacklist. IMAGE_TYPES: list[str] = [ ".png", @@ -142,6 +147,7 @@ ] PROGRAM_TYPES: list[str] = [".exe", ".app"] SHORTCUT_TYPES: list[str] = [".lnk", ".desktop", ".url"] +FONT_TYPES: list[str] = [".ttf", ".otf", ".woff", ".woff2", ".ttc"] ALL_FILE_TYPES: list[str] = ( IMAGE_TYPES @@ -153,6 +159,7 @@ + ARCHIVE_TYPES + PROGRAM_TYPES + SHORTCUT_TYPES + + FONT_TYPES ) BOX_FIELDS = ["tag_box", "text_box"] diff --git a/tagstudio/src/qt/helpers/text_wrapper.py b/tagstudio/src/qt/helpers/text_wrapper.py new file mode 100644 index 000000000..64bd4616d --- /dev/null +++ b/tagstudio/src/qt/helpers/text_wrapper.py @@ -0,0 +1,51 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +from PIL import Image, ImageDraw, ImageFont + + +def wrap_line( # type: ignore + text: str, + font: ImageFont.ImageFont, + width: int = 256, + draw: ImageDraw.ImageDraw = None, +) -> int: + """ + Takes in a single line and returns the index it should be broken up at but + it only splits one Time + """ + if draw is None: + bg = Image.new("RGB", (width, width), color="#1e1e1e") + draw = ImageDraw.Draw(bg) + if draw.textlength(text, font=font) > width: + for i in range( + int(len(text) / int(draw.textlength(text, font=font)) * width) - 2, + 0, + -1, + ): + if draw.textlength(text[:i], font=font) < width: + return i + else: + return -1 + + +def wrap_full_text( + text: str, + font: ImageFont.ImageFont, + width: int = 256, + draw: ImageDraw.ImageDraw = None, +) -> str: + """ + Takes in a string and breaks it up to fit in the canvas given accounts for kerning and font size etc. + """ + lines = [] + i = 0 + last_i = 0 + while wrap_line(text[i:], font=font, width=width, draw=draw) > 0: + i = wrap_line(text[i:], font=font, width=width, draw=draw) + last_i + lines.append(text[last_i:i]) + last_i = i + lines.append(text[last_i:]) + text_wrapped = "\n".join(lines) + return text_wrapped diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 209e8df71..6892c7f5e 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -10,7 +10,7 @@ import cv2 import rawpy -from PIL import Image, UnidentifiedImageError +from PIL import Image, UnidentifiedImageError, ImageFont from PIL.Image import DecompressionBombError from PySide6.QtCore import QModelIndex, Signal, Qt, QSize from PySide6.QtGui import QResizeEvent, QAction @@ -30,7 +30,13 @@ from src.core.enums import SettingItems, Theme from src.core.library import Entry, ItemType, Library -from src.core.constants import VIDEO_TYPES, IMAGE_TYPES, RAW_IMAGE_TYPES, TS_FOLDER_NAME +from src.core.constants import ( + VIDEO_TYPES, + IMAGE_TYPES, + RAW_IMAGE_TYPES, + TS_FOLDER_NAME, + FONT_TYPES, +) from src.qt.helpers.file_opener import FileOpenerLabel, FileOpenerHelper, open_file from src.qt.modals.add_field import AddFieldModal from src.qt.widgets.thumb_renderer import ThumbRenderer @@ -559,6 +565,11 @@ def update_widgets(self): self.dimensions_label.setText( f"{filepath.suffix.upper()[1:]} • {format_size(filepath.stat().st_size)}\n{image.width} x {image.height} px" ) + elif filepath.suffix.lower() in FONT_TYPES: + font = ImageFont.truetype(filepath) + self.dimensions_label.setText( + f"{filepath.suffix.upper()[1:]} • {format_size(filepath.stat().st_size)}\n{font.getname()[0]} ({font.getname()[1]}) " + ) else: self.dimensions_label.setText( f"{filepath.suffix.upper()[1:]} • {format_size(filepath.stat().st_size)}" diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index 9ed612c98..47421b4f3 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -23,11 +23,15 @@ from PySide6.QtCore import QObject, Signal, QSize from PySide6.QtGui import QPixmap from src.qt.helpers.gradient import four_corner_gradient_background +from src.qt.helpers.text_wrapper import wrap_full_text from src.core.constants import ( PLAINTEXT_TYPES, + FONT_TYPES, VIDEO_TYPES, IMAGE_TYPES, RAW_IMAGE_TYPES, + FONT_SAMPLE_TEXT, + FONT_SAMPLE_SIZES, BLENDER_TYPES, ) from src.core.utils.encoding import detect_char_encoding @@ -185,7 +189,40 @@ def render( text = text_file.read(256) bg = Image.new("RGB", (256, 256), color="#1e1e1e") draw = ImageDraw.Draw(bg) - draw.text((16, 16), text, file=(255, 255, 255)) + draw.text((16, 16), text, fill=(255, 255, 255)) + image = bg + # Fonts ======================================================== + elif _filepath.suffix.lower() in FONT_TYPES: + # Scale the sample font sizes to the preview image + # resolution,assuming the sizes are tuned for 256px. + scaled_sizes: list[int] = [ + math.floor(x * (adj_size / 256)) for x in FONT_SAMPLE_SIZES + ] + if gradient: + # handles small thumbnails + bg = Image.new("RGB", (adj_size, adj_size), color="#1e1e1e") + draw = ImageDraw.Draw(bg) + font = ImageFont.truetype( + _filepath, size=math.ceil(adj_size * 0.65) + ) + draw.text((10, 0), "Aa", font=font) + else: + # handles big thumbnails and renders a sample text in multiple font sizes + bg = Image.new("RGB", (adj_size, adj_size), color="#1e1e1e") + draw = ImageDraw.Draw(bg) + lines_of_padding = 2 + y_offset = 0 + + for font_size in scaled_sizes: + font = ImageFont.truetype(_filepath, size=font_size) + text_wrapped: str = wrap_full_text( + FONT_SAMPLE_TEXT, font=font, width=adj_size, draw=draw + ) + draw.multiline_text((0, y_offset), text_wrapped, font=font) + y_offset += ( + len(text_wrapped.split("\n")) + lines_of_padding + ) * draw.textbbox((0, 0), "A", font=font)[-1] + image = bg # 3D =========================================================== # elif extension == 'stl':