diff --git a/src/tagstudio/renderers/vendored/blender_thumbnailer.py b/src/tagstudio/renderers/vendored/blender_thumbnailer.py index 8886bcbdd..19cf3707c 100644 --- a/src/tagstudio/renderers/vendored/blender_thumbnailer.py +++ b/src/tagstudio/renderers/vendored/blender_thumbnailer.py @@ -7,75 +7,245 @@ import gzip import os import struct -from io import BufferedReader from pathlib import Path +from typing import BinaryIO from PIL import Image, ImageOps def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: - rend = b"REND" - test = b"TEST" + REND: bytes = b"REND" + TEST: bytes = b"TEST" + ENDB: bytes = b"ENDB" - blendfile: BufferedReader | gzip.GzipFile = open(path, "rb") + blendfile: BinaryIO | gzip.GzipFile | None = None + raw_file: BinaryIO | None = None - head = blendfile.read(12) + try: + # -------------------------------------------------------------- + # Open file. + # -------------------------------------------------------------- + raw_file = open(path, "rb") - if head[0:2] == b"\x1f\x8b": # gzip magic - blendfile.close() - blendfile = gzip.GzipFile("", "rb", 0, open(path, "rb")) - head = blendfile.read(12) + # Legacy header = 12 bytes + # Blender 5+ = 17 bytes + head: bytes = raw_file.read(17) - if not head.startswith(b"BLENDER"): - blendfile.close() - return None, 0, 0 + # -------------------------------------------------------------- + # GZIP-compressed blend file. + # -------------------------------------------------------------- + if head[:2] == b"\x1f\x8b": + raw_file.close() + raw_file = None - is_64_bit = head[7] == b"-"[0] + blendfile = gzip.open(path, "rb") + head = blendfile.read(17) + else: + blendfile = raw_file - # true for PPC, false for X86 - is_big_endian = head[8] == b"V"[0] + if not head.startswith(b"BLENDER"): + return None, 0, 0 - # blender pre 2.5 had no thumbs - if head[9:11] <= b"24": - return None, 0, 0 + if len(head) < 12: + return None, 0, 0 - sizeof_bhead = 24 if is_64_bit else 20 - int_endian = ">i" if is_big_endian else "= 17 and head[7:9].isdigit() and head[9:13] == b"-01v" # format + ) - while True: - bhead = blendfile.read(sizeof_bhead) + if is_blender_5: + try: + header_size: int = int(head[7:9]) + version: int = int(head[13:17]) + except ValueError: + return None, 0, 0 - if len(bhead) < sizeof_bhead: - return None, 0, 0 + if header_size < 17: + return None, 0, 0 - code = bhead[:4] - length = struct.unpack(int_endian, bhead[4:8])[0] # 4 == sizeof(int) + # We have already consumed 17 bytes. + if header_size > 17: + blendfile.seek(header_size - 17, os.SEEK_CUR) - if code == rend: - blendfile.seek(length, os.SEEK_CUR) + # ---------------------------------------------------------- + # Blender 5+ BHead + # + # 0-3 code + # 4-7 SDNA index (uint32) + # 8-15 old pointer (uint64) + # 16-23 block size (uint64) + # 24-31 count (uint64) + # + # Total = 32 bytes. + # ---------------------------------------------------------- + sizeof_bhead: int = 32 + large_bhead: bool = True + + int_endian: str = "<" + int_endian_pair: str = "= 4 and bhead[:4] == ENDB: + return None, 0, 0 + + if len(bhead) < sizeof_bhead: + return None, 0, 0 + + code: bytes = bhead[:4] + + # ---------------------------------------------------------- + # Blender 5+ + # + # The block size is at offset 16 and is uint64. + # ---------------------------------------------------------- + if large_bhead: + length: int = struct.unpack_from( + " Image.Image | None: @@ -88,4 +258,8 @@ def blend_thumb(file_in: Path | str) -> Image.Image | None: buf, ) image = ImageOps.flip(image) + # Upscale Image so it looks better at higher resolutions. + width, height = image.size + ratio = height / width + image = image.resize((512, round(512 * ratio)), Image.Resampling.BICUBIC) return image