Skip to content

Commit

Permalink
v0.2.1 update (#5)
Browse files Browse the repository at this point in the history
* Added "Image Filter" option. (Possible to use cubic filter for mip generation.)

* Fixed a bug that Blender can't remove the addon after using it in the process.

* Improved error messages for unsupported files.
  • Loading branch information
matyalatte authored Jan 28, 2023
1 parent 39121b1 commit ea1e1f3
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 46 deletions.
79 changes: 39 additions & 40 deletions addons/blender_dds_addon/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""Blender addon to import .dds files."""
import importlib
from pathlib import Path

from .ui import import_dds, export_dds, custom_properties, preferences
from .directx.texconv import unload_texconv

bl_info = {
'name': 'DDS textures',
'author': 'Matyalatte',
'version': (0, 2, 0),
'version': (0, 2, 1),
'blender': (2, 83, 20),
'location': 'Image Editor > Sidebar > DDS Tab',
'description': 'Import and export .dds files',
Expand All @@ -12,42 +17,36 @@
'category': 'Import-Export',
}

try:
def reload_package(module_dict_main):
"""Reload Scripts."""
import importlib
from pathlib import Path

def reload_package_recursive(current_dir, module_dict):
for path in current_dir.iterdir():
if "__init__" in str(path) or path.stem not in module_dict:
continue
if path.is_file() and path.suffix == ".py":
importlib.reload(module_dict[path.stem])
elif path.is_dir():
reload_package_recursive(path, module_dict[path.stem].__dict__)

reload_package_recursive(Path(__file__).parent, module_dict_main)

if ".import_dds" in locals():
reload_package(locals())

from .ui import import_dds, export_dds, custom_properties, preferences

def register():
"""Add addon."""
preferences.register()
import_dds.register()
export_dds.register()
custom_properties.register()

def unregister():
"""Remove addon."""
preferences.unregister()
import_dds.unregister()
export_dds.unregister()
custom_properties.unregister()

except ModuleNotFoundError as exc:
print(exc)
print('Failed to load the addon.')

def reload_package(module_dict_main):
def reload_package_recursive(current_dir, module_dict):
for path in current_dir.iterdir():
if "__init__" in str(path) or path.stem not in module_dict:
continue
if path.is_file() and path.suffix == ".py":
importlib.reload(module_dict[path.stem])
elif path.is_dir():
reload_package_recursive(path, module_dict[path.stem].__dict__)

reload_package_recursive(Path(__file__).parent, module_dict_main)


if ".import_dds" in locals():
reload_package(locals())


def register():
"""Add addon."""
preferences.register()
import_dds.register()
export_dds.register()
custom_properties.register()


def unregister():
"""Remove addon."""
preferences.unregister()
import_dds.unregister()
export_dds.unregister()
custom_properties.unregister()
unload_texconv()
11 changes: 9 additions & 2 deletions addons/blender_dds_addon/directx/dds.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,17 @@ def read(f):
raise RuntimeError(f"Unsupported DXGI format detected. ({fmt})\n" + ERR_MSG)

head.dxgi_format = DXGI_FORMAT(fmt) # dxgiFormat
util.read_const_uint32(f, 3) # resourceDimension == 3
resource_dimension = util.read_uint32(f)
f.seek(4, 1) # miscFlag == 0 or 4 (0 for 2D textures, 4 for Cube maps)
util.read_const_uint32(f, 1) # arraySize == 1
array_size = util.read_uint32(f)
f.seek(4, 1) # miscFlag2

# Raise errors for unsupported files
if resource_dimension == 2:
raise RuntimeError("1D textures are unsupported.")
if resource_dimension == 4:
raise RuntimeError("3D textures are unsupported.")
util.check(array_size, 1, msg="Texture arrays are unsupported.")
else:
head.dxgi_format = head.get_dxgi_from_header()
return head
Expand Down
56 changes: 56 additions & 0 deletions addons/blender_dds_addon/directx/texconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,63 @@
And put the dll in the same directory as texconv.py.
"""
import ctypes
from ctypes.util import find_library
import os
import tempfile

from .dds import DDSHeader, is_hdr
from .dxgi_format import DXGI_FORMAT
from . import util

DLL = None


def get_dll_close():
"""Get dll function to unload DLL."""
if util.is_windows():
return ctypes.windll.kernel32.FreeLibrary
else:
dlpath = find_library("c")
if dlpath is None:
dlpath = find_library("System")
elif dlpath is None:
# Failed to find the path to stdlib.
return None

try:
stdlib = ctypes.CDLL(dlpath)
return stdlib.dlclose
except OSError:
# Failed to load stdlib.
return None


def unload_texconv():
global DLL
if DLL is None:
return

dll_close = get_dll_close()
if dll_close is None:
raise RuntimeError("Failed to unload DLL. Restart Blender if you will remove the addon.")

handle = DLL._handle
dll_close.argtypes = [ctypes.c_void_p]
dll_close(handle)
DLL = None


class Texconv:
"""Texture converter."""
def __init__(self, dll_path=None):
self.load_dll(dll_path=dll_path)

def load_dll(self, dll_path=None):
global DLL
if DLL is not None:
self.dll = DLL
return

if dll_path is None:
file_path = os.path.realpath(__file__)
if util.is_windows():
Expand All @@ -40,9 +83,17 @@ def load_dll(self, dll_path=None):
raise RuntimeError(f'texconv not found. ({dll_path})')

self.dll = ctypes.cdll.LoadLibrary(dll_path)
DLL = self.dll

def unload_dll(self):
unload_texconv()
self.dll = None

def convert_to_tga(self, file, out=None, cubemap_layout="h-cross", invert_normals=False, verbose=True):
"""Convert dds to tga."""
if self.dll is None:
raise RuntimeError("texconv is unloaded.")

dds_header = DDSHeader.read_from_file(file)

if dds_header.is_3d():
Expand Down Expand Up @@ -86,10 +137,13 @@ def convert_to_tga(self, file, out=None, cubemap_layout="h-cross", invert_normal

def convert_to_dds(self, file, dds_fmt, out=None,
invert_normals=False, no_mip=False,
image_filter="LINEAR",
export_as_cubemap=False,
cubemap_layout="h-cross",
verbose=True, allow_slow_codec=False):
"""Convert texture to dds."""
if self.dll is None:
raise RuntimeError("texconv is unloaded.")

ext = util.get_ext(file)

Expand All @@ -110,6 +164,8 @@ def convert_to_dds(self, file, dds_fmt, out=None,
args = ['-f', dds_fmt]
if no_mip:
args += ['-m', '1']
if image_filter != "LINEAR":
args += ["-if", image_filter]

if ("BC5" in dds_fmt) and invert_normals:
args += ['-inverty']
Expand Down
11 changes: 11 additions & 0 deletions addons/blender_dds_addon/ui/custom_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ class DDSOptions(PropertyGroup):
default=False,
)

image_filter: EnumProperty(
name='Image Filter',
description="Image filter for mipmap generation",
items=[
('POINT', 'Point', 'Nearest neighbor'),
('LINEAR', 'Linear', 'Bilinear interpolation (or box filter)'),
('CUBIC', 'Cubic', 'Bicubic interpolation'),
],
default='LINEAR',
)

allow_slow_codec: BoolProperty(
name='Allow Slow Codec',
description=("Allow to use CPU codec for BC6 and BC7.\n"
Expand Down
4 changes: 4 additions & 0 deletions addons/blender_dds_addon/ui/export_dds.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@


def save_dds(tex, file, dds_fmt, invert_normals=False, no_mip=False,
image_filter='LINEAR',
allow_slow_codec=False,
export_as_cubemap=False,
cubemap_layout='h-cross',
Expand Down Expand Up @@ -102,6 +103,7 @@ def save_temp_dds(tex, temp_dir, ext, fmt, texconv, verbose=True):

temp_dds = texconv.convert_to_dds(temp, dds_fmt, out=temp_dir,
invert_normals=invert_normals, no_mip=no_mip,
image_filter=image_filter,
export_as_cubemap=export_as_cubemap,
cubemap_layout=cubemap_layout,
allow_slow_codec=allow_slow_codec, verbose=verbose)
Expand Down Expand Up @@ -153,6 +155,7 @@ def export_as_dds(context, tex, file):

save_dds(tex, file, dxgi,
invert_normals=dds_options.invert_normals, no_mip=no_mip,
image_filter=dds_options.image_filter,
allow_slow_codec=dds_options.allow_slow_codec,
export_as_cubemap=is_cube,
cubemap_layout=cubemap_layout)
Expand All @@ -162,6 +165,7 @@ def put_export_options(context, layout):
dds_options = context.scene.dds_options
if not dds_properties_exist():
layout.prop(dds_options, 'dxgi_format')
layout.prop(dds_options, 'image_filter')
layout.prop(dds_options, 'invert_normals')
if not dds_properties_exist():
layout.prop(dds_options, 'no_mip')
Expand Down
5 changes: 5 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
ver 0.2.1
- Fixed a bug that Blender can't remove the addon after using it in the process.
- Added "Image Filter" option. (Possible to use cubic filter for mip generation.)
- Improved error messages for unsupported files.

ver 0.2.0
- Added "Import from a Directory" operation
- Added "Export All Images" operation
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Blender-DDS-Addon v0.2.0
# Blender-DDS-Addon v0.2.1

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![build](https://github.com/matyalatte/Blender-DDS-Addon/actions/workflows/build.yml/badge.svg)
Expand Down
2 changes: 1 addition & 1 deletion external/Texconv-Custom-DLL
21 changes: 19 additions & 2 deletions tests/test_dds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from blender_dds_addon.ui import import_dds
from blender_dds_addon.ui import export_dds
from blender_dds_addon.ui import custom_properties

from blender_dds_addon.directx.texconv import unload_texconv
import bpy
bpy.utils.register_class(custom_properties.DDSCustomProperties)
custom_properties.add_custom_props_for_dds()
Expand All @@ -16,7 +16,16 @@ def get_test_dds():
return test_file


@pytest.mark.parametrize('export_format', ["BC4_UNORM", "B8G8R8A8_UNORM_SRGB", "R16G16B16A16_FLOAT"])
def test_unload_empty_dll():
unload_texconv()


def test_unload_dll():
import_dds.load_dds(get_test_dds())
unload_texconv()


@pytest.mark.parametrize("export_format", ["BC4_UNORM", "B8G8R8A8_UNORM_SRGB", "R16G16B16A16_FLOAT"])
def test_io(export_format):
"""Cehck if the addon can import and export dds."""
tex = import_dds.load_dds(get_test_dds())
Expand Down Expand Up @@ -44,3 +53,11 @@ def test_io_cubemap():
tex = import_dds.load_dds(os.path.join("tests", "cube.dds"))
tex = export_dds.save_dds(tex, "saved.dds", "BC1_UNORM", export_as_cubemap=True)
os.remove("saved.dds")


@pytest.mark.parametrize("image_filter", ["POINT", "CUBIC"])
def test_io_filter(image_filter):
"""Test filter options."""
tex = import_dds.load_dds(get_test_dds())
tex = export_dds.save_dds(tex, "saved.dds", "BC1_UNORM", image_filter=image_filter)
os.remove("saved.dds")

0 comments on commit ea1e1f3

Please sign in to comment.