Skip to content

Add font thumbnail preview support #307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 19, 2024
7 changes: 7 additions & 0 deletions tagstudio/src/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -153,6 +159,7 @@
+ ARCHIVE_TYPES
+ PROGRAM_TYPES
+ SHORTCUT_TYPES
+ FONT_TYPES
)

BOX_FIELDS = ["tag_box", "text_box"]
Expand Down
51 changes: 51 additions & 0 deletions tagstudio/src/qt/helpers/text_wrapper.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 13 additions & 2 deletions tagstudio/src/qt/widgets/preview_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)}"
Expand Down
39 changes: 38 additions & 1 deletion tagstudio/src/qt/widgets/thumb_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down