From 984240dcf97edc3423b85b2aacf5d09ee324c14f Mon Sep 17 00:00:00 2001 From: Ethnogeny <111099761+050011-code@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:29:10 +1000 Subject: [PATCH 1/3] Updated fork --- .../previews/vendored/blender_thumbnailer.py | 258 +++++++++++++++--- 1 file changed, 216 insertions(+), 42 deletions(-) diff --git a/src/tagstudio/previews/vendored/blender_thumbnailer.py b/src/tagstudio/previews/vendored/blender_thumbnailer.py index 8886bcbdd..19cf3707c 100644 --- a/src/tagstudio/previews/vendored/blender_thumbnailer.py +++ b/src/tagstudio/previews/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 From 5674471f2e1af13a5e45043ed5f0e2bffce8e909 Mon Sep 17 00:00:00 2001 From: Ethnogeny <111099761+050011-code@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:36:31 +1000 Subject: [PATCH 2/3] Refactor comments --- .../previews/vendored/blender_thumbnailer.py | 44 +++---------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/src/tagstudio/previews/vendored/blender_thumbnailer.py b/src/tagstudio/previews/vendored/blender_thumbnailer.py index 19cf3707c..b5acfce8d 100644 --- a/src/tagstudio/previews/vendored/blender_thumbnailer.py +++ b/src/tagstudio/previews/vendored/blender_thumbnailer.py @@ -22,18 +22,13 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: raw_file: BinaryIO | None = None try: - # -------------------------------------------------------------- - # Open file. - # -------------------------------------------------------------- raw_file = open(path, "rb") # Legacy header = 12 bytes # Blender 5+ = 17 bytes head: bytes = raw_file.read(17) - # -------------------------------------------------------------- # GZIP-compressed blend file. - # -------------------------------------------------------------- if head[:2] == b"\x1f\x8b": raw_file.close() raw_file = None @@ -81,7 +76,7 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: blendfile.seek(header_size - 17, os.SEEK_CUR) # ---------------------------------------------------------- - # Blender 5+ BHead + # Blender 5.0+ BHead # # 0-3 code # 4-7 SDNA index (uint32) @@ -134,9 +129,7 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: # We read 17 bytes above, but the old header is only 12. blendfile.seek(12, os.SEEK_SET) - # -------------------------------------------------------------- # Walk the BHeads until we find TEST. - # -------------------------------------------------------------- while True: bhead: bytes = blendfile.read(sizeof_bhead) @@ -150,16 +143,12 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: code: bytes = bhead[:4] # ---------------------------------------------------------- - # Blender 5+ + # Blender 5.0+ # # The block size is at offset 16 and is uint64. # ---------------------------------------------------------- if large_bhead: - length: int = struct.unpack_from( - " tuple[bytes | None, int, int]: # count = ... # ---------------------------------------------------------- else: - length = struct.unpack_from( - int_endian + "i", - bhead, - 4, - )[0] + length = struct.unpack_from(int_endian + "i", bhead, 4)[0] - # ---------------------------------------------------------- - # REND contains render information before TEST. - # Skip its payload. - # ---------------------------------------------------------- + # REND contains render information before TEST, skip its payload. if code == REND: if length < 0: return None, 0, 0 @@ -191,9 +173,6 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: # First non-REND block. break - # -------------------------------------------------------------- - # We need the TEST block. - # -------------------------------------------------------------- if code != TEST: return None, 0, 0 @@ -212,10 +191,8 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: try: x: int y: int - x, y = struct.unpack( - int_endian_pair, - dimensions, - ) + x, y = struct.unpack(int_endian_pair, dimensions) + except struct.error: return None, 0, 0 @@ -230,9 +207,6 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: if image_length != expected_length: return None, 0, 0 - # -------------------------------------------------------------- - # Read RGBA thumbnail. - # -------------------------------------------------------------- image_buffer: bytes = blendfile.read(image_length) if len(image_buffer) != image_length: @@ -258,8 +232,4 @@ 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 From 12822bdc6a15c78dcbcfa6cabf6b0bf26120665e Mon Sep 17 00:00:00 2001 From: Ethnogeny <111099761+050011-code@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:59:21 +1000 Subject: [PATCH 3/3] Refactor file opening to use context managers Refactor file handling to use context managers for better resource management. --- .../previews/vendored/blender_thumbnailer.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/tagstudio/previews/vendored/blender_thumbnailer.py b/src/tagstudio/previews/vendored/blender_thumbnailer.py index b5acfce8d..831f4a3f9 100644 --- a/src/tagstudio/previews/vendored/blender_thumbnailer.py +++ b/src/tagstudio/previews/vendored/blender_thumbnailer.py @@ -19,13 +19,11 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: ENDB: bytes = b"ENDB" blendfile: BinaryIO | gzip.GzipFile | None = None - raw_file: BinaryIO | None = None - - try: - raw_file = open(path, "rb") + raw_file: BinaryIO | None + with open(path, "rb") as raw_file: # Legacy header = 12 bytes - # Blender 5+ = 17 bytes + # Blender 5.0+ = 17 bytes head: bytes = raw_file.read(17) # GZIP-compressed blend file. @@ -33,8 +31,8 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: raw_file.close() raw_file = None - blendfile = gzip.open(path, "rb") - head = blendfile.read(17) + with gzip.open(path, "rb") as blendfile: + head = blendfile.read(17) else: blendfile = raw_file @@ -192,7 +190,7 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: x: int y: int x, y = struct.unpack(int_endian_pair, dimensions) - + except struct.error: return None, 0, 0 @@ -214,13 +212,6 @@ def blend_extract_thumb(path: Path | str) -> tuple[bytes | None, int, int]: return image_buffer, x, y - finally: - if blendfile is not None: - blendfile.close() - - if raw_file is not None and raw_file is not blendfile: - raw_file.close() - def blend_thumb(file_in: Path | str) -> Image.Image | None: buf, width, height = blend_extract_thumb(file_in)