static bool create_dds_tex(const crn_comp_params &params, dds_texture &dds_tex)
{
    image_u8 images[cCRNMaxFaces][cCRNMaxLevels];

    bool has_alpha = false;
    for (uint face_index = 0; face_index < params.m_faces; face_index++)
    {
        for (uint level_index = 0; level_index < params.m_levels; level_index++)
        {
            const uint width = math::maximum(1U, params.m_width >> level_index);
            const uint height = math::maximum(1U, params.m_height >> level_index);

            if (!params.m_pImages[face_index][level_index])
                return false;

            images[face_index][level_index].alias((color_quad_u8*)params.m_pImages[face_index][level_index], width, height);
            if (!has_alpha)
                has_alpha = image_utils::has_alpha(images[face_index][level_index]);
        }
    }

    for (uint face_index = 0; face_index < params.m_faces; face_index++)
        for (uint level_index = 0; level_index < params.m_levels; level_index++)
            images[face_index][level_index].set_component_valid(3, has_alpha);

    face_vec faces(params.m_faces);

    for (uint face_index = 0; face_index < params.m_faces; face_index++)
    {
        for (uint level_index = 0; level_index < params.m_levels; level_index++)
        {
            mip_level *pMip = crnlib_new<mip_level>();

            image_u8 *pImage = crnlib_new<image_u8>();
            pImage->swap(images[face_index][level_index]);
            pMip->assign(pImage);

            faces[face_index].push_back(pMip);
        }
    }

    dds_tex.assign(faces);

#ifdef CRNLIB_BUILD_DEBUG
    CRNLIB_ASSERT(dds_tex.check());
#endif

    return true;
}
      static bool write_compressed_texture(
         dds_texture& work_tex, convert_params& params, crn_comp_params &comp_params, pixel_format dst_format, progress_params& progress_state, bool perceptual, convert_stats &stats)
      {
         comp_params.m_file_type = (params.m_dst_file_type == texture_file_types::cFormatCRN) ? cCRNFileTypeCRN : cCRNFileTypeDDS;

         comp_params.m_pProgress_func = crn_progress_callback;
         comp_params.m_pProgress_func_data = &progress_state;
         comp_params.set_flag(cCRNCompFlagPerceptual, perceptual);

         crn_format crn_fmt = pixel_format_helpers::convert_pixel_format_to_best_crn_format(dst_format);
         comp_params.m_format = crn_fmt;

         console::message(L"Writing %s texture to file: \"%s\"", crn_get_format_string(crn_fmt), params.m_dst_filename.get_ptr());

         uint32 actual_quality_level;
         float actual_bitrate;
         bool status = work_tex.write_to_file(params.m_dst_filename.get_ptr(), params.m_dst_file_type, &comp_params, &actual_quality_level, &actual_bitrate);
         if (!status)
            return convert_error(params, L"Failed writing output file!");

         if (!params.m_no_stats)
         {
            if (!stats.init(params.m_pInput_texture->get_source_filename().get_ptr(), params.m_dst_filename.get_ptr(), *params.m_pIntermediate_texture, params.m_dst_file_type, params.m_lzma_stats))
            {
               console::warning(L"Unable to compute output statistics for file: %s", params.m_pInput_texture->get_source_filename().get_ptr());
            }
         }

         return true;
      }
