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
Notes: blender-bot 2023-02-14 11:07:28 +01:00
Referenced by issue #92392, Geometry Nodes Fields - Unable to alter the domain of an attribute
Referenced by issue #92387, Crash with Geometry Nodes 2.93 file
Referenced by issue #63736, Unify blend file thumbnail extraction
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()

View File

@ -131,6 +131,7 @@ add_subdirectory(io)
add_subdirectory(functions)
add_subdirectory(makesdna)
add_subdirectory(makesrna)
add_subdirectory(blendthumb)
if(WITH_COMPOSITOR)
add_subdirectory(compositor)
@ -159,7 +160,3 @@ endif()
if(WITH_FREESTYLE)
add_subdirectory(freestyle)
endif()
if(WIN32)
add_subdirectory(blendthumb)
endif()

View File

@ -19,23 +19,59 @@
# ***** END GPL LICENSE BLOCK *****
#-----------------------------------------------------------------------------
include_directories(${ZLIB_INCLUDE_DIRS})
# Shared Thumbnail Extraction Logic
include_directories(
../blenlib
../makesdna
../../../intern/guardedalloc
)
include_directories(
SYSTEM
${ZLIB_INCLUDE_DIRS}
)
set(SRC
src/BlenderThumb.cpp
src/BlendThumb.def
src/BlendThumb.rc
src/Dll.cpp
src/blendthumb.hh
src/blendthumb_extract.cc
src/blendthumb_png.cc
)
string(APPEND CMAKE_SHARED_LINKER_FLAGS_DEBUG " /nodefaultlib:MSVCRT.lib")
if(WIN32)
# -----------------------------------------------------------------------------
# Build `BlendThumb.dll`
add_library(BlendThumb SHARED ${SRC})
setup_platform_linker_flags(BlendThumb)
target_link_libraries(BlendThumb ${ZLIB_LIBRARIES})
set(SRC_WIN32
src/blendthumb_win32.cc
src/blendthumb_win32.def
src/blendthumb_win32.rc
src/blendthumb_win32_dll.cc
)
install(
FILES $<TARGET_FILE:BlendThumb>
COMPONENT Blender
DESTINATION "."
)
add_definitions(-DNOMINMAX)
add_library(BlendThumb SHARED ${SRC} ${SRC_WIN32})
target_link_libraries(BlendThumb bf_blenlib dbghelp.lib Version.lib)
set_target_properties(BlendThumb PROPERTIES LINK_FLAGS_DEBUG "/NODEFAULTLIB:msvcrt")
install(
FILES $<TARGET_FILE:BlendThumb>
COMPONENT Blender
DESTINATION "."
)
else()
# -----------------------------------------------------------------------------
# Build `blender-thumbnailer` executable
add_executable(blender-thumbnailer ${SRC} src/blender_thumbnailer.cc)
target_link_libraries(blender-thumbnailer bf_blenlib)
target_link_libraries(blender-thumbnailer ${PTHREADS_LIBRARIES})
install(
FILES $<TARGET_FILE:blender-thumbnailer>
COMPONENT Blender
DESTINATION "."
)
endif()

View File

