mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-03 00:29:14 +00:00
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>
This commit is contained in:
@@ -7,6 +7,11 @@ BACKUP_FOLDER_NAME: str = "backups"
|
||||
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 @@ BLENDER_TYPES: list[str] = [
|
||||
]
|
||||
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 @@ ALL_FILE_TYPES: list[str] = (
|
||||
+ ARCHIVE_TYPES
|
||||
+ PROGRAM_TYPES
|
||||
+ SHORTCUT_TYPES
|
||||
+ FONT_TYPES
|
||||
)
|
||||
|
||||
BOX_FIELDS = ["tag_box", "text_box"]
|
||||
|
||||
51
tagstudio/src/qt/helpers/text_wrapper.py
Normal file
51
tagstudio/src/qt/helpers/text_wrapper.py
Normal file
@@ -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
|
||||
@@ -10,7 +10,7 @@ from datetime import datetime as dt
|
||||
|
||||
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 humanfriendly import format_size
|
||||
|
||||
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 @@ class PreviewPanel(QWidget):
|
||||
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)}"
|
||||
|
||||
@@ -23,11 +23,15 @@ from PIL.Image import DecompressionBombError
|
||||
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 @@ class ThumbRenderer(QObject):
|
||||
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':
|
||||
|
||||
Reference in New Issue
Block a user