Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
49845e9
Begin splitting out renderers
TrigamDev Nov 17, 2025
ef6c061
EBook renderer (and archive file wrappers)
TrigamDev Nov 17, 2025
e979043
Oops, remove that
TrigamDev Nov 17, 2025
4e52f84
VTF renderer
TrigamDev Nov 17, 2025
55c480c
Text renderer
TrigamDev Nov 17, 2025
9a72d93
Font renderer
TrigamDev Nov 18, 2025
7ef51b3
Audio renderer, use a context object for params for each renderer
TrigamDev Nov 18, 2025
a2c14ac
Blender renderer
TrigamDev Nov 18, 2025
89654b9
Blender renderer
TrigamDev Nov 18, 2025
46b44b2
PDF renderer
TrigamDev Nov 18, 2025
f1c60c8
PowerPoint renderer
TrigamDev Nov 18, 2025
fde3781
OpenDoc renderer
TrigamDev Nov 18, 2025
439f6ae
iWork renderer (and fix it straight up just not working)
TrigamDev Nov 18, 2025
a391724
Tweak archive file wrappers to handle cases where the file name in in…
TrigamDev Nov 18, 2025
7aa9d1f
Oops, forgot to remove that
TrigamDev Nov 18, 2025
2f98c5b
Image renderers (exr not working at the moment)
TrigamDev Nov 18, 2025
029ea8d
Better EXR image handling
TrigamDev Nov 18, 2025
a7201d9
Tweaks
TrigamDev Nov 18, 2025
d89a8c3
.. more tweaks
TrigamDev Nov 18, 2025
cc8b90e
Actually handle `NoRendererError`s maybe
TrigamDev Nov 18, 2025
4db4f12
Text display widget
TrigamDev Nov 19, 2025
91856eb
Add `.gd` and `.java` to the code media type
TrigamDev Nov 19, 2025
4351690
Styling tweaks
TrigamDev Nov 19, 2025
75b043c
Tweak media types and better document them
TrigamDev Nov 23, 2025
0e915cd
Make the syntax highlighting style configurable
TrigamDev Nov 24, 2025
b9e8202
Handle cases where a file extension has multiple possible renderers
TrigamDev Nov 24, 2025
0592834
Merge branch 'refactor/thumbnail-renderers' into feat/thumbnails/code
TrigamDev Nov 24, 2025
b16e6d2
Handle missing text files
TrigamDev Nov 24, 2025
95e23ca
Merge branch 'TagStudioDev:main' into feat/code-syntax-highlighting
TrigamDev Nov 24, 2025
da85ebe
Merge branch 'TagStudioDev:main' into refactor/thumbnail-renderers
TrigamDev Nov 24, 2025
eb092fa
Add some type hints and docs
TrigamDev Nov 24, 2025
df24a7b
Merge remote-tracking branch 'TrigamDev/TagStudio/feat/code-syntax-hi…
TrigamDev Nov 24, 2025
1aa9bd9
Remove debug logs
TrigamDev Nov 24, 2025
2ae2a56
Merge branch 'refactor/thumbnail-renderers' into feat/code-syntax-hig…
TrigamDev Nov 24, 2025
7c50594
Merge branch 'main' into feat/code-syntax-highlighting
TrigamDev Nov 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 87 additions & 66 deletions docs/preview-support.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ dependencies = [
"ffmpeg-python~=0.2",
"humanfriendly==10.*",
"mutagen~=1.47",
"numexpr~=2.14.1",
"numpy~=2.2",
"opencv_python~=4.11",
"openexr~=3.4.3",
"Pillow>=10.2,<12",
"pillow-avif-plugin~=1.5",
"pillow-heif~=0.22",
"pillow-jxl-plugin~=1.3",
"py7zr==1.0.0",
Expand All @@ -29,6 +32,7 @@ dependencies = [
"SQLAlchemy~=2.0",
"srctools~=2.6",
"structlog~=25.3",
"superqt>=0.7.6",
"toml~=0.10",
"typing_extensions~=4.13",
"ujson~=5.10",
Expand Down
55 changes: 47 additions & 8 deletions src/tagstudio/core/media_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class MediaType(str, Enum):
FONT = "font"
IMAGE_ANIMATED = "image_animated"
IMAGE_RAW = "image_raw"
IMAGE_EXR = "image_exr"
IMAGE_VECTOR = "image_vector"
IMAGE = "image"
INSTALLER = "installer"
Expand All @@ -51,6 +52,7 @@ class MediaType(str, Enum):
PACKAGE = "package"
PDF = "pdf"
PLAINTEXT = "plaintext"
POWERPOINT = "powerpoint"
PRESENTATION = "presentation"
PROGRAM = "program"
SHADER = "shader"
Expand Down Expand Up @@ -109,7 +111,6 @@ class MediaCategories:
".psd",
}
_AFFINITY_PHOTO_SET: set[str] = {".afphoto"}
_KRITA_SET: set[str] = {".kra", ".krz"}
_ARCHIVE_SET: set[str] = {
".7z",
".gz",
Expand Down Expand Up @@ -187,13 +188,15 @@ class MediaCategories:
".dhtml",
".fgd",
".fish",
".gd",
".gitignore",
".h",
".hpp",
".htm",
".html",
".inf",
".ini",
".java",
".js",
".json",
".json5",
Expand Down Expand Up @@ -296,16 +299,32 @@ class MediaCategories:
".cr2",
".cr3",
".crw",
".dcs",
".dcr",
".dng",
".drf",
".erf",
".k25",
".kdc",
".mdc",
".mef",
".mos",
".mrw",
".nef",
".nrw",
".orf",
".pef",
".raf",
".raw",
".rw2",
".srf",
".srf2",
".sr2",
".srw",
".x3f",
".3fr",
}
_IMAGE_EXR_SET: set[str] = {".exr"}
_IMAGE_VECTOR_SET: set[str] = {".eps", ".epsf", ".epsi", ".svg", ".svgz"}
_IMAGE_RASTER_SET: set[str] = {
".apng",
Expand All @@ -316,24 +335,34 @@ class MediaCategories:
".heic",
".heif",
".icns",
".j2k",
".jpeg",
".jpg",
".jpe",
".jif",
".jfif",
".jp2",
".jfi",
".jpeg_large",
".jpeg",
".jpg_large",
".jpg",
".jp2",
".j2k",
".jpf",
".jpm",
".jpg2",
".j2c",
".jpc",
".jpx",
".mj2",
".jxl",
".png",
".psb",
".psd",
".tif",
".tiff",
".tif",
".webp",
}
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
_IWORK_SET: set[str] = {".key", ".pages", ".numbers"}
_KRITA_SET: set[str] = {".kra", ".krz"}
_MATERIAL_SET: set[str] = {".mtl"}
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
_OPEN_DOCUMENT_SET: set[str] = {
Expand Down Expand Up @@ -375,11 +404,11 @@ class MediaCategories:
"license",
"readme",
}
_POWERPOINT_SET: set[str] = {".pptx"}
_PRESENTATION_SET: set[str] = {
".key",
".odp",
".ppt",
".pptx",
}
_PROGRAM_SET: set[str] = {".app", ".bin", ".exe"}
_SOURCE_ENGINE_SET: set[str] = {".vtf"}
Expand Down Expand Up @@ -410,6 +439,7 @@ class MediaCategories:
".m4v",
".mkv",
".mov",
".movie",
".mp4",
".webm",
".wmv",
Expand Down Expand Up @@ -500,6 +530,9 @@ class MediaCategories:
is_iana=False,
name="raw image",
)
IMAGE_EXR_TYPES = MediaCategory(
media_type=MediaType.IMAGE_EXR, extensions=_IMAGE_EXR_SET, is_iana=False, name="exr image"
)
IMAGE_VECTOR_TYPES = MediaCategory(
media_type=MediaType.IMAGE_VECTOR,
extensions=_IMAGE_VECTOR_SET,
Expand Down Expand Up @@ -566,9 +599,15 @@ class MediaCategories:
is_iana=False,
name="plaintext",
)
POWERPOINT_TYPES = MediaCategory(
media_type=MediaType.POWERPOINT,
extensions=_POWERPOINT_SET,
is_iana=False,
name="powerpoint",
)
PRESENTATION_TYPES = MediaCategory(
media_type=MediaType.PRESENTATION,
extensions=_PRESENTATION_SET,
extensions=_PRESENTATION_SET | _POWERPOINT_SET,
is_iana=False,
name="presentation",
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pathlib import Path
from typing import TYPE_CHECKING

import structlog
from superqt.utils import CodeSyntaxHighlight

from tagstudio.qt.views.preview_panel.thumbnail.text_display_view import TextDisplayView

if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver

logger = structlog.get_logger(__name__)


class TextDisplayController(TextDisplayView):
"""A widget for displaying a plaintext file."""

def __init__(self, driver: "QtDriver") -> None:
super().__init__()
self.driver = driver

def set_file(self, path: Path) -> None:
"""Sets the text file that the text display displays.
Args:
path (Path): The path of the text file.
"""
language: str = path.suffix[1:]

try:
with open(path) as text_file:
content: str = text_file.read()

CodeSyntaxHighlight(
self.document(), language, self.driver.settings.syntax_highlighting_style
)
self.setText(content)
except ValueError:
logger.warn(f"[TextDisplayController] Couldn't find lexer for `{language}`")
except FileNotFoundError:
logger.error(f"[TextDisplayController] Couldn't find file {path}")
15 changes: 14 additions & 1 deletion src/tagstudio/qt/controllers/preview_thumb_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING

import cv2
import OpenEXR
import rawpy
import structlog
from PIL import Image, UnidentifiedImageError
Expand Down Expand Up @@ -49,11 +50,19 @@ def __get_image_stats(self, filepath: Path) -> FileAttributeData:
stats.width = image.width
stats.height = image.height
except (
rawpy._rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
FileNotFoundError,
):
pass
elif MediaCategories.IMAGE_EXR_TYPES.contains(ext, mime_fallback=True):
try:
exr_file = OpenEXR.File(str(filepath))
part = exr_file.parts[0]
stats.width = part.width()
stats.height = part.height()
except Exception:
pass
elif MediaCategories.IMAGE_RASTER_TYPES.contains(ext, mime_fallback=True):
try:
image = Image.open(str(filepath))
Expand Down Expand Up @@ -138,6 +147,10 @@ def display_file(self, filepath: Path) -> FileAttributeData:
else:
self._display_image(filepath)
return self.__get_image_stats(filepath)
# Text/Code
elif MediaCategories.PLAINTEXT_TYPES.contains(ext, mime_fallback=True):
self._display_text(filepath)
return self.__get_image_stats(filepath)
# Other Types (Including Images)
else:
self._display_image(filepath)
Expand Down
1 change: 1 addition & 0 deletions src/tagstudio/qt/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class GlobalSettings(BaseModel):
show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT)
tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT)
theme: Theme = Field(default=Theme.SYSTEM)
syntax_highlighting_style: str = Field(default="github-dark")
splash: Splash = Field(default=Splash.DEFAULT)
windows_start_command: bool = Field(default=False)

