mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-31 15:19:10 +00:00
split ThumbRenderer from ts_qt.py
This commit is contained in:
@@ -44,7 +44,7 @@ from src.core.utils.web import strip_web_protocol
|
||||
from src.qt.flowlayout import FlowLayout, FlowWidget
|
||||
from src.qt.main_window import Ui_MainWindow
|
||||
from src.qt.helpers import open_file, FileOpenerHelper, FileOpenerLabel
|
||||
from src.qt.widgets import FieldContainer, FieldWidget, CollageIconRenderer, ThumbButton
|
||||
from src.qt.widgets import FieldContainer, FieldWidget, CollageIconRenderer, ThumbButton, ThumbRenderer
|
||||
import src.qt.resources_rc
|
||||
|
||||
# SIGQUIT is not defined on Windows
|
||||
@@ -3015,401 +3015,6 @@ class ItemThumb(FlowWidget):
|
||||
# e.remove_tag(1)
|
||||
|
||||
|
||||
class ThumbRenderer(QObject):
|
||||
# finished = Signal()
|
||||
updated = Signal(float, QPixmap, QSize, str)
|
||||
updated_ratio = Signal(float)
|
||||
# updatedImage = Signal(QPixmap)
|
||||
# updatedSize = Signal(QSize)
|
||||
|
||||
thumb_mask_512: Image.Image = Image.open(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/images/thumb_mask_512.png'))
|
||||
thumb_mask_512.load()
|
||||
|
||||
thumb_mask_hl_512: Image.Image = Image.open(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/images/thumb_mask_hl_512.png'))
|
||||
thumb_mask_hl_512.load()
|
||||
|
||||
thumb_loading_512: Image.Image = Image.open(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/images/thumb_loading_512.png'))
|
||||
thumb_loading_512.load()
|
||||
|
||||
thumb_broken_512: Image.Image = Image.open(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/images/thumb_broken_512.png'))
|
||||
thumb_broken_512.load()
|
||||
|
||||
thumb_file_default_512: Image.Image = Image.open(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/images/thumb_file_default_512.png'))
|
||||
thumb_file_default_512.load()
|
||||
|
||||
# thumb_debug: Image.Image = Image.open(os.path.normpath(
|
||||
# f'{Path(__file__).parent.parent.parent}/resources/qt/images/temp.jpg'))
|
||||
# thumb_debug.load()
|
||||
|
||||
# TODO: Make dynamic font sized given different pixel ratios
|
||||
font_pixel_ratio: float = 1
|
||||
ext_font = ImageFont.truetype(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf'), math.floor(12*font_pixel_ratio))
|
||||
|
||||
def __init__(self):
|
||||
QObject.__init__(self)
|
||||
|
||||
def render(self, timestamp: float, filepath, base_size: tuple[int, int], pixelRatio: float, isLoading=False):
|
||||
"""Renders an entry/element thumbnail for the GUI."""
|
||||
adj_size: int = 1
|
||||
image = None
|
||||
pixmap = None
|
||||
final = None
|
||||
extension: str = None
|
||||
broken_thumb = False
|
||||
# adj_font_size = math.floor(12 * pixelRatio)
|
||||
if ThumbRenderer.font_pixel_ratio != pixelRatio:
|
||||
ThumbRenderer.font_pixel_ratio = pixelRatio
|
||||
ThumbRenderer.ext_font = ImageFont.truetype(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf'), math.floor(12*ThumbRenderer.font_pixel_ratio))
|
||||
|
||||
if isLoading or filepath:
|
||||
adj_size = math.ceil(base_size[0] * pixelRatio)
|
||||
|
||||
if isLoading:
|
||||
li: Image.Image = ThumbRenderer.thumb_loading_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
qim = ImageQt.ImageQt(li)
|
||||
pixmap = QPixmap.fromImage(qim)
|
||||
pixmap.setDevicePixelRatio(pixelRatio)
|
||||
elif filepath:
|
||||
mask: Image.Image = ThumbRenderer.thumb_mask_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR).getchannel(3)
|
||||
hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
extension = os.path.splitext(filepath)[1][1:].lower()
|
||||
|
||||
try:
|
||||
# Images =======================================================
|
||||
if extension in IMAGE_TYPES:
|
||||
image = Image.open(filepath)
|
||||
# image = self.thumb_debug
|
||||
if image.mode == 'RGBA':
|
||||
# logging.info(image.getchannel(3).tobytes())
|
||||
new_bg = Image.new('RGB', image.size, color='#222222')
|
||||
new_bg.paste(image, mask=image.getchannel(3))
|
||||
image = new_bg
|
||||
if image.mode != 'RGB':
|
||||
image = image.convert(mode='RGB')
|
||||
|
||||
# Videos =======================================================
|
||||
elif extension in VIDEO_TYPES:
|
||||
video = cv2.VideoCapture(filepath)
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES,
|
||||
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2))
|
||||
success, frame = video.read()
|
||||
if not success:
|
||||
# Depending on the video format, compression, and frame
|
||||
# count, seeking halfway does not work and the thumb
|
||||
# must be pulled from the earliest available frame.
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
success, frame = video.read()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
image = Image.fromarray(frame)
|
||||
|
||||
# Plain Text ===================================================
|
||||
elif extension in PLAINTEXT_TYPES:
|
||||
try:
|
||||
text: str = extension
|
||||
with open(filepath, 'r', encoding='utf-8') as text_file:
|
||||
text = text_file.read(256)
|
||||
bg = Image.new('RGB',(256,256), color='#222222')
|
||||
draw = ImageDraw.Draw(bg)
|
||||
draw.text((16,16), text, file=(255,255,255))
|
||||
image = bg
|
||||
except:
|
||||
logging.info(f'[ThumbRenderer][ERROR]: Coulnd\'t render thumbnail for {filepath}')
|
||||
# No Rendered Thumbnail ========================================
|
||||
else:
|
||||
image = ThumbRenderer.thumb_file_default_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
if not image:
|
||||
raise UnidentifiedImageError
|
||||
|
||||
orig_x, orig_y = image.size
|
||||
new_x, new_y = (adj_size, adj_size)
|
||||
|
||||
if orig_x > orig_y:
|
||||
new_x = adj_size
|
||||
new_y = math.ceil(adj_size * (orig_y / orig_x))
|
||||
elif orig_y > orig_x:
|
||||
new_y = adj_size
|
||||
new_x = math.ceil(adj_size * (orig_x / orig_y))
|
||||
|
||||
# img_ratio = new_x / new_y
|
||||
image = image.resize(
|
||||
(new_x, new_y), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
if image.size != (adj_size, adj_size):
|
||||
# Old 1 color method.
|
||||
# bg_col = image.copy().resize((1, 1)).getpixel((0,0))
|
||||
# bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col)
|
||||
# bg.thumbnail((1, 1))
|
||||
# bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)
|
||||
|
||||
# Small gradient background. Looks decent, and is only a one-liner.
|
||||
# bg = image.copy().resize((2, 2), resample=Image.Resampling.BILINEAR).resize((adj_size,adj_size),resample=Image.Resampling.BILINEAR)
|
||||
|
||||
# Four-Corner Gradient Background.
|
||||
# Not exactly a one-liner, but it's (subjectively) really cool.
|
||||
tl = image.getpixel((0, 0))
|
||||
tr = image.getpixel(((image.size[0]-1), 0))
|
||||
bl = image.getpixel((0, (image.size[1]-1)))
|
||||
br = image.getpixel(((image.size[0]-1), (image.size[1]-1)))
|
||||
bg = Image.new(mode='RGB', size=(2, 2))
|
||||
bg.paste(tl, (0, 0, 2, 2))
|
||||
bg.paste(tr, (1, 0, 2, 2))
|
||||
bg.paste(bl, (0, 1, 2, 2))
|
||||
bg.paste(br, (1, 1, 2, 2))
|
||||
bg = bg.resize((adj_size, adj_size),
|
||||
resample=Image.Resampling.BICUBIC)
|
||||
|
||||
bg.paste(image, box=(
|
||||
(adj_size-image.size[0])//2, (adj_size-image.size[1])//2))
|
||||
|
||||
bg.putalpha(mask)
|
||||
final = bg
|
||||
|
||||
else:
|
||||
image.putalpha(mask)
|
||||
final = image
|
||||
|
||||
hl_soft = hl.copy()
|
||||
hl_soft.putalpha(ImageEnhance.Brightness(
|
||||
hl.getchannel(3)).enhance(.5))
|
||||
final.paste(ImageChops.soft_light(final, hl_soft),
|
||||
mask=hl_soft.getchannel(3))
|
||||
|
||||
# hl_add = hl.copy()
|
||||
# hl_add.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(.25))
|
||||
# final.paste(hl_add, mask=hl_add.getchannel(3))
|
||||
|
||||
except (UnidentifiedImageError, FileNotFoundError, cv2.error):
|
||||
broken_thumb = True
|
||||
final = ThumbRenderer.thumb_broken_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
qim = ImageQt.ImageQt(final)
|
||||
if image:
|
||||
image.close()
|
||||
pixmap = QPixmap.fromImage(qim)
|
||||
pixmap.setDevicePixelRatio(pixelRatio)
|
||||
|
||||
if pixmap:
|
||||
self.updated.emit(timestamp, pixmap, QSize(*base_size), extension)
|
||||
|
||||
else:
|
||||
self.updated.emit(timestamp, QPixmap(),
|
||||
QSize(*base_size), extension)
|
||||
|
||||
def render_big(self, timestamp: float, filepath, base_size: tuple[int, int], pixelRatio: float, isLoading=False):
|
||||
"""Renders a large, non-square entry/element thumbnail for the GUI."""
|
||||
adj_size: int = 1
|
||||
image: Image.Image = None
|
||||
pixmap: QPixmap = None
|
||||
final: Image.Image = None
|
||||
extension: str = None
|
||||
broken_thumb = False
|
||||
img_ratio = 1
|
||||
# adj_font_size = math.floor(12 * pixelRatio)
|
||||
if ThumbRenderer.font_pixel_ratio != pixelRatio:
|
||||
ThumbRenderer.font_pixel_ratio = pixelRatio
|
||||
ThumbRenderer.ext_font = ImageFont.truetype(os.path.normpath(
|
||||
f'{Path(__file__).parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf'), math.floor(12*ThumbRenderer.font_pixel_ratio))
|
||||
|
||||
if isLoading or filepath:
|
||||
adj_size = math.ceil(max(base_size[0], base_size[1]) * pixelRatio)
|
||||
|
||||
if isLoading:
|
||||
adj_size = math.ceil((512 * pixelRatio))
|
||||
final: Image.Image = ThumbRenderer.thumb_loading_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
qim = ImageQt.ImageQt(final)
|
||||
pixmap = QPixmap.fromImage(qim)
|
||||
pixmap.setDevicePixelRatio(pixelRatio)
|
||||
self.updated_ratio.emit(1)
|
||||
|
||||
elif filepath:
|
||||
# mask: Image.Image = ThumbRenderer.thumb_mask_512.resize(
|
||||
# (adj_size, adj_size), resample=Image.Resampling.BILINEAR).getchannel(3)
|
||||
# hl: Image.Image = ThumbRenderer.thumb_mask_hl_512.resize(
|
||||
# (adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
extension = os.path.splitext(filepath)[1][1:].lower()
|
||||
|
||||
try:
|
||||
# Images =======================================================
|
||||
if extension in IMAGE_TYPES:
|
||||
image = Image.open(filepath)
|
||||
# image = self.thumb_debug
|
||||
if image.mode == 'RGBA':
|
||||
# logging.info(image.getchannel(3).tobytes())
|
||||
new_bg = Image.new('RGB', image.size, color='#222222')
|
||||
new_bg.paste(image, mask=image.getchannel(3))
|
||||
image = new_bg
|
||||
if image.mode != 'RGB':
|
||||
image = image.convert(mode='RGB')
|
||||
|
||||
# Videos =======================================================
|
||||
elif extension in VIDEO_TYPES:
|
||||
video = cv2.VideoCapture(filepath)
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES,
|
||||
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2))
|
||||
success, frame = video.read()
|
||||
if not success:
|
||||
# Depending on the video format, compression, and frame
|
||||
# count, seeking halfway does not work and the thumb
|
||||
# must be pulled from the earliest available frame.
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
success, frame = video.read()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
image = Image.fromarray(frame)
|
||||
# Plain Text ===================================================
|
||||
elif extension in PLAINTEXT_TYPES:
|
||||
try:
|
||||
text: str = extension
|
||||
with open(filepath, 'r', encoding='utf-8') as text_file:
|
||||
text = text_file.read(256)
|
||||
bg = Image.new('RGB',(256,256), color='#222222')
|
||||
draw = ImageDraw.Draw(bg)
|
||||
draw.text((16,16), text, file=(255,255,255))
|
||||
image = bg
|
||||
except:
|
||||
logging.info(f'[ThumbRenderer][ERROR]: Coulnd\'t render thumbnail for {filepath}')
|
||||
# No Rendered Thumbnail ========================================
|
||||
else:
|
||||
image = ThumbRenderer.thumb_file_default_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
if not image:
|
||||
raise UnidentifiedImageError
|
||||
|
||||
orig_x, orig_y = image.size
|
||||
if orig_x < adj_size and orig_y < adj_size:
|
||||
new_x, new_y = (adj_size, adj_size)
|
||||
if orig_x > orig_y:
|
||||
new_x = adj_size
|
||||
new_y = math.ceil(adj_size * (orig_y / orig_x))
|
||||
elif orig_y > orig_x:
|
||||
new_y = adj_size
|
||||
new_x = math.ceil(adj_size * (orig_x / orig_y))
|
||||
else:
|
||||
new_x, new_y = (adj_size, adj_size)
|
||||
if orig_x > orig_y:
|
||||
new_x = adj_size
|
||||
new_y = math.ceil(adj_size * (orig_y / orig_x))
|
||||
elif orig_y > orig_x:
|
||||
new_y = adj_size
|
||||
new_x = math.ceil(adj_size * (orig_x / orig_y))
|
||||
|
||||
self.updated_ratio.emit(new_x / new_y)
|
||||
image = image.resize(
|
||||
(new_x, new_y), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
# image = image.resize(
|
||||
# (new_x, new_y), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
# if image.size != (adj_size, adj_size):
|
||||
# # Old 1 color method.
|
||||
# # bg_col = image.copy().resize((1, 1)).getpixel((0,0))
|
||||
# # bg = Image.new(mode='RGB',size=(adj_size,adj_size),color=bg_col)
|
||||
# # bg.thumbnail((1, 1))
|
||||
# # bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST)
|
||||
|
||||
# # Small gradient background. Looks decent, and is only a one-liner.
|
||||
# # bg = image.copy().resize((2, 2), resample=Image.Resampling.BILINEAR).resize((adj_size,adj_size),resample=Image.Resampling.BILINEAR)
|
||||
|
||||
# # Four-Corner Gradient Background.
|
||||
# # Not exactly a one-liner, but it's (subjectively) really cool.
|
||||
# tl = image.getpixel((0, 0))
|
||||
# tr = image.getpixel(((image.size[0]-1), 0))
|
||||
# bl = image.getpixel((0, (image.size[1]-1)))
|
||||
# br = image.getpixel(((image.size[0]-1), (image.size[1]-1)))
|
||||
# bg = Image.new(mode='RGB', size=(2, 2))
|
||||
# bg.paste(tl, (0, 0, 2, 2))
|
||||
# bg.paste(tr, (1, 0, 2, 2))
|
||||
# bg.paste(bl, (0, 1, 2, 2))
|
||||
# bg.paste(br, (1, 1, 2, 2))
|
||||
# bg = bg.resize((adj_size, adj_size),
|
||||
# resample=Image.Resampling.BICUBIC)
|
||||
|
||||
# bg.paste(image, box=(
|
||||
# (adj_size-image.size[0])//2, (adj_size-image.size[1])//2))
|
||||
|
||||
# bg.putalpha(mask)
|
||||
# final = bg
|
||||
|
||||
# else:
|
||||
# image.putalpha(mask)
|
||||
# final = image
|
||||
|
||||
# hl_soft = hl.copy()
|
||||
# hl_soft.putalpha(ImageEnhance.Brightness(
|
||||
# hl.getchannel(3)).enhance(.5))
|
||||
# final.paste(ImageChops.soft_light(final, hl_soft),
|
||||
# mask=hl_soft.getchannel(3))
|
||||
|
||||
# hl_add = hl.copy()
|
||||
# hl_add.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(.25))
|
||||
# final.paste(hl_add, mask=hl_add.getchannel(3))
|
||||
scalar = 4
|
||||
rec: Image.Image = Image.new('RGB', tuple(
|
||||
[d * scalar for d in image.size]), 'black')
|
||||
draw = ImageDraw.Draw(rec)
|
||||
draw.rounded_rectangle(
|
||||
(0, 0)+rec.size, (base_size[0]//32) * scalar * pixelRatio, fill='red')
|
||||
rec = rec.resize(
|
||||
tuple([d // scalar for d in rec.size]), resample=Image.Resampling.BILINEAR)
|
||||
# final = image
|
||||
final = Image.new('RGBA', image.size, (0, 0, 0, 0))
|
||||
# logging.info(rec.size)
|
||||
# logging.info(image.size)
|
||||
final.paste(image, mask=rec.getchannel(0))
|
||||
|
||||
except (UnidentifiedImageError, FileNotFoundError, cv2.error):
|
||||
broken_thumb = True
|
||||
self.updated_ratio.emit(1)
|
||||
final = ThumbRenderer.thumb_broken_512.resize(
|
||||
(adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
|
||||
# if extension in VIDEO_TYPES + ['gif', 'apng'] or broken_thumb:
|
||||
# idk = ImageDraw.Draw(final)
|
||||
# # idk.textlength(file_type)
|
||||
# ext_offset_x = idk.textlength(
|
||||
# text=extension.upper(), font=ThumbRenderer.ext_font) / 2
|
||||
# ext_offset_x = math.floor(ext_offset_x * (1/pixelRatio))
|
||||
# x_margin = math.floor(
|
||||
# (adj_size-((base_size[0]//6)+ext_offset_x) * pixelRatio))
|
||||
# y_margin = math.floor(
|
||||
# (adj_size-((base_size[0]//8)) * pixelRatio))
|
||||
# stroke_width = round(2 * pixelRatio)
|
||||
# fill = 'white' if not broken_thumb else '#E32B41'
|
||||
# idk.text((x_margin, y_margin), extension.upper(
|
||||
# ), fill=fill, font=ThumbRenderer.ext_font, stroke_width=stroke_width, stroke_fill=(0, 0, 0))
|
||||
|
||||
qim = ImageQt.ImageQt(final)
|
||||
if image:
|
||||
image.close()
|
||||
pixmap = QPixmap.fromImage(qim)
|
||||
pixmap.setDevicePixelRatio(pixelRatio)
|
||||
|
||||
if pixmap:
|
||||
# logging.info(final.size)
|
||||
# self.updated.emit(pixmap, QSize(*final.size))
|
||||
self.updated.emit(timestamp, pixmap, QSize(math.ceil(
|
||||
adj_size * 1/pixelRatio), math.ceil(final.size[1] * 1/pixelRatio)), extension)
|
||||
|
||||
else:
|
||||
self.updated.emit(timestamp, QPixmap(),
|
||||
QSize(*base_size), extension)
|
||||
|
||||
class CustomRunnable(QRunnable, QObject):
|
||||
done = Signal()
|
||||
def __init__(self, function) -> None:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .fields import FieldContainer, FieldWidget
|
||||
from .collage_icon import CollageIconRenderer
|
||||
from .thumb_button import ThumbButton
|
||||
from .thumb_button import ThumbButton
|
||||
from .thumb_renderer import ThumbRenderer
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user