Thumbnails: refactor extraction to use one code-path for all platforms

Thumbnail extraction now shares code between Linux/Windows,
allowing thumbnails from Zstd compressed blend files to be extracted.

The main logic is placed in blendthumb_extract.cc and is built as static
library. For windows there is DLL which is registered during blender
install and which then reads and generates thumbnails.

For other platforms there is blender-thumbnailer executable file which
takes blend file as an input and generates PNG file. As a result
Python script blender-thumbnailer.py is no longer needed.

The thumbnail extractor shares the same code-path as Blenders file
reading, so there is no need to duplicate any file reading logic.
This means reading compressed blend files is supported (broken since
the recent move Zstd compression - D5799).

This resolves T63736.

Contributors:

- @alausic original patch.
- @LazyDodo windows fixes/support.
- @campbellbarton general fixes/update.
- @lukasstockner97 Zstd support.

Reviewed By: sybren, mont29, LazyDodo, campbellbarton

Ref D6408
This commit is contained in:
Campbell Barton
2021-10-20 10:16:36 +11:00
parent bca2701236
commit ef9269bd62
13 changed files with 883 additions and 535 deletions

View File

@@ -1,193 +0,0 @@
#!/usr/bin/env python3
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
"""
Thumbnailer runs with python 2.7 and 3.x.
To run automatically with a file manager such as Nautilus, save this file
in a directory that is listed in PATH environment variable, and create
blender.thumbnailer file in ${HOME}/.local/share/thumbnailers/ directory
with the following contents:
[Thumbnailer Entry]
TryExec=blender-thumbnailer.py
Exec=blender-thumbnailer.py %u %o
MimeType=application/x-blender;
"""
import struct
def open_wrapper_get():
""" wrap OS specific read functionality here, fallback to 'open()'
"""
class GFileWrapper:
__slots__ = ("mode", "g_file")
def __init__(self, url, mode='r'):
self.mode = mode # used in gzip module
self.g_file = Gio.File.parse_name(url).read(None)
def read(self, size):
return self.g_file.read_bytes(size, None).get_data()
def seek(self, offset, whence=0):
self.g_file.seek(offset, [1, 0, 2][whence], None)
return self.g_file.tell()
def tell(self):
return self.g_file.tell()
def close(self):
self.g_file.close(None)
def open_local_url(url, mode='r'):
o = urlparse(url)
if o.scheme == '':
path = o.path
elif o.scheme == 'file':
path = unquote(o.path)
else:
raise(IOError('URL scheme "%s" needs gi.repository.Gio module' % o.scheme))
return open(path, mode)
try:
from gi.repository import Gio
return GFileWrapper
except ImportError:
try:
# Python 3
from urllib.parse import urlparse, unquote
except ImportError:
# Python 2
from urlparse import urlparse
from urllib import unquote
return open_local_url
def blend_extract_thumb(path):
import os
open_wrapper = open_wrapper_get()
REND = b'REND'
TEST = b'TEST'
blendfile = open_wrapper(path, 'rb')
head = blendfile.read(12)
if head[0:2] == b'\x1f\x8b': # gzip magic
import gzip
blendfile.close()
blendfile = gzip.GzipFile('', 'rb', 0, open_wrapper(path, 'rb'))
head = blendfile.read(12)
if not head.startswith(b'BLENDER'):
blendfile.close()
return None, 0, 0
is_64_bit = (head[7] == b'-'[0])
# true for PPC, false for X86
is_big_endian = (head[8] == b'V'[0])
# blender pre 2.5 had no thumbs
if head[9:11] <= b'24':
return None, 0, 0
sizeof_bhead = 24 if is_64_bit else 20
int_endian = '>i' if is_big_endian else '<i'
int_endian_pair = int_endian + 'i'
while True:
bhead = blendfile.read(sizeof_bhead)
if len(bhead) < sizeof_bhead:
return None, 0, 0
code = bhead[:4]
length = struct.unpack(int_endian, bhead[4:8])[0] # 4 == sizeof(int)
if code == REND:
blendfile.seek(length, os.SEEK_CUR)
else:
break
if code != TEST:
return None, 0, 0
try:
x, y = struct.unpack(int_endian_pair, blendfile.read(8)) # 8 == sizeof(int) * 2
except struct.error:
return None, 0, 0
length -= 8 # sizeof(int) * 2
if length != x * y * 4:
return None, 0, 0
image_buffer = blendfile.read(length)
if len(image_buffer) != length:
return None, 0, 0
return image_buffer, x, y
def write_png(buf, width, height):
import zlib
# reverse the vertical line order and add null bytes at the start
width_byte_4 = width * 4
raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in range((height - 1) * width * 4, -1, - width_byte_4))
def png_pack(png_tag, data):
chunk_head = png_tag + data
return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
return b"".join([
b'\x89PNG\r\n\x1a\n',
png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
png_pack(b'IDAT', zlib.compress(raw_data, 9)),
png_pack(b'IEND', b'')])
def main():
import sys
if len(sys.argv) < 3:
print("Expected 2 arguments <input.blend> <output.png>")
else:
file_in = sys.argv[-2]
buf, width, height = blend_extract_thumb(file_in)
if buf:
file_out = sys.argv[-1]
f = open(file_out, "wb")
f.write(write_png(buf, width, height))
f.close()
if __name__ == '__main__':
main()