Expand Down
21 changes: 21 additions & 0 deletions src/tagstudio/qt/helpers/file_wrappers/archive/archive_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Literal


class ArchiveFile(ABC):
@abstractmethod
def __init__(self, path: Path, mode: Literal["r"]) -> None:
pass

@abstractmethod
def get_name_list(self) -> list[str]:
raise NotImplementedError

@abstractmethod
def has_file_name(self, file_name: str) -> bool:
raise NotImplementedError

@abstractmethod
def read(self, file_name: str) -> bytes:
raise NotImplementedError
52 changes: 52 additions & 0 deletions src/tagstudio/qt/helpers/file_wrappers/archive/rar_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from pathlib import Path
from types import TracebackType
from typing import Literal, Self

import rarfile

from tagstudio.qt.helpers.file_wrappers.archive.archive_file import ArchiveFile


class RarFile(ArchiveFile):
"""Wrapper around rarfile.RarFile."""

def __init__(self, path: Path, mode: Literal["r"]) -> None:
super().__init__(path, mode)
self.path = path
self.__rar_file: rarfile.RarFile = rarfile.RarFile(path, mode)

def __enter__(self) -> Self:
return self

def __exit__(
self,
exception_type: type[BaseException] | None,
exception_value: BaseException | None,
exception_traceback: TracebackType | None,
) -> None:
self.__rar_file.close()

def get_name_list(self) -> list[str]:
without_own_file_name: map = map(
lambda file_name: file_name.replace(f"{self.path.name}/", ""),
self.__rar_file.namelist(),
)
without_empty_items: filter = filter(None, without_own_file_name)

return list(without_empty_items)

def has_file_name(self, file_name: str) -> bool:
return file_name in self.get_name_list()

def read(self, file_name: str) -> bytes | None:
search_paths: list[Path] = [Path(file_name), Path(self.path.name, file_name)]
try:
for file_path in search_paths:
try:
return self.__rar_file.read(file_path)
except KeyError:
continue

return None
except KeyError as e:
raise e
Loading