@ -1,321 +0,0 @@
/*
* 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.
*/
#include <new>
#include <shlwapi.h>
#include <thumbcache.h> // For IThumbnailProvider.
#pragma comment(lib, "shlwapi.lib")
// this thumbnail provider implements IInitializeWithStream to enable being hosted
// in an isolated process for robustness
class CBlendThumb : public IInitializeWithStream, public IThumbnailProvider {
public:
CBlendThumb() : _cRef(1), _pStream(NULL)
{
}
virtual ~CBlendThumb()
{
if (_pStream) {
_pStream->Release();
}
}
// IUnknown
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
static const QITAB qit[] = {
QITABENT(CBlendThumb, IInitializeWithStream),
QITABENT(CBlendThumb, IThumbnailProvider),
{0},
};
return QISearch(this, qit, riid, ppv);
}
IFACEMETHODIMP_(ULONG) AddRef()
{
return InterlockedIncrement(&_cRef);
}
IFACEMETHODIMP_(ULONG) Release()
{
ULONG cRef = InterlockedDecrement(&_cRef);
if (!cRef) {
delete this;
}
return cRef;
}
// IInitializeWithStream
IFACEMETHODIMP Initialize(IStream *pStream, DWORD grfMode);
// IThumbnailProvider
IFACEMETHODIMP GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha);
private:
long _cRef;
IStream *_pStream; // provided during initialization.
};
HRESULT CBlendThumb_CreateInstance(REFIID riid, void **ppv)
{
CBlendThumb *pNew = new (std::nothrow) CBlendThumb();
HRESULT hr = pNew ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr)) {
hr = pNew->QueryInterface(riid, ppv);
pNew->Release();
}
return hr;
}
// IInitializeWithStream
IFACEMETHODIMP CBlendThumb::Initialize(IStream *pStream, DWORD)
{
HRESULT hr = E_UNEXPECTED; // can only be inited once
if (_pStream == NULL) {
// take a reference to the stream if we have not been inited yet
hr = pStream->QueryInterface(&_pStream);
}
return hr;
}
#include "Wincodec.h"
#include <math.h>
#include <zlib.h>
const unsigned char gzip_magic[3] = {0x1f, 0x8b, 0x08};
// IThumbnailProvider
IFACEMETHODIMP CBlendThumb::GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha)
{
ULONG BytesRead;
HRESULT hr = S_FALSE;
LARGE_INTEGER SeekPos;
// Compressed?
unsigned char in_magic[3];
_pStream->Read(&in_magic, 3, &BytesRead);
bool gzipped = true;
for (int i = 0; i < 3; i++)
if (in_magic[i] != gzip_magic[i]) {
gzipped = false;
break;
}
if (gzipped) {
// Zlib inflate
z_stream stream;
stream.zalloc = Z_NULL;
stream.zfree = Z_NULL;
stream.opaque = Z_NULL;
// Get compressed file length
SeekPos.QuadPart = 0;
_pStream->Seek(SeekPos, STREAM_SEEK_END, NULL);
// Get compressed and uncompressed size
uLong source_size;
uLongf dest_size;
// SeekPos.QuadPart = -4; // last 4 bytes define size of uncompressed file
// ULARGE_INTEGER Tell;
//_pStream->Seek(SeekPos,STREAM_SEEK_END,&Tell);
// source_size = (uLong)Tell.QuadPart + 4; // src
//_pStream->Read(&dest_size,4,&BytesRead); // dest
dest_size = 1024 * 70; // thumbnail is currently always inside the first 65KB...if it moves or
// enlargens this line will have to change or go!
source_size = (uLong)max(SeekPos.QuadPart, dest_size); // for safety, assume no compression
// Input
Bytef *src = new Bytef[source_size];
stream.next_in = (Bytef *)src;
stream.avail_in = (uInt)source_size;
// Output
Bytef *dest = new Bytef[dest_size];
stream.next_out = (Bytef *)dest;
stream.avail_out = dest_size;
// IStream to src
SeekPos.QuadPart = 0;
_pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL);
_pStream->Read(src, source_size, &BytesRead);
// Do the inflation
int err;
err = inflateInit2(&stream, 16); // 16 means "gzip"...nice!
err = inflate(&stream, Z_FINISH);
err = inflateEnd(&stream);
// Replace the IStream, which is read-only
_pStream->Release();
_pStream = SHCreateMemStream(dest, dest_size);
delete[] src;
delete[] dest;
}
// Blender version, early out if sub 2.5
SeekPos.QuadPart = 9;
_pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL);
char version[4];
version[3] = '\0';
_pStream->Read(&version, 3, &BytesRead);
if (BytesRead != 3) {
return E_UNEXPECTED;
}
int iVersion = atoi(version);
if (iVersion < 250) {
return S_FALSE;
}
// 32 or 64 bit blend?
SeekPos.QuadPart = 7;
_pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL);
char _PointerSize;
_pStream->Read(&_PointerSize, 1, &BytesRead);
int PointerSize = _PointerSize == '_' ? 4 : 8;
int HeaderSize = 16 + PointerSize;
// Find and read thumbnail ("TEST") block
SeekPos.QuadPart = 12;
_pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL);
int BlockOffset = 12;
while (_pStream) {
// Scan current block
char BlockName[5];
BlockName[4] = '\0';
int BlockSize = 0;
if (_pStream->Read(BlockName, 4, &BytesRead) == S_OK &&
_pStream->Read((void *)&BlockSize, 4, &BytesRead) == S_OK) {
if (strcmp(BlockName, "TEST") != 0) {
SeekPos.QuadPart = BlockOffset += HeaderSize + BlockSize;
_pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL);
continue;
}
}
else {
break; // eof
}
// Found the block
SeekPos.QuadPart = BlockOffset + HeaderSize;
_pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL);
int width, height;
_pStream->Read((char *)&width, 4, &BytesRead);
_pStream->Read((char *)&height, 4, &BytesRead);
BlockSize -= 8;
// Isolate RGBA data
char *pRGBA = new char[BlockSize];
_pStream->Read(pRGBA, BlockSize, &BytesRead);
if (BytesRead != (ULONG)BlockSize) {
return E_UNEXPECTED;
}
// Convert to BGRA for Windows
for (int i = 0; i < BlockSize; i += 4) {
#define RED_BYTE pRGBA[i]
#define BLUE_BYTE pRGBA[i + 2]
char red = RED_BYTE;
RED_BYTE = BLUE_BYTE;
BLUE_BYTE = red;
}
// Flip vertically (Blender stores it upside-down)
unsigned int LineSize = width * 4;
char *FlippedImage = new char[BlockSize];
for (int i = 0; i < height; i++) {
if (0 != memcpy_s(&FlippedImage[(height - i - 1) * LineSize],
LineSize,
&pRGBA[i * LineSize],
LineSize)) {
return E_UNEXPECTED;
}
}
delete[] pRGBA;
pRGBA = FlippedImage;
// Create image
*phbmp = CreateBitmap(width, height, 1, 32, pRGBA);
if (!*phbmp) {
return E_FAIL;
}
*pdwAlpha = WTSAT_ARGB; // it's actually BGRA, not sure why this works
// Scale down if required
if ((unsigned)width > cx || (unsigned)height > cx) {
float scale = 1.0f / (max(width, height) / (float)cx);
LONG NewWidth = (LONG)(width * scale);
LONG NewHeight = (LONG)(height * scale);
#ifdef _DEBUG
# if 0
MessageBox(0, "Attach now", "Debugging", MB_OK);
# endif
#endif
IWICImagingFactory *pImgFac;
hr = CoCreateInstance(
CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pImgFac));
IWICBitmap *WICBmp;
hr = pImgFac->CreateBitmapFromHBITMAP(*phbmp, 0, WICBitmapUseAlpha, &WICBmp);
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
bmi.bmiHeader.biWidth = NewWidth;
bmi.bmiHeader.biHeight = -NewHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
BYTE *pBits;
HBITMAP ResizedHBmp = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pBits, NULL, 0);
hr = ResizedHBmp ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr)) {
IWICBitmapScaler *pIScaler;
hr = pImgFac->CreateBitmapScaler(&pIScaler);
hr = pIScaler->Initialize(WICBmp, NewWidth, NewHeight, WICBitmapInterpolationModeFant);
WICRect rect = {0, 0, NewWidth, NewHeight};
hr = pIScaler->CopyPixels(&rect, NewWidth * 4, NewWidth * NewHeight * 4, pBits);
if (SUCCEEDED(hr)) {
DeleteObject(*phbmp);
*phbmp = ResizedHBmp;
}
else {
DeleteObject(ResizedHBmp);
}
pIScaler->Release();
}
WICBmp->Release();
pImgFac->Release();
}
else {
hr = S_OK;
}
break;
}
return hr;
}

