Skip to content

Commit e463635

Browse files
Add font thumbnail preview support (#307)
* 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 <46939827+CyanVoxel@users.noreply.github.com>
1 parent aa0aad4 commit e463635

File tree

4 files changed

+109
-3
lines changed

4 files changed

+109
-3
lines changed

tagstudio/src/core/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
COLLAGE_FOLDER_NAME: str = "collages"
88
LIBRARY_FILENAME: str = "ts_library.json"
99

10+
FONT_SAMPLE_TEXT: str = (
11+
"""ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?@$%(){}[]"""
12+
)
13+
FONT_SAMPLE_SIZES: list[int] = [10, 15, 20]
14+
1015
# TODO: Turn this whitelist into a user-configurable blacklist.
1116
IMAGE_TYPES: list[str] = [
1217
".png",
@@ -142,6 +147,7 @@
142147
]
143148
PROGRAM_TYPES: list[str] = [".exe", ".app"]
144149
SHORTCUT_TYPES: list[str] = [".lnk", ".desktop", ".url"]
150+
FONT_TYPES: list[str] = [".ttf", ".otf", ".woff", ".woff2", ".ttc"]
145151

146152
ALL_FILE_TYPES: list[str] = (
147153
IMAGE_TYPES
@@ -153,6 +159,7 @@
153159
+ ARCHIVE_TYPES
154160
+ PROGRAM_TYPES
155161
+ SHORTCUT_TYPES
162+
+ FONT_TYPES
156163
)
157164

158165
BOX_FIELDS = ["tag_box", "text_box"]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
5+
from PIL import Image, ImageDraw, ImageFont
6+
7+
8+
def wrap_line( # type: ignore
9+
text: str,
10+
font: ImageFont.ImageFont,
11+
width: int = 256,
12+
draw: ImageDraw.ImageDraw = None,
13+
) -> int:
14+
"""
15+
Takes in a single line and returns the index it should be broken up at but
16+
it only splits one Time
17+
"""
18+
if draw is None:
19+
bg = Image.new("RGB", (width, width), color="#1e1e1e")
20+
draw = ImageDraw.Draw(bg)
21+
if draw.textlength(text, font=font) > width:
22+
for i in range(
23+
int(len(text) / int(draw.textlength(text, font=font)) * width) - 2,
24+
0,
25+
-1,
26+
):
27+
if draw.textlength(text[:i], font=font) < width:
28+
return i
29+
else:
30+
return -1
31+
32+
33+
def wrap_full_text(
34+
text: str,
35+
font: ImageFont.ImageFont,
36+
width: int = 256,
37+
draw: ImageDraw.ImageDraw = None,
38+
) -> str:
39+
"""
40+
Takes in a string and breaks it up to fit in the canvas given accounts for kerning and font size etc.
41+
"""
42+
lines = []
43+
i = 0
44+
last_i = 0
45+
while wrap_line(text[i:], font=font, width=width, draw=draw) > 0:
46+
i = wrap_line(text[i:], font=font, width=width, draw=draw) + last_i
47+
lines.append(text[last_i:i])
48+
last_i = i
49+
lines.append(text[last_i:])
50+
text_wrapped = "\n".join(lines)
51+
return text_wrapped

tagstudio/src/qt/widgets/preview_panel.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import cv2
1212
import rawpy
13-
from PIL import Image, UnidentifiedImageError
13+
from PIL import Image, UnidentifiedImageError, ImageFont
1414
from PIL.Image import DecompressionBombError
1515
from PySide6.QtCore import QModelIndex, Signal, Qt, QSize
1616
from PySide6.QtGui import QResizeEvent, QAction
@@ -30,7 +30,13 @@
3030

3131
from src.core.enums import SettingItems, Theme
3232
from src.core.library import Entry, ItemType, Library
33-
from src.core.constants import VIDEO_TYPES, IMAGE_TYPES, RAW_IMAGE_TYPES, TS_FOLDER_NAME
33+
from src.core.constants import (
34+
VIDEO_TYPES,
35+
IMAGE_TYPES,
36+
RAW_IMAGE_TYPES,
37+
TS_FOLDER_NAME,
38+
FONT_TYPES,
39+
)
3440
from src.qt.helpers.file_opener import FileOpenerLabel, FileOpenerHelper, open_file
3541
from src.qt.modals.add_field import AddFieldModal
3642
from src.qt.widgets.thumb_renderer import ThumbRenderer
@@ -559,6 +565,11 @@ def update_widgets(self):
559565
self.dimensions_label.setText(
560566
f"{filepath.suffix.upper()[1:]}{format_size(filepath.stat().st_size)}\n{image.width} x {image.height} px"
561567
)
568+
elif filepath.suffix.lower() in FONT_TYPES:
569+
font = ImageFont.truetype(filepath)
570+
self.dimensions_label.setText(
571+
f"{filepath.suffix.upper()[1:]}{format_size(filepath.stat().st_size)}\n{font.getname()[0]} ({font.getname()[1]}) "
572+
)
562573
else:
563574
self.dimensions_label.setText(
564575
f"{filepath.suffix.upper()[1:]}{format_size(filepath.stat().st_size)}"

tagstudio/src/qt/widgets/thumb_renderer.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@
2323
from PySide6.QtCore import QObject, Signal, QSize
2424
from PySide6.QtGui import QPixmap
2525
from src.qt.helpers.gradient import four_corner_gradient_background
26+
from src.qt.helpers.text_wrapper import wrap_full_text
2627
from src.core.constants import (
2728
PLAINTEXT_TYPES,
29+
FONT_TYPES,
2830
VIDEO_TYPES,
2931
IMAGE_TYPES,
3032
RAW_IMAGE_TYPES,
33+
FONT_SAMPLE_TEXT,
34+
FONT_SAMPLE_SIZES,
3135
BLENDER_TYPES,
3236
)
3337
from src.core.utils.encoding import detect_char_encoding
@@ -185,7 +189,40 @@ def render(
185189
text = text_file.read(256)
186190
bg = Image.new("RGB", (256, 256), color="#1e1e1e")
187191
draw = ImageDraw.Draw(bg)
188-
draw.text((16, 16), text, file=(255, 255, 255))
192+
draw.text((16, 16), text, fill=(255, 255, 255))
193+
image = bg
194+
# Fonts ========================================================
195+
elif _filepath.suffix.lower() in FONT_TYPES:
196+
# Scale the sample font sizes to the preview image
197+
# resolution,assuming the sizes are tuned for 256px.
198+
scaled_sizes: list[int] = [
199+
math.floor(x * (adj_size / 256)) for x in FONT_SAMPLE_SIZES
200+
]
201+
if gradient:
202+
# handles small thumbnails
203+
bg = Image.new("RGB", (adj_size, adj_size), color="#1e1e1e")
204+
draw = ImageDraw.Draw(bg)
205+
font = ImageFont.truetype(
206+
_filepath, size=math.ceil(adj_size * 0.65)
207+
)
208+
draw.text((10, 0), "Aa", font=font)
209+
else:
210+
# handles big thumbnails and renders a sample text in multiple font sizes
211+
bg = Image.new("RGB", (adj_size, adj_size), color="#1e1e1e")
212+
draw = ImageDraw.Draw(bg)
213+
lines_of_padding = 2
214+
y_offset = 0
215+
216+
for font_size in scaled_sizes:
217+
font = ImageFont.truetype(_filepath, size=font_size)
218+
text_wrapped: str = wrap_full_text(
219+
FONT_SAMPLE_TEXT, font=font, width=adj_size, draw=draw
220+
)
221+
draw.multiline_text((0, y_offset), text_wrapped, font=font)
222+
y_offset += (
223+
len(text_wrapped.split("\n")) + lines_of_padding
224+
) * draw.textbbox((0, 0), "A", font=font)[-1]
225+
189226
image = bg
190227
# 3D ===========================================================
191228
# elif extension == 'stl':

0 commit comments

Comments
 (0)