Skip to content
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] = [12, 15, 20]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight adjustment to font sample sizes to show more difference between them

Suggested change
FONT_SAMPLE_SIZES: list[int] = [12, 15, 20]
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 @@ -107,6 +112,7 @@
]
PROGRAM_TYPES: list[str] = [".exe", ".app"]
SHORTCUT_TYPES: list[str] = [".lnk", ".desktop", ".url"]
FONT_TYPES: list[str] = [".ttf", ".otf", ".woff"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add .ttc and .woff2 file extensions to the constants list (I haven't personally verified .woff2 but .ttc seems to work fine)

Suggested change
FONT_TYPES: list[str] = [".ttf", ".otf", ".woff"]
FONT_TYPES: list[str] = [".ttf", ".ttc", ".otf", ".woff", ".woff2"]


ALL_FILE_TYPES: list[str] = (
IMAGE_TYPES
Expand All @@ -118,6 +124,7 @@
+ ARCHIVE_TYPES
+ PROGRAM_TYPES
+ SHORTCUT_TYPES
+ FONT_TYPES
)

BOX_FIELDS = ["tag_box", "text_box"]
Expand Down
54 changes: 54 additions & 0 deletions tagstudio/src/qt/helpers/text_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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) > 256:
# print(draw.textlength(text, font=font))
for i in range(
int(len(text) / int(draw.textlength(text, font=font)) * 256) - 2,
0,
-1,
):
if draw.textlength(text[:i], font=font) < 256:
# print(len(text))
# print(i)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to use width variable instead of hardcoded values (plus remove print comments)

Suggested change
if draw.textlength(text, font=font) > 256:
# print(draw.textlength(text, font=font))
for i in range(
int(len(text) / int(draw.textlength(text, font=font)) * 256) - 2,
0,
-1,
):
if draw.textlength(text[:i], font=font) < 256:
# print(len(text))
# print(i)
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

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, draw=draw) + last_i

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass width variable to wrap_line

Suggested change
i = wrap_line(text[i:], font=font, draw=draw) + last_i
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 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
32 changes: 31 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,
)
from src.core.utils.encoding import detect_char_encoding

Expand Down Expand Up @@ -183,7 +187,33 @@ 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:
if gradient:
# handles small thumbnails
bg = Image.new("RGB", (256, 256), color="#1e1e1e")
draw = ImageDraw.Draw(bg)
font = ImageFont.truetype(_filepath, size=170)
draw.text((10, 0), "Aa", font=font)
else:
# handles big thumbnails and renders a sample text in multiple font sizes
bg = Image.new("RGB", (256, 256), color="#1e1e1e")
draw = ImageDraw.Draw(bg)
lines_of_padding = 2
y_offset = 0

for font_size in FONT_SAMPLE_SIZES:
font = ImageFont.truetype(_filepath, size=font_size)
text_wrapped: str = wrap_full_text(
FONT_SAMPLE_TEXT, font=font, draw=draw

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use native preview sizes with dynamic scaling

Suggested change
if gradient:
# handles small thumbnails
bg = Image.new("RGB", (256, 256), color="#1e1e1e")
draw = ImageDraw.Draw(bg)
font = ImageFont.truetype(_filepath, size=170)
draw.text((10, 0), "Aa", font=font)
else:
# handles big thumbnails and renders a sample text in multiple font sizes
bg = Image.new("RGB", (256, 256), color="#1e1e1e")
draw = ImageDraw.Draw(bg)
lines_of_padding = 2
y_offset = 0
for font_size in FONT_SAMPLE_SIZES:
font = ImageFont.truetype(_filepath, size=font_size)
text_wrapped: str = wrap_full_text(
FONT_SAMPLE_TEXT, font=font, draw=draw
# 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