Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ 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,<=11",
"pillow-avif-plugin~=1.5",
"pillow-heif~=0.22",
Expand Down
27 changes: 24 additions & 3 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 @@ -297,15 +298,25 @@ class MediaCategories:
".cr3",
".crw",
".dng",
".erf",
".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 Down Expand Up @@ -334,6 +345,7 @@ class MediaCategories:
}
_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 +387,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 @@ -500,6 +512,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 +581,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
11 changes: 11 additions & 0 deletions 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 @@ -54,6 +55,15 @@ def __get_image_stats(self, filepath: Path) -> FileAttributeData:
FileNotFoundError,
):
pass
elif MediaCategories.IMAGE_EXR_TYPES.contains(ext, mime_fallback=True):
try:
exr_file = OpenEXR.File(str(filepath))
part = exr_file.parts[0]
logger.debug("[PreviewThumb]", part=part)
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 All @@ -69,6 +79,7 @@ def __get_image_stats(self, filepath: Path) -> FileAttributeData:
elif MediaCategories.IMAGE_VECTOR_TYPES.contains(ext, mime_fallback=True):
pass # TODO

logger.debug("[PreviewThumb]", stats=stats)
return stats

def __get_gif_data(self, filepath: Path) -> tuple[bytes, tuple[int, int]] | None:
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
59 changes: 59 additions & 0 deletions src/tagstudio/qt/helpers/file_wrappers/archive/seven_zip_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from pathlib import Path
from types import TracebackType
from typing import Literal, Self

import py7zr

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


class SevenZipFile(ArchiveFile):
"""Wrapper around py7zr.SevenZipFile."""

def __init__(self, path: Path, mode: Literal["r"]) -> None:
super().__init__(path, mode)
self.path = path
self.__seven_zip_file: py7zr.SevenZipFile = py7zr.SevenZipFile(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.__seven_zip_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.__seven_zip_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:
# py7zr.SevenZipFile must be reset after every extraction
# See https://py7zr.readthedocs.io/en/stable/api.html#py7zr.SevenZipFile.extract
self.__seven_zip_file.reset()

factory = py7zr.io.BytesIOFactory(limit=10485760) # 10 MiB

search_paths: list[Path] = [Path(file_name), Path(self.path.name, file_name)]
try:
for file_path in search_paths:
try:
self.__seven_zip_file.extract(targets=[str(file_path)], factory=factory)
return factory.get(file_path).read()
except KeyError:
continue

return None
except KeyError as e:
raise e
52 changes: 52 additions & 0 deletions src/tagstudio/qt/helpers/file_wrappers/archive/tar_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import tarfile
from pathlib import Path
from types import TracebackType
from typing import Literal, Self

from tagstudio.core.utils.types import unwrap
from tagstudio.qt.helpers.file_wrappers.archive.archive_file import ArchiveFile


class TarFile(ArchiveFile):
"""Wrapper around tarfile.TarFile."""

def __init__(self, path: Path, mode: Literal["r"]) -> None:
super().__init__(path, mode)
self.path = path
self.__tar_file: tarfile.TarFile = tarfile.TarFile(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.__tar_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.__tar_file.getnames(),
)
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 unwrap(self.__tar_file.extractfile(str(file_path))).read()
except KeyError:
continue

return None
except KeyError as e:
raise e
51 changes: 51 additions & 0 deletions src/tagstudio/qt/helpers/file_wrappers/archive/zip_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import zipfile
from pathlib import Path
from types import TracebackType
from typing import Literal, Self

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


class ZipFile(ArchiveFile):
"""Wrapper around zipfile.ZipFile."""

def __init__(self, path: Path, mode: Literal["r"]) -> None:
super().__init__(path, mode)
self.path = path
self.__zip_file: zipfile.ZipFile = zipfile.ZipFile(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.__zip_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.__zip_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.__zip_file.read(str(file_path))
except KeyError:
continue

return None
except KeyError as e:
raise e
Loading