View File

@ -0,0 +1,113 @@
/*
* 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.
*/
/** \file
* \ingroup blendthumb
*
* This file defines the thumbnail generation command (typically used on UNIX).
*
* 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:
*
* \code{.txt}
* [Thumbnailer Entry]
* TryExec=blender-thumbnailer
* Exec=blender-thumbnailer %u %o
* MimeType=application/x-blender;
* \endcode
*/
#include <fstream>
#include <optional>
#include <fcntl.h>
#ifndef WIN32
# include <unistd.h> /* For read close. */
#else
# include "BLI_winstuff.h"
# include "winsock2.h"
# include <io.h> /* For open close read. */
#endif
#include "BLI_fileops.h"
#include "BLI_filereader.h"
#include "BLI_vector.hh"
#include "blendthumb.hh"
/**
* This function opens .blend file from src_blend, extracts thumbnail from file if there is one,
* and writes `.png` image into `dst_png`.
* Returns exit code (0 if successful).
*/
static eThumbStatus extract_png_from_blend_file(const char *src_blend, const char *dst_png)
{
eThumbStatus err;
/* Open source file `src_blend`. */
const int src_file = BLI_open(src_blend, O_BINARY | O_RDONLY, 0);
if (src_file == -1) {
return BT_FILE_ERR;
}
/* Thumbnail reading is responsible for freeing `file` and closing `src_file`. */
FileReader *file = BLI_filereader_new_file(src_file);
if (file == nullptr) {
close(src_file);
return BT_FILE_ERR;
}
/* Extract thumbnail from file. */
Thumbnail thumb;
err = blendthumb_create_thumb_from_file(file, &thumb);
if (err != BT_OK) {
return err;
}
/* Write thumbnail to `dst_png`. */
const int dst_file = BLI_open(dst_png, O_BINARY | O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (dst_file == -1) {
return BT_FILE_ERR;
}
std::optional<blender::Vector<uint8_t>> png_buf_opt = blendthumb_create_png_data_from_thumb(
&thumb);
if (png_buf_opt == std::nullopt) {
err = BT_ERROR;
}
else {
blender::Vector<uint8_t> png_buf = *png_buf_opt;
err = (write(dst_file, png_buf.data(), png_buf.size()) == png_buf.size()) ? BT_OK :
BT_FILE_ERR;
}
close(dst_file);
return err;
}
int main(int argc, char *argv[])
{
if (argc < 3) {
std::cerr << "Usage: blender-thumbnailer <input.blend> <output.png>" << std::endl;
return -1;
}
eThumbStatus ret = extract_png_from_blend_file(argv[1], argv[2]);
return (int)ret;
}

View File

@ -0,0 +1,65 @@
/*
* 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.
*
* The Original Code is Copyright (C) 2008-2021 Blender Foundation.
* All rights reserved.
*/
/** \file
* \ingroup blendthumb
*
* Shared thumbnail extraction logic.
*
* Used for both MS-Windows DLL and Unix command line.
*/
#pragma once
#include <optional>
#include "BLI_array.hh"
#include "BLI_vector.hh"
struct FileReader;
struct Thumbnail {
blender::Array<uint8_t> data;
int width;
int height;
};
enum eThumbStatus {
BT_OK = 0,
BT_FILE_ERR = 1,
BT_COMPRES_ERR = 2,
BT_DECOMPRESS_ERR = 3,
BT_INVALID_FILE = 4,
BT_EARLY_VERSION = 5,
BT_INVALID_THUMB = 6,
BT_ERROR = 9
};
std::optional<blender::Vector<uint8_t>> blendthumb_create_png_data_from_thumb(
const Thumbnail *thumb);
eThumbStatus blendthumb_create_thumb_from_file(struct FileReader *rawfile, Thumbnail *thumb);
/* INTEGER CODES */
#ifdef __BIG_ENDIAN__
/* Big Endian */
# define MAKE_ID(a, b, c, d) ((int)(a) << 24 | (int)(b) << 16 | (c) << 8 | (d))
#else
/* Little Endian */
# define MAKE_ID(a, b, c, d) ((int)(d) << 24 | (int)(c) << 16 | (b) << 8 | (a))
#endif

View File

@ -0,0 +1,257 @@
/*
* 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.
*
* The Original Code is Copyright (C) 2008 Blender Foundation.
* All rights reserved.
*/
/** \file
* \ingroup blendthumb
*
* Expose #blendthumb_create_thumb_from_file that creates the PNG data
* but does not write it to a file.
*/
#include <cstring>
#include "BLI_alloca.h"
#include "BLI_endian_defines.h"
#include "BLI_endian_switch.h"
#include "BLI_fileops.h"
#include "BLI_filereader.h"
#include "BLI_string.h"
#include "blendthumb.hh"
static bool blend_header_check_magic(const char header[12])
{
/* Check magic string at start of file. */
if (!BLI_str_startswith(header, "BLENDER")) {
return false;
}
/* Check pointer size and endianness indicators. */
if (((header[7] != '_') && (header[7] != '-')) || ((header[8] != 'v') && (header[8] != 'V'))) {
return false;
}
/* Check version number. */
if (!isdigit(header[9]) || !isdigit(header[10]) || !isdigit(header[11])) {
return false;
}
return true;
}
static bool blend_header_is_version_valid(const char header[12])
{
/* Thumbnails are only in files with version >= 2.50 */
char num[4];
memcpy(num, header + 9, 3);
num[3] = 0;
return atoi(num) >= 250;
}
static int blend_header_pointer_size(const char header[12])
{
return header[7] == '_' ? 4 : 8;
}
static bool blend_header_is_endian_switch_needed(const char header[12])
{
return (((header[8] == 'v') ? L_ENDIAN : B_ENDIAN) != ENDIAN_ORDER);
}
static void thumb_data_vertical_flip(Thumbnail *thumb)
{
uint32_t *rect = (uint32_t *)thumb->data.data();
int x = thumb->width, y = thumb->height;
uint32_t *top = rect;
uint32_t *bottom = top + ((y - 1) * x);
uint32_t *line = (uint32_t *)malloc(x * sizeof(uint32_t));
y >>= 1;
for (; y > 0; y--) {
memcpy(line, top, x * sizeof(uint32_t));
memcpy(top, bottom, x * sizeof(uint32_t));
memcpy(bottom, line, x * sizeof(uint32_t));
bottom -= x;
top += x;
}
free(line);
}
static int32_t bytes_to_native_i32(const uint8_t bytes[4], bool endian_switch)
{
int32_t data;
memcpy(&data, bytes, 4);
if (endian_switch) {
BLI_endian_switch_int32(&data);
}
return data;
}
static bool file_read(FileReader *file, uint8_t *buf, size_t buf_len)
{
return (file->read(file, buf, buf_len) == buf_len);
}
static bool file_seek(FileReader *file, size_t len)
{
if (file->seek != nullptr) {
if (file->seek(file, len, SEEK_CUR) == -1) {
return false;
}
return true;
}
/* File doesn't support seeking (e.g. gzip), so read and discard in chunks. */
constexpr size_t dummy_data_size = 4096;
blender::Array<char> dummy_data(dummy_data_size);
while (len > 0) {
const size_t len_chunk = std::min(len, dummy_data_size);
if ((size_t)file->read(file, dummy_data.data(), len_chunk) != len_chunk) {
return false;
}
len -= len_chunk;
}
return true;
}
static eThumbStatus blendthumb_extract_from_file_impl(FileReader *file,
Thumbnail *thumb,
const size_t bhead_size,
const bool endian_switch)
{
/* Iterate over file blocks until we find the thumbnail or run out of data. */
uint8_t *bhead_data = (uint8_t *)BLI_array_alloca(bhead_data, bhead_size);
while (file_read(file, bhead_data, bhead_size)) {
/* Parse type and size from `BHead`. */
const int32_t block_size = bytes_to_native_i32(&bhead_data[4], endian_switch);
/* We're looking for the thumbnail, so skip any other block. */
switch (*((int32_t *)bhead_data)) {
case MAKE_ID('T', 'E', 'S', 'T'): {
uint8_t shape[8];
if (!file_read(file, shape, sizeof(shape))) {
return BT_INVALID_THUMB;
}
thumb->width = bytes_to_native_i32(&shape[0], endian_switch);
thumb->height = bytes_to_native_i32(&shape[4], endian_switch);
/* Verify that image dimensions and data size make sense. */
size_t data_size = block_size - 8;
const size_t expected_size = thumb->width * thumb->height * 4;
if (thumb->width < 0 || thumb->height < 0 || data_size != expected_size) {
return BT_INVALID_THUMB;
}
thumb->data = blender::Array<uint8_t>(data_size);
if (!file_read(file, thumb->data.data(), data_size)) {
return BT_INVALID_THUMB;
}
return BT_OK;
}
case MAKE_ID('R', 'E', 'N', 'D'): {
if (!file_seek(file, block_size)) {
return BT_INVALID_THUMB;
}
/* Check the next block. */
break;
}
default: {
/* Early exit if there are no `TEST` or `REND` blocks.
* This saves scanning the entire blend file which could be slow. */
return BT_INVALID_THUMB;
}
}
}
return BT_INVALID_THUMB;
}
/**
* This function extracts the thumbnail from the .blend file into thumb.
* Returns #BT_OK for success and the relevant error code otherwise.
*/
eThumbStatus blendthumb_create_thumb_from_file(FileReader *rawfile, Thumbnail *thumb)
{
/* Read header in order to identify file type. */
char header[12];
if (rawfile->read(rawfile, header, sizeof(header)) != sizeof(header)) {
rawfile->close(rawfile);
return BT_ERROR;
}
/* Rewind the file after reading the header. */
rawfile->seek(rawfile, 0, SEEK_SET);
/* Try to identify the file type from the header. */
FileReader *file = nullptr;
if (BLI_str_startswith(header, "BLENDER")) {
file = rawfile;
rawfile = nullptr;
}
else if (BLI_file_magic_is_gzip(header)) {
file = BLI_filereader_new_gzip(rawfile);
if (file != nullptr) {
rawfile = nullptr; /* The Gzip #FileReader takes ownership of raw-file. */
}
}
else if (BLI_file_magic_is_zstd(header)) {
file = BLI_filereader_new_zstd(rawfile);
if (file != nullptr) {
rawfile = nullptr; /* The Zstd #FileReader takes ownership of raw-file. */
}
}
/* Clean up rawfile if it wasn't taken over. */
if (rawfile != nullptr) {
rawfile->close(rawfile);
}
if (file == nullptr) {
return BT_ERROR;
}
/* Re-read header in case we had compression. */
if (file->read(file, header, sizeof(header)) != sizeof(header)) {
file->close(file);
return BT_ERROR;
}
/* Check if the header format is valid for a .blend file. */
if (!blend_header_check_magic(header)) {
file->close(file);
return BT_INVALID_FILE;
}
/* Check if the file is new enough to contain a thumbnail. */
if (!blend_header_is_version_valid(header)) {
file->close(file);
return BT_EARLY_VERSION;
}
/* Depending on where it was saved, the file can use different pointer size or endianness. */
int bhead_size = 16 + blend_header_pointer_size(header);
const bool endian_switch = blend_header_is_endian_switch_needed(header);
/* Read the thumbnail. */
eThumbStatus err = blendthumb_extract_from_file_impl(file, thumb, bhead_size, endian_switch);
file->close(file);
if (err != BT_OK) {
return err;
}
thumb_data_vertical_flip(thumb);
return BT_OK;
}

View File

@ -0,0 +1,158 @@
/*
* 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.
*
* The Original Code is Copyright (C) 2008 Blender Foundation.
* All rights reserved.
*/
/** \file
* \ingroup blendthumb
*
* Expose #blendthumb_create_png_data_from_thumb that creates the PNG data
* but does not write it to a file.
*/
#include <cstring>
#include <optional>
#include <zlib.h>
#include "blendthumb.hh"
#include "BLI_endian_defines.h"
#include "BLI_endian_switch.h"
#include "BLI_vector.hh"
static void png_extend_native_int32(blender::Vector<uint8_t> &output, int32_t data)
{
if (ENDIAN_ORDER == L_ENDIAN) {
BLI_endian_switch_int32(&data);
}
output.extend_unchecked(blender::Span((uint8_t *)&data, 4));
}
/** The number of bytes each chunk uses on top of the data that's written. */
#define PNG_CHUNK_EXTRA 12
static void png_chunk_create(blender::Vector<uint8_t> &output,
const uint32_t tag,
const blender::Vector<uint8_t> &data)
{
uint32_t crc = crc32(0, nullptr, 0);
crc = crc32(crc, (uint8_t *)&tag, sizeof(tag));
crc = crc32(crc, (uint8_t *)data.data(), data.size());
png_extend_native_int32(output, data.size());
output.extend_unchecked(blender::Span((uint8_t *)&tag, sizeof(tag)));
output.extend_unchecked(data);
png_extend_native_int32(output, crc);
}
static blender::Vector<uint8_t> filtered_rows_from_thumb(const Thumbnail *thumb)
{
/* In the image data sent to the compression step, each scan-line is preceded by a filter type
* byte containing the numeric code of the filter algorithm used for that scan-line. */
const size_t line_size = thumb->width * 4;
blender::Vector<uint8_t> filtered{};
size_t final_size = thumb->height * (line_size + 1);
filtered.reserve(final_size);
for (int i = 0; i < thumb->height; i++) {
filtered.append_unchecked(0x00);
filtered.extend_unchecked(blender::Span(&thumb->data[i * line_size], line_size));
}
BLI_assert(final_size == filtered.size());
return filtered;
}
static std::optional<blender::Vector<uint8_t>> zlib_compress(const blender::Vector<uint8_t> &data)
{
unsigned long uncompressed_size = data.size();
uLongf compressed_size = compressBound(uncompressed_size);
blender::Vector<uint8_t> compressed(compressed_size, 0x00);
int return_value = compress2((uchar *)compressed.data(),
&compressed_size,
(uchar *)data.data(),
uncompressed_size,
Z_NO_COMPRESSION);
if (return_value != Z_OK) {
/* Something went wrong with compression of data. */
return std::nullopt;
}
compressed.resize(compressed_size);
return compressed;
}
std::optional<blender::Vector<uint8_t>> blendthumb_create_png_data_from_thumb(
const Thumbnail *thumb)
{
if (thumb->data.is_empty()) {
return std::nullopt;
}
/* Create `IDAT` chunk data. */
blender::Vector<uint8_t> image_data{};
{
auto image_data_opt = zlib_compress(filtered_rows_from_thumb(thumb));
if (image_data_opt == std::nullopt) {
return std::nullopt;
}
image_data = *image_data_opt;
}
/* Create the IHDR chunk data. */
blender::Vector<uint8_t> ihdr_data{};
{
const size_t ihdr_data_final_size = 4 + 4 + 5;
ihdr_data.reserve(ihdr_data_final_size);
png_extend_native_int32(ihdr_data, thumb->width);
png_extend_native_int32(ihdr_data, thumb->height);
ihdr_data.extend_unchecked({
0x08, /* Bit Depth. */
0x06, /* Color Type. */
0x00, /* Compression method. */
0x00, /* Filter method. */
0x00, /* Interlace method. */
});
BLI_assert((size_t)ihdr_data.size() == ihdr_data_final_size);
}
/* Join it all together to create a PNG image. */
blender::Vector<uint8_t> png_buf{};
{
const size_t png_buf_final_size = (
/* Header. */
8 +
/* `IHDR` chunk. */
(ihdr_data.size() + PNG_CHUNK_EXTRA) +
/* `IDAT` chunk. */
(image_data.size() + PNG_CHUNK_EXTRA) +
/* `IEND` chunk. */
PNG_CHUNK_EXTRA);
png_buf.reserve(png_buf_final_size);
/* This is the standard PNG file header. Every PNG file starts with it. */
png_buf.extend_unchecked({0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A});
png_chunk_create(png_buf, MAKE_ID('I', 'H', 'D', 'R'), ihdr_data);
png_chunk_create(png_buf, MAKE_ID('I', 'D', 'A', 'T'), image_data);
png_chunk_create(png_buf, MAKE_ID('I', 'E', 'N', 'D'), {});
BLI_assert((size_t)png_buf.size() == png_buf_final_size);
}
return png_buf;
}

View File

@ -0,0 +1,237 @@
/*
* 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.
*/
/** \file
* \ingroup blendthumb
*
* Thumbnail from Blend file extraction for MS-Windows.
*/
#include <math.h>
#include <new>
#include <shlwapi.h>
#include <string>
#include <thumbcache.h> /* for #IThumbnailProvider */
#include "Wincodec.h"
#include "blendthumb.hh"
#include "BLI_filereader.h"
#pragma comment(lib, "shlwapi.lib")
/**
* This thumbnail provider implements #IInitializeWithStream to enable being hosted
* in an isolated process for robustness.
*/
class CBlendThumb : public IInitializeWithStream, public IThumbnailProvider {
public:
CBlendThumb() : _cRef(1), _pStream(NULL)
{
}
virtual ~CBlendThumb()
{
if (_pStream) {
_pStream->Release();
}
}
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
static const QITAB qit[] = {
QITABENT(CBlendThumb, IInitializeWithStream),
QITABENT(CBlendThumb, IThumbnailProvider),
{0},
};
return QISearch(this, qit, riid, ppv);
}
IFACEMETHODIMP_(ULONG) AddRef()
{
return InterlockedIncrement(&_cRef);
}
IFACEMETHODIMP_(ULONG) Release()
{
ULONG cRef = InterlockedDecrement(&_cRef);
if (!cRef) {
delete this;
}
return cRef;
}
/** IInitializeWithStream */
IFACEMETHODIMP Initialize(IStream *pStream, DWORD grfMode);
/** IThumbnailProvider */
IFACEMETHODIMP GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha);
private:
long _cRef;
IStream *_pStream; /* provided in Initialize(). */
};
HRESULT CBlendThumb_CreateInstance(REFIID riid, void **ppv)
{
CBlendThumb *pNew = new (std::nothrow) CBlendThumb();
HRESULT hr = pNew ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr)) {
hr = pNew->QueryInterface(riid, ppv);
pNew->Release();
}
return hr;
}
IFACEMETHODIMP CBlendThumb::Initialize(IStream *pStream, DWORD)
{
if (_pStream != NULL) {
/* Can only be initialized once. */
return E_UNEXPECTED;
}
/* Take a reference to the stream. */
return pStream->QueryInterface(&_pStream);
}
/**
* #FileReader compatible wrapper around the Windows stream that gives access to the .blend file.
*/
typedef struct {
FileReader reader;
IStream *_pStream;
} StreamReader;
static ssize_t stream_read(FileReader *reader, void *buffer, size_t size)
{
StreamReader *stream = (StreamReader *)reader;
ULONG readsize;
stream->_pStream->Read(buffer, size, &readsize);
stream->reader.offset += readsize;
return (ssize_t)readsize;
}
static off64_t stream_seek(FileReader *reader, off64_t offset, int whence)
{
StreamReader *stream = (StreamReader *)reader;
DWORD origin = STREAM_SEEK_SET;
switch (whence) {
case SEEK_CUR:
origin = STREAM_SEEK_CUR;
break;
case SEEK_END:
origin = STREAM_SEEK_END;
break;
}
LARGE_INTEGER offsetI;
offsetI.QuadPart = offset;
ULARGE_INTEGER newPos;
stream->_pStream->Seek(offsetI, origin, &newPos);
stream->reader.offset = newPos.QuadPart;
return stream->reader.offset;
}
static void stream_close(FileReader *reader)
{
StreamReader *stream = (StreamReader *)reader;
delete stream;
}
IFACEMETHODIMP CBlendThumb::GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha)
{
HRESULT hr = S_FALSE;
StreamReader *file = new StreamReader;
file->reader.read = stream_read;
file->reader.seek = stream_seek;
file->reader.close = stream_close;
file->reader.offset = 0;
file->_pStream = _pStream;
file->reader.seek(&file->reader, 0, SEEK_SET);
/* Extract thumbnail from stream. */
Thumbnail thumb;
if (blendthumb_create_thumb_from_file(&file->reader, &thumb) != BT_OK) {
return S_FALSE;
}
/* Convert to BGRA for Windows. */
for (int i = 0; i < thumb.width * thumb.height; i++) {
std::swap(thumb.data[4 * i], thumb.data[4 * i + 2]);
}
*phbmp = CreateBitmap(thumb.width, thumb.height, 1, 32, thumb.data.data());
if (!*phbmp) {
return E_FAIL;
}
*pdwAlpha = WTSAT_ARGB;
/* Scale down the thumbnail if required. */
if ((unsigned)thumb.width > cx || (unsigned)thumb.height > cx) {
float scale = 1.0f / (std::max(thumb.width, thumb.height) / (float)cx);
LONG NewWidth = (LONG)(thumb.width * scale);
LONG NewHeight = (LONG)(thumb.height * scale);
IWICImagingFactory *pImgFac;
hr = CoCreateInstance(
CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pImgFac));
IWICBitmap *WICBmp;
hr = pImgFac->CreateBitmapFromHBITMAP(*phbmp, 0, WICBitmapUseAlpha, &WICBmp);
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
bmi.bmiHeader.biWidth = NewWidth;
bmi.bmiHeader.biHeight = -NewHeight;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
BYTE *pBits;
HBITMAP ResizedHBmp = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pBits, NULL, 0);
hr = ResizedHBmp ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr)) {
IWICBitmapScaler *pIScaler;
hr = pImgFac->CreateBitmapScaler(&pIScaler);
hr = pIScaler->Initialize(WICBmp, NewWidth, NewHeight, WICBitmapInterpolationModeFant);
WICRect rect = {0, 0, NewWidth, NewHeight};
hr = pIScaler->CopyPixels(&rect, NewWidth * 4, NewWidth * NewHeight * 4, pBits);
if (SUCCEEDED(hr)) {
DeleteObject(*phbmp);
*phbmp = ResizedHBmp;
}
else {
DeleteObject(ResizedHBmp);
}
pIScaler->Release();
}
WICBmp->Release();
pImgFac->Release();
}
else {
hr = S_OK;
}
return hr;
}

View File

@ -516,8 +516,7 @@ if(UNIX AND NOT APPLE)
)
install(
PROGRAMS
${CMAKE_SOURCE_DIR}/release/bin/blender-thumbnailer.py
TARGETS blender-thumbnailer
DESTINATION "."
)
@ -560,7 +559,7 @@ if(UNIX AND NOT APPLE)
DESTINATION share/icons/hicolor/symbolic/apps
)
install(
PROGRAMS ${CMAKE_SOURCE_DIR}/release/bin/blender-thumbnailer.py
TARGETS blender-thumbnailer
DESTINATION bin
)
set(BLENDER_TEXT_FILES_DESTINATION share/doc/blender)