bool create_texture_mipmaps(dds_texture &work_tex, const crn_comp_params &params, const crn_mipmap_params &mipmap_params, bool generate_mipmaps)
{
    crn_comp_params new_params(params);

    bool generate_new_mips = false;

    switch (mipmap_params.m_mode)
    {
    case cCRNMipModeUseSourceOrGenerateMips:
    {
        if (work_tex.get_num_levels() == 1)
            generate_new_mips = true;
        break;
    }
    case cCRNMipModeUseSourceMips:
    {
        break;
    }
    case cCRNMipModeGenerateMips:
    {
        generate_new_mips = true;
        break;
    }
    case cCRNMipModeNoMips:
    {
        work_tex.discard_mipmaps();
        break;
    }
    default:
    {
        CRNLIB_ASSERT(0);
        break;
    }
    }

    rect window_rect(mipmap_params.m_window_left, mipmap_params.m_window_top, mipmap_params.m_window_right, mipmap_params.m_window_bottom);

    if (!window_rect.is_empty())
    {
        if (work_tex.get_num_faces() > 1)
        {
            console::warning(L"Can't crop cubemap textures");
        }
        else
        {
            console::info(L"Cropping input texture from window (%ux%u)-(%ux%u)", window_rect.get_left(), window_rect.get_top(), window_rect.get_right(), window_rect.get_bottom());

            if (!work_tex.crop(window_rect.get_left(), window_rect.get_top(), window_rect.get_width(), window_rect.get_height()))
                console::warning(L"Failed cropping window rect");
        }
    }

    int new_width = work_tex.get_width();
    int new_height = work_tex.get_height();

    if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height))
    {
        if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height))
        {
            if (!mipmap_params.m_clamp_scale)
            {
                if (work_tex.get_num_faces() > 1)
                {
                    console::warning(L"Can't crop cubemap textures");
                }
                else
                {
                    new_width = math::minimum<uint>(mipmap_params.m_clamp_width, new_width);
                    new_height = math::minimum<uint>(mipmap_params.m_clamp_height, new_height);
                    console::info(L"Clamping input texture to %ux%u", new_width, new_height);
                    work_tex.crop(0, 0, new_width, new_height);
                }
            }
        }
    }

    if (mipmap_params.m_scale_mode != cCRNSMDisabled)
    {
        bool is_pow2 = math::is_power_of_2((uint32)new_width) && math::is_power_of_2((uint32)new_height);

        switch (mipmap_params.m_scale_mode)
        {
        case cCRNSMAbsolute:
        {
            new_width = (uint)mipmap_params.m_scale_x;
            new_height = (uint)mipmap_params.m_scale_y;
            break;
        }
        case cCRNSMRelative:
        {
            new_width = (uint)(mipmap_params.m_scale_x * new_width + .5f);
            new_height = (uint)(mipmap_params.m_scale_y * new_height + .5f);
            break;
        }
        case cCRNSMLowerPow2:
        {
            if (!is_pow2)
                math::compute_lower_pow2_dim(new_width, new_height);
            break;
        }
        case cCRNSMNearestPow2:
        {
            if (!is_pow2)
            {
                int lwidth = new_width;
                int lheight = new_height;
                math::compute_lower_pow2_dim(lwidth, lheight);

                int uwidth = new_width;
                int uheight = new_height;
                math::compute_upper_pow2_dim(uwidth, uheight);

                if (labs(new_width - lwidth) < labs(new_width - uwidth))
                    new_width = lwidth;
                else
                    new_width = uwidth;

                if (labs(new_height - lheight) < labs(new_height - uheight))
                    new_height = lheight;
                else
                    new_height = uheight;
            }
            break;
        }
        case cCRNSMNextPow2:
        {
            if (!is_pow2)
                math::compute_upper_pow2_dim(new_width, new_height);
            break;
        }
        default:
            break;
        }
    }

    if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height))
    {
        if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height))
        {
            if (mipmap_params.m_clamp_scale)
            {
                new_width = math::minimum<uint>(mipmap_params.m_clamp_width, new_width);
                new_height = math::minimum<uint>(mipmap_params.m_clamp_height, new_height);
            }
        }
    }

    new_width = math::clamp<int>(new_width, 1, cCRNMaxLevelResolution);
    new_height = math::clamp<int>(new_height, 1, cCRNMaxLevelResolution);

    if ((new_width != (int)work_tex.get_width()) || (new_height != (int)work_tex.get_height()))
    {
        console::info(L"Resampling input texture to %ux%u", new_width, new_height);

        const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter);

        bool srgb = mipmap_params.m_gamma_filtering != 0;

        dds_texture::resample_params res_params;
        res_params.m_pFilter = pFilter;
        res_params.m_wrapping = mipmap_params.m_tiled != 0;
        if (work_tex.get_num_faces())
            res_params.m_wrapping = false;
        res_params.m_renormalize = mipmap_params.m_renormalize != 0;
        res_params.m_filter_scale = 1.0f;
        res_params.m_gamma = mipmap_params.m_gamma;
        res_params.m_srgb = srgb;
        res_params.m_multithreaded = (params.m_num_helper_threads > 0);

        if (!work_tex.resize(new_width, new_height, res_params))
        {
            console::error(L"Failed resizing texture!");
            return false;
        }
    }

    if ((generate_new_mips) && (generate_mipmaps))
    {
        bool srgb = mipmap_params.m_gamma_filtering != 0;

        const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter);

        dds_texture::generate_mipmap_params gen_params;
        gen_params.m_pFilter = pFilter;
        gen_params.m_wrapping = mipmap_params.m_tiled != 0;
        gen_params.m_renormalize = mipmap_params.m_renormalize != 0;
        gen_params.m_filter_scale = mipmap_params.m_blurriness;
        gen_params.m_gamma = mipmap_params.m_gamma;
        gen_params.m_srgb = srgb;
        gen_params.m_multithreaded = params.m_num_helper_threads > 0;
        gen_params.m_max_mips = mipmap_params.m_max_levels;
        gen_params.m_min_mip_size = mipmap_params.m_min_mip_size;

        console::info(L"Generating mipmaps using filter \"%S\"", pFilter);

        timer tm;
        tm.start();
        if (!work_tex.generate_mipmaps(gen_params, true))
        {
            console::error(L"Failed generating mipmaps!");
            return false;
        }
        double t = tm.get_elapsed_secs();

        console::info(L"Generated %u mipmap levels in %3.3fs", work_tex.get_num_levels() - 1, t);
    }

    return true;
}
      static bool convert_and_write_normal_texture(dds_texture& work_tex, convert_params& params, const crn_comp_params &comp_params, pixel_format dst_format, progress_params& progress_state, bool formats_differ, bool perceptual, convert_stats& stats)
      {
         if (formats_differ)
         {
            dxt_image::pack_params pack_params;

            pack_params.m_perceptual = perceptual;
            pack_params.m_compressor = comp_params.m_dxt_compressor_type;
            pack_params.m_pProgress_callback = dxt_progress_callback_func;
            pack_params.m_pProgress_callback_user_data_ptr = &progress_state;
            pack_params.m_dxt1a_alpha_threshold = comp_params.m_dxt1a_alpha_threshold;
            pack_params.m_quality = comp_params.m_dxt_quality;
            pack_params.m_endpoint_caching = !comp_params.get_flag(cCRNCompFlagDisableEndpointCaching);
            pack_params.m_grayscale_sampling = comp_params.get_flag(cCRNCompFlagGrayscaleSampling);
            if ((!comp_params.get_flag(cCRNCompFlagUseBothBlockTypes)) && (!comp_params.get_flag(cCRNCompFlagDXT1AForTransparency)))
               pack_params.m_use_both_block_types = false;

            pack_params.m_num_helper_threads = comp_params.m_num_helper_threads;
            pack_params.m_use_transparent_indices_for_black = comp_params.get_flag(cCRNCompFlagUseTransparentIndicesForBlack);

            console::info(L"Converting texture format from %s to %s", pixel_format_helpers::get_pixel_format_string(work_tex.get_format()), pixel_format_helpers::get_pixel_format_string(dst_format));

            timer tm;
            tm.start();

            bool status = work_tex.convert(dst_format, pack_params);

            double t = tm.get_elapsed_secs();

            console::info(L"");

            if (!status)
            {
               if (progress_state.m_canceled)
               {
                  params.m_canceled = true;
                  return false;
               }
               else
               {
                  return convert_error(params, L"Failed converting texture to output format!");
               }
            }

            console::info(L"Texture format conversion took %3.3fs", t);
         }

         if (params.m_write_mipmaps_to_multiple_files)
         {
            for (uint f = 0; f < work_tex.get_num_faces(); f++)
            {
               for (uint l = 0; l < work_tex.get_num_levels(); l++)
               {
                  dynamic_wstring filename(params.m_dst_filename.get_ptr());

                  dynamic_wstring drv, dir, fn, ext;
                  if (!split_path(params.m_dst_filename.get_ptr(), &drv, &dir, &fn, &ext))
                     return false;

                  fn += dynamic_wstring(cVarArg, L"_face%u_mip%u", f, l).get_ptr();
                  filename = drv + dir + fn + ext;

                  mip_level *pLevel = work_tex.get_level(f, l);

                  face_vec face(1);
                  face[0].push_back(crnlib_new<mip_level>(*pLevel));

                  dds_texture new_tex;
                  new_tex.assign(face);

                  console::info(L"Writing texture face %u mip level %u to file %s", f, l, filename.get_ptr());

                  if (!new_tex.write_to_file(filename.get_ptr(), params.m_dst_file_type, NULL, NULL, NULL))
                     return convert_error(params, L"Failed writing output file!");
               }
            }
         }
         else
         {
            console::message(L"Writing texture to file: \"%s\"", params.m_dst_filename.get_ptr());

            if (!work_tex.write_to_file(params.m_dst_filename.get_ptr(), params.m_dst_file_type, NULL, NULL, NULL))
               return convert_error(params, L"Failed writing output file!");

            if (!params.m_no_stats)
            {
               if (!stats.init(params.m_pInput_texture->get_source_filename().get_ptr(), params.m_dst_filename.get_ptr(), *params.m_pIntermediate_texture, params.m_dst_file_type, params.m_lzma_stats))
               {
                  console::warning(L"Unable to compute output statistics for file: %s", params.m_pInput_texture->get_source_filename().get_ptr());
               }
            }
         }

         return true;
      }
      static pixel_format choose_pixel_format(convert_params& params, const crn_comp_params &comp_params, const dds_texture& src_tex, texture_type tex_type)
      {
         if (params.m_use_source_format)
            return src_tex.get_format();

         const bool is_normal_map = (tex_type == cTextureTypeNormalMap);

         if (params.m_dst_file_type == texture_file_types::cFormatCRN)
         {
            if (is_normal_map)
            {
               switch (src_tex.get_format())
               {
                  case PIXEL_FMT_DXN:
                  case PIXEL_FMT_3DC:
                  case PIXEL_FMT_DXT5_xGBR:
                  case PIXEL_FMT_DXT5_AGBR:
                  case PIXEL_FMT_DXT5_xGxR:
                     return src_tex.get_format();
                  default:
                     return PIXEL_FMT_DXT5_AGBR;
               }
            }
         }
         else if (params.m_dst_file_type == texture_file_types::cFormatDDS)
         {
            if (src_tex.get_source_file_type() != texture_file_types::cFormatCRN)
            {
               if (is_normal_map)
               {
                  switch (src_tex.get_format())
                  {
                     case PIXEL_FMT_DXN:
                     case PIXEL_FMT_3DC:
                     case PIXEL_FMT_DXT5_xGBR:
                     case PIXEL_FMT_DXT5_AGBR:
                     case PIXEL_FMT_DXT5_xGxR:
                        return src_tex.get_format();
                     default:
                        return PIXEL_FMT_DXT5_AGBR;
                  }
               }
               else if (pixel_format_helpers::is_grayscale(src_tex.get_format()))
               {
                  if (pixel_format_helpers::has_alpha(src_tex.get_format()))
                     return comp_params.get_flag(cCRNCompFlagDXT1AForTransparency) ? PIXEL_FMT_DXT1A : PIXEL_FMT_DXT5;
                  else
                     return PIXEL_FMT_DXT1;
               }
               else if (pixel_format_helpers::has_alpha(src_tex.get_format()))
                  return comp_params.get_flag(cCRNCompFlagDXT1AForTransparency) ? PIXEL_FMT_DXT1A : PIXEL_FMT_DXT5;
               else
                  return PIXEL_FMT_DXT1;
            }
         }
         else
         {
            // A regular image format.
            if (pixel_format_helpers::is_grayscale(src_tex.get_format()))
            {
               if (pixel_format_helpers::has_alpha(src_tex.get_format()))
                  return PIXEL_FMT_A8L8;
               else
                  return PIXEL_FMT_L8;
            }
            else if (pixel_format_helpers::has_alpha(src_tex.get_format()))
               return PIXEL_FMT_A8R8G8B8;
            else
               return PIXEL_FMT_R8G8B8;
         }

         return src_tex.get_format();
      }