diff --git a/blenderkit/image_utils.py b/blenderkit/image_utils.py
index 214168af81f614930e525d4149b88a6df72a7fe1..00c6191786434535974ed1fe3781aa275c690e1e 100644
--- a/blenderkit/image_utils.py
+++ b/blenderkit/image_utils.py
@@ -1,5 +1,6 @@
 import bpy
 import os
+import time
 
 def get_orig_render_settings():
     rs = bpy.context.scene.render
@@ -98,3 +99,391 @@ def generate_hdr_thumbnail():
         inew.scale(thumbnailWidth, thumbnailHeight)
 
     img_save_as(inew, filepath=inew.filepath)
+
+
+def find_color_mode(image):
+    if not isinstance(image, bpy.types.Image):
+        raise(TypeError)
+    else:
+        depth_mapping = {
+            8: 'BW',
+            24: 'RGB',
+            32: 'RGBA',#can also be bw.. but image.channels doesn't work.
+            96: 'RGB',
+            128: 'RGBA',
+        }
+        return depth_mapping.get(image.depth,'RGB')
+
+def find_image_depth(image):
+    if not isinstance(image, bpy.types.Image):
+        raise(TypeError)
+    else:
+        depth_mapping = {
+            8: '8',
+            24: '8',
+            32: '8',#can also be bw.. but image.channels doesn't work.
+            96: '16',
+            128: '16',
+        }
+        return depth_mapping.get(image.depth,'8')
+
+def can_erase_alpha(na):
+    alpha = na[3::4]
+    alpha_sum = alpha.sum()
+    if alpha_sum == alpha.size:
+        print('image can have alpha erased')
+    # print(alpha_sum, alpha.size)
+    return alpha_sum == alpha.size
+
+
+def is_image_black(na):
+    r = na[::4]
+    g = na[1::4]
+    b = na[2::4]
+
+    rgbsum = r.sum() + g.sum() + b.sum()
+
+    # print('rgb sum', rgbsum, r.sum(), g.sum(), b.sum())
+    if rgbsum == 0:
+        print('image can have alpha channel dropped')
+    return rgbsum == 0
+
+def is_image_bw(na):
+    r = na[::4]
+    g = na[1::4]
+    b = na[2::4]
+
+    rg_equal = r == g
+    gb_equal = g == b
+    rgbequal = rg_equal.all() and gb_equal.all()
+    if rgbequal:
+        print('image is black and white, can have channels reduced')
+
+    return rgbequal
+
+
+def numpytoimage(a, iname, width=0, height=0, channels=3):
+    t = time.time()
+    foundimage = False
+
+    for image in bpy.data.images:
+
+        if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]:
+            i = image
+            foundimage = True
+    if not foundimage:
+        if channels == 4:
+            bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0, 1), alpha=True,
+                              generated_type='BLANK', float=True)
+        if channels == 3:
+            bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0), alpha=False,
+                              generated_type='BLANK', float=True)
+
+    i = None
+
+    for image in bpy.data.images:
+        # print(image.name[:len(iname)],iname, image.size[0],a.shape[0],image.size[1],a.shape[1])
+        if image.name[:len(iname)] == iname and image.size[0] == width and image.size[1] == height:
+            i = image
+    if i is None:
+        i = bpy.data.images.new(iname, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False, tiled=False)
+
+    # dropping this re-shaping code -  just doing flat array for speed and simplicity
+    #    d = a.shape[0] * a.shape[1]
+    #    a = a.swapaxes(0, 1)
+    #    a = a.reshape(d)
+    #    a = a.repeat(channels)
+    #    a[3::4] = 1
+    i.pixels.foreach_set(a)  # this gives big speedup!
+    print('\ntime ' + str(time.time() - t))
+    return i
+
+
+def imagetonumpy_flat(i):
+    t = time.time()
+
+    import numpy
+
+    width = i.size[0]
+    height = i.size[1]
+    # print(i.channels)
+
+    size = width * height * i.channels
+    na = numpy.empty(size, numpy.float32)
+    i.pixels.foreach_get(na)
+
+    # dropping this re-shaping code -  just doing flat array for speed and simplicity
+    #    na = na[::4]
+    #    na = na.reshape(height, width, i.channels)
+    #    na = na.swapaxnes(0, 1)
+
+    # print('\ntime of image to numpy ' + str(time.time() - t))
+    return na
+
+def imagetonumpy(i):
+    t = time.time()
+
+    import numpy as np
+
+    width = i.size[0]
+    height = i.size[1]
+    # print(i.channels)
+
+    size = width * height * i.channels
+    na = np.empty(size, np.float32)
+    i.pixels.foreach_get(na)
+
+    # dropping this re-shaping code -  just doing flat array for speed and simplicity
+    # na = na[::4]
+    na = na.reshape(height, width, i.channels)
+    na = na.swapaxes(0, 1)
+
+    # print('\ntime of image to numpy ' + str(time.time() - t))
+    return na
+
+
+def downscale(i):
+    minsize = 128
+
+    sx, sy = i.size[:]
+    sx = round(sx / 2)
+    sy = round(sy / 2)
+    if sx > minsize and sy > minsize:
+        i.scale(sx, sy)
+
+
+def get_rgb_mean(i):
+    '''checks if normal map values are ok.'''
+    import numpy
+
+    na = imagetonumpy_flat(i)
+
+    r = na[::4]
+    g = na[1::4]
+    b = na[2::4]
+
+    rmean = r.mean()
+    gmean = g.mean()
+    bmean = b.mean()
+
+    rmedian = numpy.median(r)
+    gmedian = numpy.median(g)
+    bmedian = numpy.median(b)
+
+    #    return(rmedian,gmedian, bmedian)
+    return (rmean, gmean, bmean)
+
+def check_nmap_mean_ok(i):
+    '''checks if normal map values are in standard range.'''
+
+    rmean,gmean,bmean = get_rgb_mean(i)
+
+    #we could/should also check blue, but some ogl substance exports have 0-1, while 90% nmaps have 0.5 - 1.
+    nmap_ok = 0.45< rmean < 0.55 and .45 < gmean < .55
+
+    return nmap_ok
+
+
+def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
+    '''
+    checks if normal map is directX or OpenGL.
+    Returns - String value - DirectX and OpenGL
+    '''
+    import numpy
+    width = i.size[0]
+    height = i.size[1]
+
+
+
+    rmean, gmean, bmean = get_rgb_mean(i)
+
+    na = imagetonumpy(i)
+
+    if mask:
+        mask = imagetonumpy(mask)
+
+    red_x_comparison = numpy.zeros((width, height), numpy.float32)
+    green_y_comparison = numpy.zeros((width, height), numpy.float32)
+
+    if generated_test_images:
+        red_x_comparison_img = numpy.empty((width, height, 4), numpy.float32) #images for debugging purposes
+        green_y_comparison_img = numpy.empty((width, height, 4), numpy.float32)#images for debugging purposes
+
+    ogl = numpy.zeros((width, height), numpy.float32)
+    dx = numpy.zeros((width, height), numpy.float32)
+
+    if generated_test_images:
+        ogl_img = numpy.empty((width, height, 4), numpy.float32)  # images for debugging purposes
+        dx_img = numpy.empty((width, height, 4), numpy.float32)  # images for debugging purposes
+
+    for y in range(0, height):
+        for x in range(0, width):
+            #try to mask with UV mask image
+            if mask is None or mask[x,y,3]>0:
+
+                last_height_x = ogl[max(x - 1, 0), min(y, height - 1)]
+                last_height_y = ogl[max(x,0), min(y - 1,height-1)]
+
+                diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5)))
+                diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5)))
+                calc_height = (last_height_x + last_height_y) \
+                         -  diff_x - diff_y
+                calc_height = calc_height /2
+                ogl[x, y] = calc_height
+                if generated_test_images:
+                    rgb = calc_height *.1 +.5
+                    ogl_img[x,y] = [rgb,rgb,rgb,1]
+
+                # green channel
+                last_height_x = dx[max(x - 1, 0), min(y, height - 1)]
+                last_height_y = dx[max(x, 0), min(y - 1, height - 1)]
+
+                diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5)))
+                diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5)))
+                calc_height = (last_height_x + last_height_y) \
+                              - diff_x + diff_y
+                calc_height = calc_height / 2
+                dx[x, y] = calc_height
+                if generated_test_images:
+                    rgb = calc_height * .1 + .5
+                    dx_img[x, y] = [rgb, rgb, rgb, 1]
+
+
+    ogl_std = ogl.std()
+    dx_std = dx.std()
+
+    # print(mean_ogl, mean_dx)
+    # print(max_ogl, max_dx)
+    print(ogl_std, dx_std)
+    print(i.name)
+    #    if abs(mean_ogl) > abs(mean_dx):
+    if abs(ogl_std) > abs(dx_std):
+        print('this is probably a DirectX texture')
+    else:
+        print('this is probably an OpenGL texture')
+
+
+    if generated_test_images:
+        # red_x_comparison_img = red_x_comparison_img.swapaxes(0,1)
+        # red_x_comparison_img = red_x_comparison_img.flatten()
+        #
+        # green_y_comparison_img = green_y_comparison_img.swapaxes(0,1)
+        # green_y_comparison_img = green_y_comparison_img.flatten()
+        #
+        # numpytoimage(red_x_comparison_img, 'red_' + i.name, width=width, height=height, channels=1)
+        # numpytoimage(green_y_comparison_img, 'green_' + i.name, width=width, height=height, channels=1)
+
+        ogl_img = ogl_img.swapaxes(0, 1)
+        ogl_img = ogl_img.flatten()
+
+        dx_img = dx_img.swapaxes(0, 1)
+        dx_img = dx_img.flatten()
+
+        numpytoimage(ogl_img, 'OpenGL', width=width, height=height, channels=1)
+        numpytoimage(dx_img, 'DirectX', width=width, height=height, channels=1)
+
+    if abs(ogl_std) > abs(dx_std):
+            return 'DirectX'
+    return 'OpenGL'
+
+def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=False, do_downscale=False):
+    '''checks the image and saves it to drive with possibly reduced channels.
+    Also can remove the image from the asset if the image is pure black
+    - it finds it's usages and replaces the inputs where the image is used
+    with zero/black color.
+    currently implemented file type conversions:
+    PNG->JPG
+    '''
+    colorspace = teximage.colorspace_settings.name
+    teximage.colorspace_settings.name = 'Non-Color'
+    #teximage.colorspace_settings.name = 'sRGB' color correction mambo jambo.
+
+    JPEG_QUALITY = 90
+    # is_image_black(na)
+    # is_image_bw(na)
+
+    rs = bpy.context.scene.render
+    ims = rs.image_settings
+
+    orig_file_format = ims.file_format
+    orig_quality = ims.quality
+    orig_color_mode = ims.color_mode
+    orig_compression = ims.compression
+    orig_depth = ims.color_depth
+
+    # if is_image_black(na):
+    #     # just erase the image from the asset here, no need to store black images.
+    #     pass;
+
+    # fp = teximage.filepath
+
+    # setup  image depth, 8 or 16 bit.
+    # this should normally divide depth with number of channels, but blender always states that number of channels is 4, even if there are only 3
+
+    print(teximage.name)
+    print(teximage.depth)
+    print(teximage.channels)
+
+    bpy.context.scene.display_settings.display_device = 'None'
+
+    image_depth = find_image_depth(teximage)
+
+    ims.color_mode = find_color_mode(teximage)
+    #image_depth = str(max(min(int(teximage.depth / 3), 16), 8))
+    print('resulting depth set to:', image_depth)
+
+    fp = input_filepath
+    if do_reductions:
+        na = imagetonumpy_flat(teximage)
+
+        if can_erase_alpha(na):
+            print(teximage.file_format)
+            if teximage.file_format == 'PNG':
+                print('changing type of image to JPG')
+                base, ext = os.path.splitext(fp)
+                teximage['original_extension'] = ext
+
+                fp = fp.replace('.png', '.jpg')
+                fp = fp.replace('.PNG', '.jpg')
+
+                teximage.name = teximage.name.replace('.png', '.jpg')
+                teximage.name = teximage.name.replace('.PNG', '.jpg')
+
+                teximage.file_format = 'JPEG'
+                ims.quality = JPEG_QUALITY
+                ims.color_mode = 'RGB'
+
+            if is_image_bw(na):
+                ims.color_mode = 'BW'
+
+    ims.file_format = teximage.file_format
+    ims.color_depth = image_depth
+
+    # all pngs with max compression
+    if ims.file_format == 'PNG':
+        ims.compression = 100
+    # all jpgs brought to reasonable quality
+    if ims.file_format == 'JPG':
+        ims.quality = JPEG_QUALITY
+
+    if do_downscale:
+        downscale(teximage)
+
+
+
+    # it's actually very important not to try to change the image filepath and packed file filepath before saving,
+    # blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes.
+    teximage.save_render(filepath=bpy.path.abspath(fp), scene=bpy.context.scene)
+    if len(teximage.packed_files) > 0:
+        teximage.unpack(method='REMOVE')
+    teximage.filepath = fp
+    teximage.filepath_raw = fp
+    teximage.reload()
+
+    teximage.colorspace_settings.name = colorspace
+
+    ims.file_format = orig_file_format
+    ims.quality = orig_quality
+    ims.color_mode = orig_color_mode
+    ims.compression = orig_compression
+    ims.color_depth = orig_depth
\ No newline at end of file
diff --git a/blenderkit/resolutions.py b/blenderkit/resolutions.py
index cc4fbb2228e06dec0eb2ac4b2af56a52579f47c2..f50ff9fe8b3b7a789e7011f23884511d1e7d3fa0 100644
--- a/blenderkit/resolutions.py
+++ b/blenderkit/resolutions.py
@@ -55,100 +55,9 @@ def get_current_resolution():
     return actres
 
 
