diff --git a/src/tagstudio/previews/vendored/blender_thumbnailer.py b/src/tagstudio/previews/vendored/blender_thumbnailer.py index 8886bcbdd..831f4a3f9 100644 --- a/src/tagstudio/previews/vendored/blender_thumbnailer.py +++ b/src/tagstudio/previews/vendored/blender_thumbnailer.py @@ -7,75 +7,210 @@ 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 - head = blendfile.read(12) + with open(path, "rb") as raw_file: + # Legacy header = 12 bytes + # Blender 5.0+ = 17 bytes + head: bytes = raw_file.read(17) - if head[0:2] == b"\x1f\x8b": # gzip magic - blendfile.close() - blendfile = gzip.GzipFile("", "rb", 0, open(path, "rb")) - head = blendfile.read(12) + # GZIP-compressed blend file. + if head[:2] == b"\x1f\x8b": + raw_file.close() + raw_file = None - if not head.startswith(b"BLENDER"): - blendfile.close() - return None, 0, 0 + with gzip.open(path, "rb") as blendfile: + head = blendfile.read(17) + else: + blendfile = raw_file - is_64_bit = head[7] == b"-"[0] + if not head.startswith(b"BLENDER"): + return None, 0, 0 - # true for PPC, false for X86 - is_big_endian = head[8] == b"V"[0] + if len(head) < 12: + return None, 0, 0 - # blender pre 2.5 had no thumbs - if head[9:11] <= b"24": - return None, 0, 0 + # -------------------------------------------------------------- + # Blender 5.0+ header + # + # BLENDER17-01v0501 + # 01234567890123456 + # + # 0-6 = BLENDER + # 7-8 = header size + # 9 = '-' + # 10-11 = header format + # 12 = 'v' + # 13-16 = Blender version + # -------------------------------------------------------------- + is_blender_5: bool = ( + len(head) >= 17 and head[7:9].isdigit() and head[9:13] == b"-01v" # format + ) + + 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 header_size < 17: + return None, 0, 0 + + # We have already consumed 17 bytes. + if header_size > 17: + blendfile.seek(header_size - 17, os.SEEK_CUR) + + # ---------------------------------------------------------- + # Blender 5.0+ 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.0+ + # + # The block size is at offset 16 and is uint64. + # ---------------------------------------------------------- + if large_bhead: + length: int = struct.unpack_from(" Image.Image | None: