Skip to content

Commit

Permalink
Add font thumbnail preview support (TagStudioDev#307)
Browse files Browse the repository at this point in the history
* Add font thumbnail preview support

* Add multiple font sizes to thumbnail

* Ruff reformat

* Ruff reformat

* Added Metadata to info

* Change the way thumbnails are structured

* Small performance improvement

* changed Metadata display structure

* added copyright notice to added file

* fix(ui): dynamically scale font previews; add .woff2, .ttc

---------

Co-authored-by: Travis Abendshien <[email protected]>
  • Loading branch information
Thesacraft and CyanVoxel authored Jul 19, 2024
1 parent aa0aad4 commit e463635
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 3 deletions.
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

0 comments on commit e463635

Please sign in to comment.