-def can_erase_alpha(na):
-    alpha = na[3::4]
-    alpha_sum = alpha.sum()
-    if alpha_sum == alpha.size:
-        print('image can have alpha erased')
-    # print(alpha_sum, alpha.size)
-    return alpha_sum == alpha.size
-
-
-def is_image_black(na):
-    r = na[::4]
-    g = na[1::4]
-    b = na[2::4]
-
-    rgbsum = r.sum() + g.sum() + b.sum()
-
-    # print('rgb sum', rgbsum, r.sum(), g.sum(), b.sum())
-    if rgbsum == 0:
-        print('image can have alpha channel dropped')
-    return rgbsum == 0
-
-
-def is_image_bw(na):
-    r = na[::4]
-    g = na[1::4]
-    b = na[2::4]
-
-    rg_equal = r == g
-    gb_equal = g == b
-    rgbequal = rg_equal.all() and gb_equal.all()
-    if rgbequal:
-        print('image is black and white, can have channels reduced')
-
-    return rgbequal
-
-
-def numpytoimage(a, iname, width=0, height=0, channels=3):
-    t = time.time()
-    foundimage = False
-
-    for image in bpy.data.images:
-
-        if image.name[:len(iname)] == iname and image.size[0] == a.shape[0] and image.size[1] == a.shape[1]:
-            i = image
-            foundimage = True
-    if not foundimage:
-        if channels == 4:
-            bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0, 1), alpha=True,
-                              generated_type='BLANK', float=True)
-        if channels == 3:
-            bpy.ops.image.new(name=iname, width=width, height=height, color=(0, 0, 0), alpha=False,
-                              generated_type='BLANK', float=True)
-
-    for image in bpy.data.images:
-        # print(image.name[:len(iname)],iname, image.size[0],a.shape[0],image.size[1],a.shape[1])
-        if image.name[:len(iname)] == iname and image.size[0] == width and image.size[1] == height:
-            i = image
-
-    # dropping this re-shaping code -  just doing flat array for speed and simplicity
-    #    d = a.shape[0] * a.shape[1]
-    #    a = a.swapaxes(0, 1)
-    #    a = a.reshape(d)
-    #    a = a.repeat(channels)
-    #    a[3::4] = 1
-    i.pixels.foreach_set(a)  # this gives big speedup!
-    print('\ntime ' + str(time.time() - t))
-    return i
-
-
-def imagetonumpy(i):
-    t = time.time()
-
-    import numpy as np
-
-    width = i.size[0]
-    height = i.size[1]
-    # print(i.channels)
-
-    size = width * height * i.channels
-    na = np.empty(size, np.float32)
-    i.pixels.foreach_get(na)
-
-    # dropping this re-shaping code -  just doing flat array for speed and simplicity
-    #    na = na[::4]
-    #    na = na.reshape(height, width, i.channels)
-    #    na = na.swapaxnes(0, 1)
-
-    # print('\ntime of image to numpy ' + str(time.time() - t))
-    return na
-
-
 def save_image_safely(teximage, filepath):
     '''
-    Blender makes it really hard to save images... this is to fix it's crazy bad image saving.
+    Blender makes it really hard to save images...
     Would be worth investigating PIL or similar instead
     Parameters
     ----------
@@ -204,95 +113,8 @@ def extxchange_to_resolution(filepath):
         ext = 'jpg'
 
 
-def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=False, do_downscale=False):
-    '''checks the image and saves it to drive with possibly reduced channels.
-    Also can remove the image from the asset if the image is pure black
-    - it finds it's usages and replaces the inputs where the image is used
-    with zero/black color.
-    currently implemented file type conversions:
-    PNG->JPG
-    '''
-    colorspace = teximage.colorspace_settings.name
-    teximage.colorspace_settings.name = 'Non-Color'
 
-    JPEG_QUALITY = 90
-    # is_image_black(na)
-    # is_image_bw(na)
-
-    rs = bpy.context.scene.render
-    ims = rs.image_settings
-
-    orig_file_format = ims.file_format
-    orig_quality = ims.quality
-    orig_color_mode = ims.color_mode
-    orig_compression = ims.compression
 
-    # if is_image_black(na):
-    #     # just erase the image from the asset here, no need to store black images.
-    #     pass;
-
-    # fp = teximage.filepath
-    fp = input_filepath
-    if do_reductions:
-        na = imagetonumpy(teximage)
-
-        if can_erase_alpha(na):
-            print(teximage.file_format)
-            if teximage.file_format == 'PNG':
-                print('changing type of image to JPG')
-                base, ext = os.path.splitext(fp)
-                teximage['original_extension'] = ext
-
-                fp = fp.replace('.png', '.jpg')
-                fp = fp.replace('.PNG', '.jpg')
-
-                teximage.name = teximage.name.replace('.png', '.jpg')
-                teximage.name = teximage.name.replace('.PNG', '.jpg')
-
-                teximage.file_format = 'JPEG'
-                ims.quality = JPEG_QUALITY
-                ims.color_mode = 'RGB'
-
-            if is_image_bw(na):
-                ims.color_mode = 'BW'
-
-    ims.file_format = teximage.file_format
-
-    # all pngs with max compression
-    if ims.file_format == 'PNG':
-        ims.compression = 100
-    # all jpgs brought to reasonable quality
-    if ims.file_format == 'JPG':
-        ims.quality = JPEG_QUALITY
-
-    if do_downscale:
-        downscale(teximage)
-
-    # it's actually very important not to try to change the image filepath and packed file filepath before saving,
-    # blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes.
-    teximage.save_render(filepath=bpy.path.abspath(fp), scene=bpy.context.scene)
-    if len(teximage.packed_files) > 0:
-        teximage.unpack(method='REMOVE')
-    teximage.filepath = fp
-    teximage.filepath_raw = fp
-    teximage.reload()
-
-    teximage.colorspace_settings.name = colorspace
-
-    ims.file_format = orig_file_format
-    ims.quality = orig_quality
-    ims.color_mode = orig_color_mode
-    ims.compression = orig_compression
-
-
-def downscale(i):
-    minsize = 128
-
-    sx, sy = i.size[:]
-    sx = round(sx / 2)
-    sy = round(sy / 2)
-    if sx > minsize and sy > minsize:
-        i.scale(sx, sy)
 
 
 def upload_resolutions(files, asset_data):
@@ -341,9 +163,10 @@ def unpack_asset(data):
                 pf.filepath = fp  # bpy.path.abspath(fp)
             image.filepath = fp  # bpy.path.abspath(fp)
             image.filepath_raw = fp  # bpy.path.abspath(fp)
-            image.save()
+            # image.save()
             if len(image.packed_files) > 0:
-                image.unpack(method='REMOVE')
+                # image.unpack(method='REMOVE')
+                image.unpack(method='WRITE_ORIGINAL')
 
     bpy.ops.wm.save_mainfile(compress=False)
     # now try to delete the .blend1 file
@@ -524,11 +347,11 @@ def generate_lower_resolutions(data):
                             # first, let's link the image back to the original one.
                             i['blenderkit_original_path'] = i.filepath
                             # first round also makes reductions on the image, while keeping resolution
-                            make_possible_reductions_on_image(i, fp, do_reductions=True, do_downscale=False)
+                            image_utils.make_possible_reductions_on_image(i, fp, do_reductions=True, do_downscale=False)
 
                         else:
                             # lower resolutions only downscale
-                            make_possible_reductions_on_image(i, fp, do_reductions=False, do_downscale=True)
+                            image_utils.make_possible_reductions_on_image(i, fp, do_reductions=False, do_downscale=True)
 
                         abspath = bpy.path.abspath(i.filepath)
                         if os.path.exists(abspath):
@@ -556,7 +379,7 @@ def generate_lower_resolutions(data):
                 else:
                     p2res = rkeys[rkeys.index(p2res) - 1]
             print('uploading resolution files')
-            upload_resolutions(files, data['asset_data'])
+            #upload_resolutions(files, data['asset_data'])
             preferences = bpy.context.preferences.addons['blenderkit'].preferences
             patch_asset_empty(data['asset_data']['id'], preferences.api_key)
         return
@@ -666,41 +489,6 @@ def get_materials_for_validation(page_size=100, max_results=100000000):
     return filepath
 
 
-# This gets all assets in the database through the/assets endpoint. Currently not used, since we use elastic for everything.
-# def get_assets_list():
-#     bpy.app.debug_value = 2
-#
-#     results = []
-#     preferences = bpy.context.preferences.addons['blenderkit'].preferences
-#     url = paths.get_api_url() + 'assets/all'
-#     i = 0
-#     while url is not None:
-#         headers = utils.get_headers(preferences.api_key)
-#         print('fetching assets from assets endpoint')
-#         print(url)
-#         retries = 0
-#         while retries < 3:
-#             r = rerequests.get(url, headers=headers)
-#
-#             try:
-#                 adata = r.json()
-#                 url = adata.get('next')
-#                 print(i)
-#                 i += 1
-#             except Exception as e:
-#                 print(e)
-#                 print('failed to get next')
-#                 if retries == 2:
-#                     url = None
-#             if adata.get('results') != None:
-#                 results.extend(adata['results'])
-#                 retries = 3
-#             print(f'fetched page {i}')
-#             retries += 1
-#
-#     fpath = assets_db_path()
-#     with open(fpath, 'w', encoding = 'utf-8') as s:
-#         json.dump(results, s, ensure_ascii=False, indent=4)
 
 
 def load_assets_list(filepath):
@@ -758,6 +546,7 @@ def generate_resolution_thread(asset_data, api_key):
     '''
 
     fpath = download_asset(asset_data, unpack=True, api_key=api_key)
+
     if fpath:
         if asset_data['assetType'] != 'hdr':
             print('send to bg ', fpath)