2022-09-17 03:23:32 +00:00
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
// ___ ___ __ _ _ ___ ___ ___ _ _ _
|
|
|
|
// / _ \| _ ) / /| | || _ \ __| |_ _|_ __ __ _ __ _ ___ | | (_) |__ _ _ __ _ _ _ _ _
|
|
|
|
// | (_) | _ \/ _ \_ _| _/ _| | || ' \/ _` / _` / -_) | |__| | '_ \ '_/ _` | '_| || |
|
|
|
|
// \__\_\___/\___/ |_||_| |___| |___|_|_|_\__,_\__, \___| |____|_|_.__/_| \__,_|_| \_, |
|
|
|
|
// |___/ |__/
|
|
|
|
//
|
2023-09-15 15:35:41 +00:00
|
|
|
// Powered by:
|
2023-09-17 06:22:54 +00:00
|
|
|
// stb_image & stb_image_write (https://github.com/nothings/stb)
|
2023-09-15 15:35:41 +00:00
|
|
|
// dr_pcx (https://github.com/mackron/dr_pcx)
|
|
|
|
// nanosvg (https://github.com/memononen/nanosvg)
|
|
|
|
// qoi (https://qoiformat.org)
|
|
|
|
// pixelscalers (https://github.com/janert/pixelscalers)
|
2023-09-17 06:22:54 +00:00
|
|
|
// mmpx (https://github.com/ITotalJustice/mmpx)
|
2022-09-17 03:23:32 +00:00
|
|
|
//
|
|
|
|
//-----------------------------------------------------------------------------------------------------
|
|
|
|
|
2023-09-17 06:22:54 +00:00
|
|
|
// Set this to 1 if we want to print debug messages to stderr
|
2023-09-17 08:43:17 +00:00
|
|
|
#define IMAGE_DEBUG 0
|
2023-09-17 06:22:54 +00:00
|
|
|
#include "image.h"
|
|
|
|
// We need 'qbs' and 'image' structs stuff from here
|
|
|
|
// This should eventually change when things are moved to smaller, logical and self-contained files
|
|
|
|
#include "../../../libqb.h"
|
2023-12-04 00:18:32 +00:00
|
|
|
#include "filepath.h"
|
|
|
|
#include <cctype>
|
2023-09-17 06:22:54 +00:00
|
|
|
#include <string>
|
2022-09-17 18:33:08 +00:00
|
|
|
#include <unordered_map>
|
2023-09-17 06:22:54 +00:00
|
|
|
#include <vector>
|
2022-09-17 03:23:32 +00:00
|
|
|
#define DR_PCX_IMPLEMENTATION
|
|
|
|
#include "dr_pcx.h"
|
|
|
|
#define STB_IMAGE_IMPLEMENTATION
|
2023-09-15 22:20:24 +00:00
|
|
|
#include "stb/stb_image.h"
|
2023-09-15 15:35:41 +00:00
|
|
|
#define QOI_IMPLEMENTATION
|
|
|
|
#include "qoi.h"
|
2023-09-17 06:22:54 +00:00
|
|
|
#define NANOSVG_ALL_COLOR_KEYWORDS
|
2023-09-15 10:45:16 +00:00
|
|
|
#define NANOSVG_IMPLEMENTATION
|
|
|
|
#include "nanosvg/nanosvg.h"
|
|
|
|
#define NANOSVGRAST_IMPLEMENTATION
|
|
|
|
#include "nanosvg/nanosvgrast.h"
|
2023-09-15 22:20:24 +00:00
|
|
|
#define SXBR_IMPLEMENTATION
|
|
|
|
#include "pixelscalers/sxbr.hpp"
|
|
|
|
#define MMPX_IMPLEMENTATION
|
|
|
|
#include "pixelscalers/mmpx.hpp"
|
2023-09-15 08:40:27 +00:00
|
|
|
#define HQX_IMPLEMENTATION
|
|
|
|
#include "pixelscalers/hqx.hpp"
|
2023-09-15 22:20:24 +00:00
|
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
|
|
#include "stb/stb_image_write.h"
|
2022-09-17 03:23:32 +00:00
|
|
|
|
|
|
|
// This is returned to the caller if something goes wrong while loading the image
|
|
|
|
#define INVALID_IMAGE_HANDLE -1
|
|
|
|
|
|
|
|
#ifdef QB64_WINDOWS
|
|
|
|
# define ZERO_VARIABLE(_v_) ZeroMemory(&(_v_), sizeof(_v_))
|
|
|
|
#else
|
|
|
|
# define ZERO_VARIABLE(_v_) memset(&(_v_), 0, sizeof(_v_))
|
|
|
|
#endif
|
|
|
|
|
2023-09-17 06:22:54 +00:00
|
|
|
#define GET_ARRAY_SIZE(_x_) (sizeof(_x_) / sizeof(_x_[0]))
|
|
|
|
|
|
|
|
extern const img_struct *img; // used by func__loadimage
|
|
|
|
extern const img_struct *write_page; // used by func__loadimage
|
|
|
|
extern const uint32_t palette_256[]; // used by func__loadimage
|
|
|
|
extern const int32_t *page; // used by func__saveimage
|
|
|
|
extern const int32_t nextimg; // used by func__saveimage
|
|
|
|
extern const uint8_t charset8x8[256][8][8]; // used by func__saveimage
|
|
|
|
extern const uint8_t charset8x16[256][16][8]; // used by func__saveimage
|
2022-09-17 03:23:32 +00:00
|
|
|
|
2023-09-15 08:40:27 +00:00
|
|
|
/// @brief Pixel scaler algorithms
|
2023-09-18 20:37:57 +00:00
|
|
|
enum class ImageScaler { NONE = 0, SXBR2, MMPX2, HQ2XA, HQ2XB, HQ3XA, HQ3XB };
|
2023-09-17 06:22:54 +00:00
|
|
|
/// @brief This is the scaling factors for ImageScaler enum
|
2023-09-18 20:37:57 +00:00
|
|
|
static const int g_ImageScaleFactor[] = {1, 2, 2, 2, 2, 3, 3};
|
2023-09-17 11:50:38 +00:00
|
|
|
/// @brief Pixel scaler names for ImageScaler enum
|
2023-09-18 20:37:57 +00:00
|
|
|
static const char *g_ImageScalerName[] = {"NONE", "SXBR2", "MMPX2", "HQ2XA", "HQ2XB", "HQ3XA", "HQ3XB"};
|
2023-09-15 08:40:27 +00:00
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief Runs a pixel scaler algorithm on raw image pixels. It will free 'data' if scaling occurs!
|
2023-09-15 08:40:27 +00:00
|
|
|
/// @param data In + Out: The source raw image data in RGBA format
|
|
|
|
/// @param xOut In + Out: The image width
|
|
|
|
/// @param yOut In + Out: The image height
|
|
|
|
/// @param scaler The scaler algorithm to use
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @return A pointer to the scaled image or 'data' if there is no change
|
2023-09-15 08:40:27 +00:00
|
|
|
static uint32_t *image_scale(uint32_t *data, int32_t *xOut, int32_t *yOut, ImageScaler scaler) {
|
|
|
|
if (scaler > ImageScaler::NONE) {
|
2023-09-15 10:45:16 +00:00
|
|
|
auto newX = *xOut * g_ImageScaleFactor[(int)(scaler)];
|
|
|
|
auto newY = *yOut * g_ImageScaleFactor[(int)(scaler)];
|
2023-09-15 08:40:27 +00:00
|
|
|
|
|
|
|
auto pixels = (uint32_t *)malloc(sizeof(uint32_t) * newX * newY);
|
|
|
|
if (pixels) {
|
|
|
|
IMAGE_DEBUG_PRINT("Scaler %i: (%i x %i) -> (%i x %i)", scaler, *xOut, *yOut, newX, newY);
|
|
|
|
|
|
|
|
switch (scaler) {
|
|
|
|
case ImageScaler::SXBR2:
|
|
|
|
scaleSuperXBR2(data, *xOut, *yOut, pixels);
|
|
|
|
break;
|
|
|
|
|
2023-09-15 22:20:24 +00:00
|
|
|
case ImageScaler::MMPX2:
|
|
|
|
mmpx_scale2x(data, pixels, *xOut, *yOut);
|
|
|
|
break;
|
|
|
|
|
2023-09-15 08:40:27 +00:00
|
|
|
case ImageScaler::HQ2XA:
|
|
|
|
hq2xA(data, *xOut, *yOut, pixels);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ImageScaler::HQ2XB:
|
|
|
|
hq2xB(data, *xOut, *yOut, pixels);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ImageScaler::HQ3XA:
|
|
|
|
hq3xA(data, *xOut, *yOut, pixels);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ImageScaler::HQ3XB:
|
|
|
|
hq3xB(data, *xOut, *yOut, pixels);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
IMAGE_DEBUG_PRINT("Unsupported scaler %i", scaler);
|
2023-09-15 22:20:24 +00:00
|
|
|
free(pixels);
|
|
|
|
return data;
|
2023-09-15 08:40:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
free(data);
|
|
|
|
data = pixels;
|
|
|
|
*xOut = newX;
|
|
|
|
*yOut = newY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief This is internally used by image_svg_load_from_file() and image_svg_load_fron_memory(). It always frees 'image' once done!
|
|
|
|
/// @param image nanosvg image object pointer
|
|
|
|
/// @param xOut Out: width in pixels. This cannot be NULL
|
|
|
|
/// @param yOut Out: height in pixels. This cannot be NULL
|
|
|
|
/// @param scaler An optional pixel scaler to use (it just used this to scale internally)
|
|
|
|
/// @param components Out: color channels. This cannot be NULL
|
|
|
|
/// @param isVG Out: vector graphics? Always set to true
|
|
|
|
/// @return A pointer to the raw pixel data in RGBA format or NULL on failure
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint32_t *image_svg_load(NSVGimage *image, int32_t *xOut, int32_t *yOut, ImageScaler scaler, int *components, bool *isVG) {
|
2023-09-15 10:45:16 +00:00
|
|
|
auto rast = nsvgCreateRasterizer();
|
|
|
|
if (!rast) {
|
|
|
|
nsvgDelete(image);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto w = (int32_t)image->width * g_ImageScaleFactor[(int)(scaler)];
|
|
|
|
auto h = (int32_t)image->height * g_ImageScaleFactor[(int)(scaler)];
|
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
auto pixels = (uint32_t *)malloc(sizeof(uint32_t) * w * h);
|
2023-09-15 10:45:16 +00:00
|
|
|
if (!pixels) {
|
|
|
|
nsvgDeleteRasterizer(rast);
|
|
|
|
nsvgDelete(image);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
nsvgRasterize(rast, image, 0, 0, g_ImageScaleFactor[(int)(scaler)], reinterpret_cast<unsigned char *>(pixels), w, h, sizeof(uint32_t) * w);
|
2023-09-15 10:45:16 +00:00
|
|
|
nsvgDeleteRasterizer(rast);
|
|
|
|
nsvgDelete(image);
|
|
|
|
|
|
|
|
*xOut = w;
|
|
|
|
*yOut = h;
|
|
|
|
*components = sizeof(uint32_t);
|
|
|
|
*isVG = true;
|
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief Loads an SVG image file from disk
|
|
|
|
/// @param fileName The file path name to load
|
|
|
|
/// @param xOut Out: width in pixels. This cannot be NULL
|
|
|
|
/// @param yOut Out: height in pixels. This cannot be NULL
|
|
|
|
/// @param scaler An optional pixel scaler to use (it just used this to scale internally)
|
|
|
|
/// @param components Out: color channels. This cannot be NULL
|
|
|
|
/// @param isVG Out: vector graphics? Always set to true
|
|
|
|
/// @return A pointer to the raw pixel data in RGBA format or NULL on failure
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint32_t *image_svg_load_from_file(const char *fileName, int32_t *xOut, int32_t *yOut, ImageScaler scaler, int *components, bool *isVG) {
|
2023-12-04 00:18:32 +00:00
|
|
|
if (!filepath_has_extension(fileName, "svg"))
|
|
|
|
return nullptr;
|
|
|
|
|
2023-12-04 01:16:04 +00:00
|
|
|
auto fp = fopen(fileName, "rb");
|
|
|
|
if (!fp)
|
2023-09-15 10:45:16 +00:00
|
|
|
return nullptr;
|
|
|
|
|
2023-12-04 01:16:04 +00:00
|
|
|
fseek(fp, 0, SEEK_END);
|
|
|
|
auto size = ftell(fp);
|
|
|
|
fseek(fp, 0, SEEK_SET);
|
|
|
|
|
|
|
|
auto svgString = (char *)malloc(size + 1);
|
|
|
|
if (!svgString) {
|
|
|
|
fclose(fp);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fread(svgString, 1, size, fp) != size) {
|
|
|
|
free(svgString);
|
|
|
|
fclose(fp);
|
|
|
|
return nullptr;
|
|
|
|
}
|
2023-12-05 03:50:25 +00:00
|
|
|
svgString[size] = '\0'; // must be null terminated
|
2023-12-04 01:16:04 +00:00
|
|
|
|
|
|
|
fclose(fp);
|
|
|
|
|
2023-12-05 03:50:25 +00:00
|
|
|
// Check if it has a valid SVG start tag
|
|
|
|
if (!strstr(svgString, "<svg")) {
|
|
|
|
free(svgString);
|
|
|
|
return nullptr;
|
2023-12-04 01:16:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
auto image = nsvgParse(svgString, "px", 96.0f); // important note: changes the string
|
|
|
|
if (!image) {
|
|
|
|
free(svgString);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2023-12-04 01:34:59 +00:00
|
|
|
auto pixels = image_svg_load(image, xOut, yOut, scaler, components, isVG); // this is where everything else is freed
|
2023-12-04 01:16:04 +00:00
|
|
|
free(svgString);
|
|
|
|
|
|
|
|
return pixels;
|
2023-09-15 10:45:16 +00:00
|
|
|
}
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief Loads an SVG image file from memory
|
|
|
|
/// @param buffer The raw pointer to the file in memory
|
|
|
|
/// @param size The size of the file in memory
|
|
|
|
/// @param xOut Out: width in pixels. This cannot be NULL
|
|
|
|
/// @param yOut Out: height in pixels. This cannot be NULL
|
|
|
|
/// @param scaler An optional pixel scaler to use (it just used this to scale internally)
|
|
|
|
/// @param components Out: color channels. This cannot be NULL
|
|
|
|
/// @param isVG Out: vector graphics? Always set to true
|
|
|
|
/// @return A pointer to the raw pixel data in RGBA format or NULL on failure
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint32_t *image_svg_load_from_memory(const uint8_t *buffer, size_t size, int32_t *xOut, int32_t *yOut, ImageScaler scaler, int *components, bool *isVG) {
|
2023-09-15 10:45:16 +00:00
|
|
|
auto svgString = (char *)malloc(size + 1);
|
|
|
|
if (!svgString)
|
|
|
|
return nullptr;
|
|
|
|
|
2023-12-05 03:50:25 +00:00
|
|
|
memcpy(svgString, buffer, size);
|
2023-12-04 01:16:04 +00:00
|
|
|
svgString[size] = '\0'; // must be null terminated
|
2023-09-15 10:45:16 +00:00
|
|
|
|
2023-12-05 03:50:25 +00:00
|
|
|
// Check if it has a valid SVG start tag
|
|
|
|
if (!strstr(svgString, "<svg")) {
|
|
|
|
free(svgString);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
auto image = nsvgParse(svgString, "px", 96.0f); // important note: changes the string
|
2023-09-15 10:45:16 +00:00
|
|
|
if (!image) {
|
|
|
|
free(svgString);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2023-12-04 01:34:59 +00:00
|
|
|
auto pixels = image_svg_load(image, xOut, yOut, scaler, components, isVG); // this is where everything else is freed
|
2023-09-15 10:45:16 +00:00
|
|
|
free(svgString);
|
|
|
|
|
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief Loads a QOI image file from disk
|
|
|
|
/// @param fileName The file path name to load
|
|
|
|
/// @param xOut Out: width in pixels. This cannot be NULL
|
|
|
|
/// @param yOut Out: height in pixels. This cannot be NULL
|
|
|
|
/// @param components Out: color channels. This cannot be NULL
|
|
|
|
/// @return A pointer to the raw pixel data in RGBA format or NULL on failure
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint32_t *image_qoi_load_from_file(const char *fileName, int32_t *xOut, int32_t *yOut, int *components) {
|
2023-09-15 15:35:41 +00:00
|
|
|
qoi_desc desc;
|
2023-09-17 11:50:38 +00:00
|
|
|
auto pixels = reinterpret_cast<uint32_t *>(qoi_read(fileName, &desc, sizeof(uint32_t)));
|
2023-09-15 15:35:41 +00:00
|
|
|
if (pixels) {
|
|
|
|
*xOut = desc.width;
|
|
|
|
*yOut = desc.height;
|
|
|
|
*components = desc.channels;
|
|
|
|
}
|
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @brief Loads a QOI image file from memory
|
|
|
|
/// @param buffer The raw pointer to the file in memory
|
|
|
|
/// @param size The size of the file in memory
|
|
|
|
/// @param xOut Out: width in pixels. This cannot be NULL
|
|
|
|
/// @param yOut Out: height in pixels. This cannot be NULL
|
|
|
|
/// @param components Out: color channels. This cannot be NULL
|
|
|
|
/// @return A pointer to the raw pixel data in RGBA format or NULL on failure
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint32_t *image_qoi_load_from_memory(const uint8_t *buffer, size_t size, int32_t *xOut, int32_t *yOut, int *components) {
|
2023-09-15 15:35:41 +00:00
|
|
|
qoi_desc desc;
|
2023-09-17 11:50:38 +00:00
|
|
|
auto pixels = reinterpret_cast<uint32_t *>(qoi_decode(buffer, size, &desc, sizeof(uint32_t)));
|
2023-09-15 15:35:41 +00:00
|
|
|
if (pixels) {
|
|
|
|
*xOut = desc.width;
|
|
|
|
*yOut = desc.height;
|
|
|
|
*components = desc.channels;
|
|
|
|
}
|
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
|
2023-12-29 09:57:27 +00:00
|
|
|
/// @brief Decodes an image file from a file using the dr_pcx & stb_image libraries.
|
2023-01-09 07:31:56 +00:00
|
|
|
/// @param fileName A valid filename
|
|
|
|
/// @param xOut Out: width in pixels. This cannot be NULL
|
|
|
|
/// @param yOut Out: height in pixels. This cannot be NULL
|
2023-09-15 08:40:27 +00:00
|
|
|
/// @param scaler An optional pixel scaler to use
|
2023-01-09 07:31:56 +00:00
|
|
|
/// @return A pointer to the raw pixel data in RGBA format or NULL on failure
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint32_t *image_decode_from_file(const char *fileName, int32_t *xOut, int32_t *yOut, ImageScaler scaler) {
|
2022-09-17 03:23:32 +00:00
|
|
|
auto compOut = 0;
|
2023-09-15 08:40:27 +00:00
|
|
|
auto isVG = false; // we will not use scalers for vector graphics
|
2022-09-17 03:23:32 +00:00
|
|
|
|
2023-01-09 17:03:45 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Loading image from file %s", fileName);
|
2022-09-17 03:23:32 +00:00
|
|
|
|
|
|
|
// Attempt to load file as a PCX first using dr_pcx
|
2023-09-17 11:50:38 +00:00
|
|
|
auto pixels = reinterpret_cast<uint32_t *>(drpcx_load_file(fileName, DRPCX_FALSE, xOut, yOut, &compOut, 4));
|
2022-09-17 03:23:32 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (dr_pcx) = (%i, %i)", *xOut, *yOut);
|
|
|
|
if (!pixels) {
|
|
|
|
// If dr_pcx failed to load, then use stb_image
|
2023-09-17 11:50:38 +00:00
|
|
|
pixels = reinterpret_cast<uint32_t *>(stbi_load(fileName, xOut, yOut, &compOut, 4));
|
2022-09-17 03:23:32 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (stb_image) = (%i, %i)", *xOut, *yOut);
|
2023-09-15 10:45:16 +00:00
|
|
|
|
|
|
|
if (!pixels) {
|
2023-09-15 15:35:41 +00:00
|
|
|
pixels = image_qoi_load_from_file(fileName, xOut, yOut, &compOut);
|
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (qoi) = (%i, %i)", *xOut, *yOut);
|
2023-09-15 10:45:16 +00:00
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
if (!pixels) {
|
|
|
|
pixels = image_svg_load_from_file(fileName, xOut, yOut, scaler, &compOut, &isVG);
|
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (nanosvg) = (%i, %i)", *xOut, *yOut);
|
|
|
|
|
|
|
|
if (!pixels)
|
|
|
|
return nullptr; // Return NULL if all attempts failed
|
|
|
|
}
|
2023-09-15 10:45:16 +00:00
|
|
|
}
|
2022-09-17 03:23:32 +00:00
|
|
|
}
|
|
|
|
|
2023-02-12 22:22:16 +00:00
|
|
|
IMAGE_DEBUG_CHECK(compOut > 2);
|
2022-09-17 03:23:32 +00:00
|
|
|
|
2023-09-15 08:40:27 +00:00
|
|
|
if (!isVG)
|
2023-09-17 11:50:38 +00:00
|
|
|
pixels = image_scale(pixels, xOut, yOut, scaler);
|
2023-09-15 08:40:27 +00:00
|
|
|
|
2022-09-17 03:23:32 +00:00
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
|
2023-01-09 07:31:56 +00:00
|
|
|
/// @brief Decodes an image file from memory using the dr_pcx & stb_image libraries
|
|
|
|
/// @param data The raw pointer to the file in memory
|
|
|
|
/// @param size The size of the file in memory
|
|
|
|
/// @param xOut Out: width in pixels. This cannot be NULL
|
|
|
|
/// @param yOut Out: height in pixels. This cannot be NULL
|
2023-09-15 08:40:27 +00:00
|
|
|
/// @param scaler An optional pixel scaler to use
|
2023-01-09 07:31:56 +00:00
|
|
|
/// @return A pointer to the raw pixel data in RGBA format or NULL on failure
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint32_t *image_decode_from_memory(const uint8_t *data, size_t size, int32_t *xOut, int32_t *yOut, ImageScaler scaler) {
|
2023-01-09 07:31:56 +00:00
|
|
|
auto compOut = 0;
|
2023-09-15 08:40:27 +00:00
|
|
|
auto isVG = false; // we will not use scalers for vector graphics
|
2023-01-09 07:31:56 +00:00
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Loading image from memory");
|
|
|
|
|
|
|
|
// Attempt to load file as a PCX first using dr_pcx
|
2023-09-17 11:50:38 +00:00
|
|
|
auto pixels = reinterpret_cast<uint32_t *>(drpcx_load_memory(data, size, DRPCX_FALSE, xOut, yOut, &compOut, 4));
|
2023-01-09 07:31:56 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (dr_pcx) = (%i, %i)", *xOut, *yOut);
|
|
|
|
if (!pixels) {
|
|
|
|
// If dr_pcx failed to load, then use stb_image
|
2023-09-17 11:50:38 +00:00
|
|
|
pixels = reinterpret_cast<uint32_t *>(stbi_load_from_memory(reinterpret_cast<const stbi_uc *>(data), size, xOut, yOut, &compOut, 4));
|
2023-01-09 07:31:56 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (stb_image) = (%i, %i)", *xOut, *yOut);
|
2023-09-15 10:45:16 +00:00
|
|
|
|
|
|
|
if (!pixels) {
|
2023-09-17 11:50:38 +00:00
|
|
|
pixels = image_qoi_load_from_memory(data, size, xOut, yOut, &compOut);
|
2023-09-15 15:35:41 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (qoi) = (%i, %i)", *xOut, *yOut);
|
2023-09-15 10:45:16 +00:00
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
if (!pixels) {
|
2023-09-17 11:50:38 +00:00
|
|
|
pixels = image_svg_load_from_memory(data, size, xOut, yOut, scaler, &compOut, &isVG);
|
2023-09-15 15:35:41 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Image dimensions (nanosvg) = (%i, %i)", *xOut, *yOut);
|
|
|
|
|
|
|
|
if (!pixels)
|
|
|
|
return nullptr; // Return NULL if all attempts failed
|
|
|
|
}
|
2023-09-15 10:45:16 +00:00
|
|
|
}
|
2023-01-09 07:31:56 +00:00
|
|
|
}
|
|
|
|
|
2023-02-12 22:22:16 +00:00
|
|
|
IMAGE_DEBUG_CHECK(compOut > 2);
|
2023-01-09 07:31:56 +00:00
|
|
|
|
2023-09-15 08:40:27 +00:00
|
|
|
if (!isVG)
|
2023-09-17 11:50:38 +00:00
|
|
|
pixels = image_scale(pixels, xOut, yOut, scaler);
|
2023-09-15 08:40:27 +00:00
|
|
|
|
2023-01-09 07:31:56 +00:00
|
|
|
return pixels;
|
2022-09-17 03:23:32 +00:00
|
|
|
}
|
|
|
|
|
2023-01-09 07:31:56 +00:00
|
|
|
/// @brief Clamps a color channel to the range 0 - 255
|
|
|
|
/// @param n The color component
|
|
|
|
/// @return The clamped value
|
|
|
|
static inline uint8_t image_clamp_component(int32_t n) { return n < 0 ? 0 : n > 255 ? 255 : n; }
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief This takes in a 32bpp (BGRA) image raw data and spits out an 8bpp raw image along with it's 256 color (BGRA) palette.
|
2023-09-17 11:50:38 +00:00
|
|
|
/// @param src32 The source raw image data. This must be in BGRA format and not NULL
|
2023-12-29 09:57:27 +00:00
|
|
|
/// @param w The width of the image in pixels
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @param h The height of the image in pixels
|
|
|
|
/// @param paletteOut A 256 color palette if the operation was successful. This cannot be NULL
|
|
|
|
/// @return A pointer to a 8bpp raw image or NULL if operation failed
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint8_t *image_convert_8bpp(const uint32_t *src32, int32_t w, int32_t h, uint32_t *paletteOut) {
|
2022-09-17 03:23:32 +00:00
|
|
|
static struct {
|
|
|
|
uint32_t r, g, b;
|
|
|
|
uint32_t count;
|
|
|
|
} cubes[256];
|
|
|
|
|
|
|
|
// https://en.wikipedia.org/wiki/Ordered_dithering
|
2023-09-15 15:35:41 +00:00
|
|
|
static const uint8_t bayerMatrix[16] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
|
2022-09-17 03:23:32 +00:00
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Converting 32bpp image (%i, %i) to 8bpp", w, h);
|
|
|
|
|
|
|
|
// Allocate memory for new image (8-bit indexed)
|
|
|
|
auto pixels = (uint8_t *)malloc(w * h);
|
|
|
|
if (!pixels) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
ZERO_VARIABLE(cubes);
|
|
|
|
|
|
|
|
// Quantization phase
|
2023-09-17 11:50:38 +00:00
|
|
|
auto src = reinterpret_cast<const uint8_t *>(src32);
|
2022-09-17 03:23:32 +00:00
|
|
|
auto dst = pixels;
|
|
|
|
for (auto y = 0; y < h; y++) {
|
|
|
|
for (auto x = 0; x < w; x++) {
|
|
|
|
int32_t t = bayerMatrix[((y & 3) << 2) + (x & 3)];
|
|
|
|
int32_t b = image_clamp_component((*src++) + (t << 1));
|
|
|
|
int32_t g = image_clamp_component((*src++) + (t << 1));
|
|
|
|
int32_t r = image_clamp_component((*src++) + (t << 1));
|
|
|
|
++src; // Ignore alpha
|
|
|
|
|
|
|
|
// Quantize
|
|
|
|
uint8_t k = ((r >> 5) << 5) + ((g >> 5) << 2) + (b >> 6);
|
|
|
|
(*dst++) = k;
|
|
|
|
|
|
|
|
// Prepare RGB cubes for CLUT
|
|
|
|
cubes[k].r += r;
|
|
|
|
cubes[k].g += g;
|
|
|
|
cubes[k].b += b;
|
|
|
|
cubes[k].count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a uniform CLUT based on the quantized colors
|
|
|
|
for (auto i = 0; i < 256; i++) {
|
|
|
|
if (cubes[i].count) {
|
|
|
|
paletteOut[i] = IMAGE_MAKE_BGRA(cubes[i].r / cubes[i].count, cubes[i].g / cubes[i].count, cubes[i].b / cubes[i].count, 0xFF);
|
|
|
|
} else {
|
|
|
|
paletteOut[i] = IMAGE_MAKE_BGRA(0, 0, 0, 0xFF);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief This takes in a 32bpp (BGRA) image raw data and spits out an 8bpp raw image along with it's 256 color (BGRA) palette.
|
2022-09-17 03:23:32 +00:00
|
|
|
/// If the number of unique colors in the 32bpp image > 256, then the functions returns a NULL.
|
|
|
|
/// Unlike image_convert_8bpp(), no 'real' conversion takes place.
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @param src The source raw image data. This must be in BGRA format and not NULL
|
2023-12-29 09:57:27 +00:00
|
|
|
/// @param w The width of the image in pixels
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @param h The height of the image in pixels
|
|
|
|
/// @param paletteOut A 256 color palette if the operation was successful. This cannot be NULL
|
|
|
|
/// @return A pointer to a 8bpp raw image or NULL if operation failed
|
2023-09-17 11:50:38 +00:00
|
|
|
static uint8_t *image_extract_8bpp(const uint32_t *src, int32_t w, int32_t h, uint32_t *paletteOut) {
|
2022-09-17 03:23:32 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Extracting 8bpp image (%i, %i) from 32bpp", w, h);
|
|
|
|
|
2023-01-09 07:31:56 +00:00
|
|
|
std::unordered_map<uint32_t, int> colorMap;
|
2022-09-17 18:33:08 +00:00
|
|
|
|
2022-09-17 03:23:32 +00:00
|
|
|
// Allocate memory for new image (8-bit indexed)
|
|
|
|
auto pixels = (uint8_t *)malloc(w * h);
|
2023-09-17 11:50:38 +00:00
|
|
|
if (!pixels)
|
2022-09-17 03:23:32 +00:00
|
|
|
return nullptr;
|
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
auto uniqueColors = 0; // as long as this is < 256 we will keep going until we are done
|
|
|
|
size_t size = w * h;
|
|
|
|
for (auto i = 0; i < size; i++) {
|
|
|
|
auto srcColor = src[i]; // get the 32bpp pixel
|
2022-09-17 03:23:32 +00:00
|
|
|
|
|
|
|
// Check if the src color exists in our palette
|
2023-09-15 08:40:27 +00:00
|
|
|
if (colorMap.count(srcColor) == 0) {
|
2022-09-17 03:23:32 +00:00
|
|
|
// If we reached here, then the color is not in our table
|
2023-01-11 10:48:43 +00:00
|
|
|
if (uniqueColors > 255) {
|
|
|
|
IMAGE_DEBUG_PRINT("Image has more than %i unique colors", uniqueColors);
|
2022-09-17 03:23:32 +00:00
|
|
|
free(pixels);
|
|
|
|
return nullptr; // Exit with failure if we have > 256 colors
|
|
|
|
}
|
|
|
|
|
2023-01-11 10:48:43 +00:00
|
|
|
paletteOut[uniqueColors] = srcColor; // Store the color as unique
|
|
|
|
colorMap[srcColor] = uniqueColors; // Add this color to the map
|
|
|
|
pixels[i] = uniqueColors; // set the pixel to the color index
|
|
|
|
++uniqueColors; // increment unique colors
|
2022-09-17 03:23:32 +00:00
|
|
|
} else {
|
|
|
|
// If we reached here, then the color is in our table
|
2022-09-17 18:33:08 +00:00
|
|
|
pixels[i] = colorMap[srcColor]; // Simply fetch the index from the map
|
2022-09-17 03:23:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-09 17:03:45 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Unique colors = %i", uniqueColors);
|
|
|
|
|
2022-09-17 03:23:32 +00:00
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
|
2023-09-15 15:35:41 +00:00
|
|
|
/// @brief Calculates the distance between 2 RBG points in the RGB color cube
|
|
|
|
/// @param r1 R1
|
|
|
|
/// @param g1 G1
|
|
|
|
/// @param b1 B1
|
|
|
|
/// @param r2 R2
|
|
|
|
/// @param g2 G2
|
|
|
|
/// @param b2 B2
|
|
|
|
/// @return The distance in floating point
|
|
|
|
static inline float image_calculate_rgb_distance(uint8_t r1, uint8_t g1, uint8_t b1, uint8_t r2, uint8_t g2, uint8_t b2) {
|
|
|
|
return sqrt(((float(r2) - float(r1)) * (float(r2) - float(r1))) + ((float(g2) - float(g1)) * (float(g2) - float(g1))) +
|
|
|
|
((float(b2) - float(b1)) * (float(b2) - float(b1))));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// @brief This modifies an *8bpp* image 'src' to use 'dst_pal' instead of 'src_pal'
|
|
|
|
/// @param src A pointer to the 8bpp image pixel data. This modifies data 'src' points to and cannot be NULL
|
|
|
|
/// @param w The width of the image in pixels
|
|
|
|
/// @param h The height of the image in pixels
|
|
|
|
/// @param src_pal The image's original palette. This cannot be NULL
|
|
|
|
/// @param dst_pal The destination palette. This cannot be NULL
|
2023-09-17 06:22:54 +00:00
|
|
|
static void image_remap_palette(uint8_t *src, int32_t w, int32_t h, const uint32_t *src_pal, const uint32_t *dst_pal) {
|
2023-09-17 11:50:38 +00:00
|
|
|
auto const maxRGBDist = image_calculate_rgb_distance(0, 0, 0, 255, 255, 255); // The farthest we can go in the color cube
|
2022-09-17 03:23:32 +00:00
|
|
|
static uint32_t palMap[256];
|
2023-09-17 06:22:54 +00:00
|
|
|
ZERO_VARIABLE(palMap);
|
2022-09-17 03:23:32 +00:00
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Remapping 8bpp image (%i, %i) palette", w, h);
|
|
|
|
|
|
|
|
// Match the palette
|
|
|
|
for (auto x = 0; x < 256; x++) {
|
2023-09-17 11:50:38 +00:00
|
|
|
auto oldDist = maxRGBDist;
|
2023-09-17 06:22:54 +00:00
|
|
|
|
2022-09-17 03:23:32 +00:00
|
|
|
for (auto y = 0; y < 256; y++) {
|
2023-09-15 15:35:41 +00:00
|
|
|
auto newDist = image_calculate_rgb_distance(IMAGE_GET_BGRA_RED(src_pal[x]), IMAGE_GET_BGRA_GREEN(src_pal[x]), IMAGE_GET_BGRA_BLUE(src_pal[x]),
|
2022-09-17 03:23:32 +00:00
|
|
|
IMAGE_GET_BGRA_RED(dst_pal[y]), IMAGE_GET_BGRA_GREEN(dst_pal[y]), IMAGE_GET_BGRA_BLUE(dst_pal[y]));
|
|
|
|
|
|
|
|
if (oldDist > newDist) {
|
|
|
|
oldDist = newDist;
|
|
|
|
palMap[x] = y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the bitmap to use the matched palette
|
|
|
|
for (auto c = 0; c < (w * h); c++) {
|
|
|
|
src[c] = palMap[src[c]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
/// @brief Helps convert a BGRA color to an RGBA color and back
|
|
|
|
/// @param clr A BGRA color or an RGBA color
|
|
|
|
/// @return An RGBA color or a BGRA color
|
|
|
|
static inline uint32_t image_swap_red_blue(uint32_t clr) { return ((clr & 0xFF00FF00u) | ((clr & 0x00FF0000u) >> 16) | ((clr & 0x000000FFu) << 16)); }
|
|
|
|
|
2023-02-13 10:54:11 +00:00
|
|
|
/// @brief This function loads an image into memory and returns valid LONG image handle values that are less than -1
|
2023-09-17 06:22:54 +00:00
|
|
|
/// @param qbsFileName The filename or memory buffer (see requirements below) of the image
|
2023-12-29 09:57:27 +00:00
|
|
|
/// @param bpp 32 = 32bpp, 33 = 32bpp (hardware accelerated), 256=8bpp or 257=8bpp (without palette remap)
|
2023-09-17 06:22:54 +00:00
|
|
|
/// @param qbsRequirements A qbs that can contain one or more of: hardware, memory, adaptive
|
2023-01-11 10:48:43 +00:00
|
|
|
/// @param passed How many parameters were passed?
|
|
|
|
/// @return Valid LONG image handle values that are less than -1 or -1 on failure
|
2023-09-17 06:22:54 +00:00
|
|
|
int32_t func__loadimage(qbs *qbsFileName, int32_t bpp, qbs *qbsRequirements, int32_t passed) {
|
|
|
|
if (new_error || !qbsFileName->len) // leave if we do not have a file name, data or there was an error
|
2023-01-09 17:03:45 +00:00
|
|
|
return INVALID_IMAGE_HANDLE;
|
2022-09-17 03:23:32 +00:00
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
auto isLoadFromMemory = false; // should the image be loaded from memory?
|
|
|
|
auto isHardwareImage = false; // should the image be converted to a hardware image?
|
|
|
|
auto isRemapPalette = true; // should the palette be re-mapped to the QB64 default palette?
|
|
|
|
auto scaler = ImageScaler::NONE; // default to no scaling
|
2023-01-09 17:03:45 +00:00
|
|
|
|
|
|
|
// Handle special cases and set the above flags if required
|
2023-09-15 08:40:27 +00:00
|
|
|
IMAGE_DEBUG_PRINT("bpp = %i, passed = 0x%X", bpp, passed);
|
2023-02-12 22:22:16 +00:00
|
|
|
if (passed & 1) {
|
|
|
|
if (bpp == 33) { // hardware image?
|
|
|
|
isHardwareImage = true;
|
|
|
|
bpp = 32;
|
|
|
|
IMAGE_DEBUG_PRINT("bpp = 0x%X", bpp);
|
|
|
|
} else if (bpp == 257) { // adaptive palette?
|
|
|
|
isRemapPalette = false;
|
|
|
|
bpp = 256;
|
|
|
|
IMAGE_DEBUG_PRINT("bpp = 0x%X", bpp);
|
|
|
|
}
|
2022-09-17 03:23:32 +00:00
|
|
|
|
2023-02-12 22:22:16 +00:00
|
|
|
if ((bpp != 32) && (bpp != 256)) { // invalid BPP?
|
2023-01-09 17:03:45 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Invalid bpp (0x%X)", bpp);
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
|
2023-01-09 17:03:45 +00:00
|
|
|
return INVALID_IMAGE_HANDLE;
|
2022-09-17 03:23:32 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-02-12 22:22:16 +00:00
|
|
|
if (write_page->bits_per_pixel < 32) { // default to 8bpp for all legacy screen modes
|
2023-01-09 17:03:45 +00:00
|
|
|
bpp = 256;
|
|
|
|
IMAGE_DEBUG_PRINT("Defaulting to 8bpp");
|
2023-02-12 22:22:16 +00:00
|
|
|
} else { // default to 32bpp for everything else
|
2023-01-09 17:03:45 +00:00
|
|
|
bpp = 32;
|
|
|
|
IMAGE_DEBUG_PRINT("Defaulting to 32bpp");
|
2022-09-17 03:23:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-12 22:22:16 +00:00
|
|
|
// Check requirements string and set appropriate flags
|
2023-09-17 06:22:54 +00:00
|
|
|
if ((passed & 2) && qbsRequirements->len) {
|
2023-09-17 11:50:38 +00:00
|
|
|
// Parse the requirements string and setup save settings
|
|
|
|
std::string requirements(reinterpret_cast<char *>(qbsRequirements->chr), qbsRequirements->len);
|
|
|
|
std::transform(requirements.begin(), requirements.end(), requirements.begin(), [](unsigned char c) { return std::toupper(c); });
|
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Parsing requirements string: %s", requirements.c_str());
|
2023-02-12 22:22:16 +00:00
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
if (requirements.find("HARDWARE") != std::string::npos && bpp == 32) {
|
2023-02-12 22:22:16 +00:00
|
|
|
isHardwareImage = true;
|
2023-09-17 11:50:38 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Hardware image selected");
|
|
|
|
} else if (requirements.find("ADAPTIVE") != std::string::npos && bpp == 256) {
|
2023-02-12 22:22:16 +00:00
|
|
|
isRemapPalette = false;
|
2023-09-17 11:50:38 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Adaptive palette selected");
|
2023-02-12 22:22:16 +00:00
|
|
|
}
|
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
if (requirements.find("MEMORY") != std::string::npos) {
|
2023-02-12 22:22:16 +00:00
|
|
|
isLoadFromMemory = true;
|
|
|
|
IMAGE_DEBUG_PRINT("Loading image from memory");
|
|
|
|
}
|
2023-09-15 08:40:27 +00:00
|
|
|
|
|
|
|
// Parse scaler string
|
2023-09-17 11:50:38 +00:00
|
|
|
for (auto i = 0; i < GET_ARRAY_SIZE(g_ImageScalerName); i++) {
|
|
|
|
IMAGE_DEBUG_PRINT("Checking for: %s", g_ImageScalerName[i]);
|
|
|
|
if (requirements.find(g_ImageScalerName[i]) != std::string::npos) {
|
|
|
|
scaler = (ImageScaler)i;
|
|
|
|
IMAGE_DEBUG_PRINT("%s scaler selected", g_ImageScalerName[(int)scaler]);
|
|
|
|
break;
|
|
|
|
}
|
2023-09-15 08:40:27 +00:00
|
|
|
}
|
2023-02-12 22:22:16 +00:00
|
|
|
}
|
|
|
|
|
2023-09-17 06:22:54 +00:00
|
|
|
auto x = 0, y = 0;
|
2023-09-17 11:50:38 +00:00
|
|
|
uint32_t *pixels;
|
2023-01-09 07:31:56 +00:00
|
|
|
|
2023-01-09 17:03:45 +00:00
|
|
|
if (isLoadFromMemory) {
|
2023-09-17 06:22:54 +00:00
|
|
|
pixels = image_decode_from_memory(qbsFileName->chr, qbsFileName->len, &x, &y, scaler);
|
2023-01-09 07:31:56 +00:00
|
|
|
} else {
|
2023-09-17 11:50:38 +00:00
|
|
|
std::string fileName(reinterpret_cast<char *>(qbsFileName->chr), qbsFileName->len);
|
2023-12-28 21:25:52 +00:00
|
|
|
pixels = image_decode_from_file(filepath_fix_directory(fileName), &x, &y, scaler);
|
2023-01-09 07:31:56 +00:00
|
|
|
}
|
|
|
|
|
2022-09-17 03:23:32 +00:00
|
|
|
if (!pixels)
|
|
|
|
return INVALID_IMAGE_HANDLE; // Return invalid handle if loading the image failed
|
|
|
|
|
|
|
|
// Convert RGBA to BGRA
|
2023-09-17 11:50:38 +00:00
|
|
|
size_t size = x * y;
|
|
|
|
for (auto i = 0; i < size; i++)
|
|
|
|
pixels[i] = image_swap_red_blue(pixels[i]);
|
2022-09-17 03:23:32 +00:00
|
|
|
|
|
|
|
int32_t i; // Image handle to be returned
|
|
|
|
|
|
|
|
// Convert image to 8bpp if requested by the user
|
|
|
|
if (bpp == 256) {
|
|
|
|
IMAGE_DEBUG_PRINT("Entering 8bpp path");
|
|
|
|
|
|
|
|
i = func__newimage(x, y, 256, 1);
|
2023-01-09 17:03:45 +00:00
|
|
|
if (i == INVALID_IMAGE_HANDLE) {
|
2022-09-17 03:23:32 +00:00
|
|
|
free(pixels);
|
|
|
|
return INVALID_IMAGE_HANDLE;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto palette = (uint32_t *)malloc(256 * sizeof(uint32_t)); // 3 bytes for bgr + 1 for alpha (basically a uint32_t)
|
|
|
|
if (!palette) {
|
|
|
|
free(pixels);
|
|
|
|
return INVALID_IMAGE_HANDLE;
|
|
|
|
}
|
|
|
|
|
2023-09-17 11:50:38 +00:00
|
|
|
auto pixels256 = image_extract_8bpp(pixels, x, y, palette); // Try to simply 'extract' the 8bpp image first
|
2022-09-17 03:23:32 +00:00
|
|
|
if (!pixels256) {
|
|
|
|
pixels256 = image_convert_8bpp(pixels, x, y, palette); // If that fails, then 'convert' it to 8bpp
|
|
|
|
if (!pixels256) {
|
|
|
|
free(palette);
|
|
|
|
free(pixels);
|
|
|
|
return INVALID_IMAGE_HANDLE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-09 17:03:45 +00:00
|
|
|
if (isRemapPalette) {
|
2022-09-17 03:23:32 +00:00
|
|
|
// Remap the image indexes to QB64 default palette and then free our palette
|
|
|
|
image_remap_palette(pixels256, x, y, palette, palette_256);
|
|
|
|
free(palette);
|
|
|
|
|
|
|
|
// Copy the 8bpp pixel data and then free it
|
|
|
|
memcpy(img[-i].offset, pixels256, x * y);
|
|
|
|
free(pixels256);
|
|
|
|
|
|
|
|
// Copy the default QB64 palette
|
|
|
|
memcpy(img[-i].pal, palette_256, 256 * sizeof(uint32_t));
|
2023-01-09 17:03:45 +00:00
|
|
|
} else {
|
|
|
|
// Copy the 8bpp pixel data and then free it
|
|
|
|
memcpy(img[-i].offset, pixels256, x * y);
|
|
|
|
free(pixels256);
|
|
|
|
|
|
|
|
// Copy the palette and then free it
|
|
|
|
memcpy(img[-i].pal, palette, 256 * sizeof(uint32_t));
|
|
|
|
free(palette);
|
2022-09-17 03:23:32 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
IMAGE_DEBUG_PRINT("Entering 32bpp path");
|
|
|
|
|
|
|
|
i = func__newimage(x, y, 32, 1);
|
2023-01-09 17:03:45 +00:00
|
|
|
if (i == INVALID_IMAGE_HANDLE) {
|
2022-09-17 03:23:32 +00:00
|
|
|
free(pixels);
|
|
|
|
return INVALID_IMAGE_HANDLE;
|
|
|
|
}
|
2023-09-17 11:50:38 +00:00
|
|
|
memcpy(img[-i].offset32, pixels, size * sizeof(uint32_t));
|
2022-09-17 03:23:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Free pixel memory. We can do this because both dr_pcx and stb_image uses free()
|
|
|
|
free(pixels);
|
|
|
|
|
|
|
|
// This only executes if bpp is 32
|
2023-01-09 17:03:45 +00:00
|
|
|
if (isHardwareImage) {
|
2022-09-17 03:23:32 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Making hardware image");
|
|
|
|
|
|
|
|
auto iHardware = func__copyimage(i, 33, 1);
|
|
|
|
sub__freeimage(i, 1);
|
|
|
|
i = iHardware;
|
|
|
|
}
|
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Returning handle value = %i", i);
|
|
|
|
|
|
|
|
return i;
|
|
|
|
}
|
2023-09-17 06:22:54 +00:00
|
|
|
|
|
|
|
/// @brief Saves an image to the disk from a QB64-PE image handle
|
|
|
|
/// @param qbsFileName The file path name to save to
|
|
|
|
/// @param imageHandle Optional: The image handle. If omitted, then this is _DISPLAY()
|
|
|
|
/// @param qbsRequirements Optional: Extra format and setting arguments
|
2023-12-29 09:57:27 +00:00
|
|
|
/// @param passed Optional parameters
|
2023-09-17 06:22:54 +00:00
|
|
|
void sub__saveimage(qbs *qbsFileName, int32_t imageHandle, qbs *qbsRequirements, int32_t passed) {
|
2023-09-20 15:02:29 +00:00
|
|
|
enum struct SaveFormat { PNG = 0, QOI, BMP, TGA, JPG, HDR };
|
|
|
|
static const char *formatName[] = {"png", "qoi", "bmp", "tga", "jpg", "hdr"};
|
2023-09-17 06:22:54 +00:00
|
|
|
|
|
|
|
if (new_error) // leave if there was an error
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!qbsFileName->len) { // empty file names not allowed
|
|
|
|
IMAGE_DEBUG_PRINT("Empty file name");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_BAD_FILE_NAME);
|
2023-09-17 06:22:54 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (passed & 1) {
|
|
|
|
// Check and validate image handle
|
|
|
|
IMAGE_DEBUG_PRINT("Validating handle %i", imageHandle);
|
|
|
|
|
|
|
|
if (imageHandle >= 0) {
|
|
|
|
validatepage(imageHandle);
|
|
|
|
imageHandle = page[imageHandle];
|
|
|
|
} else {
|
|
|
|
imageHandle = -imageHandle;
|
|
|
|
|
|
|
|
if (imageHandle >= nextimg) {
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_INVALID_HANDLE);
|
2023-09-17 06:22:54 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!img[imageHandle].valid) {
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_INVALID_HANDLE);
|
2023-09-17 06:22:54 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Use default image handle
|
|
|
|
IMAGE_DEBUG_PRINT("Using default handle");
|
|
|
|
|
|
|
|
imageHandle = -func__display();
|
|
|
|
|
|
|
|
// Safety
|
|
|
|
if (imageHandle >= nextimg) {
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_INVALID_HANDLE);
|
2023-09-17 06:22:54 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!img[imageHandle].valid) {
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_INVALID_HANDLE);
|
2023-09-17 06:22:54 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Using image handle %i", imageHandle);
|
|
|
|
|
|
|
|
auto format = SaveFormat::PNG; // we always default to PNG
|
|
|
|
|
|
|
|
if ((passed & 2) && qbsRequirements->len) {
|
|
|
|
// Parse the requirements string and setup save settings
|
|
|
|
std::string requirements(reinterpret_cast<char *>(qbsRequirements->chr), qbsRequirements->len);
|
|
|
|
std::transform(requirements.begin(), requirements.end(), requirements.begin(), [](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Parsing requirements string: %s", requirements.c_str());
|
|
|
|
|
|
|
|
for (auto i = 0; i < GET_ARRAY_SIZE(formatName); i++) {
|
|
|
|
IMAGE_DEBUG_PRINT("Checking for: %s", formatName[i]);
|
|
|
|
if (requirements.find(formatName[i]) != std::string::npos) {
|
|
|
|
format = (SaveFormat)i;
|
|
|
|
IMAGE_DEBUG_PRINT("Found: %s", formatName[(int)format]);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Format selected: %s", formatName[(int)format]);
|
|
|
|
|
|
|
|
std::string fileName(reinterpret_cast<char *>(qbsFileName->chr), qbsFileName->len);
|
2023-12-28 21:25:52 +00:00
|
|
|
filepath_fix_directory(fileName);
|
2023-09-17 06:22:54 +00:00
|
|
|
|
|
|
|
// Check if fileName has a valid extension and add one if it does not have one
|
|
|
|
if (fileName.length() > 4) { // must be at least n.ext
|
|
|
|
auto fileExtension = fileName.substr(fileName.length() - 4);
|
|
|
|
std::transform(fileExtension.begin(), fileExtension.end(), fileExtension.begin(), [](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("File extension: %s", fileExtension.c_str());
|
|
|
|
|
|
|
|
int i;
|
|
|
|
for (i = 0; i < GET_ARRAY_SIZE(formatName); i++) {
|
|
|
|
std::string formatExtension;
|
|
|
|
|
|
|
|
formatExtension = ".";
|
|
|
|
formatExtension.append(formatName[i]);
|
|
|
|
|
|
|
|
IMAGE_DEBUG_PRINT("Check extension name: %s", formatExtension.c_str());
|
|
|
|
|
|
|
|
if (fileExtension == formatExtension) {
|
|
|
|
IMAGE_DEBUG_PRINT("Extension (%s) matches with format %i", formatExtension.c_str(), i);
|
2023-09-18 18:36:13 +00:00
|
|
|
format = (SaveFormat)i;
|
|
|
|
IMAGE_DEBUG_PRINT("Format selected by extension: %s", formatName[(int)format]);
|
2023-09-17 06:22:54 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i >= GET_ARRAY_SIZE(formatName)) { // no matches
|
|
|
|
IMAGE_DEBUG_PRINT("No matching extension. Adding .%s", formatName[(int)format]);
|
|
|
|
|
|
|
|
fileName.append(".");
|
|
|
|
fileName.append(formatName[(int)format]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Simply add the selected format's extension
|
|
|
|
IMAGE_DEBUG_PRINT("Adding extension: .%s", formatName[(int)format]);
|
|
|
|
|
|
|
|
fileName.append(".");
|
|
|
|
fileName.append(formatName[(int)format]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This will hold our raw RGBA pixel data
|
|
|
|
std::vector<uint32_t> pixels;
|
|
|
|
int32_t width, height;
|
|
|
|
|
|
|
|
if (img[imageHandle].text) {
|
|
|
|
IMAGE_DEBUG_PRINT("Rendering text surface to RGBA");
|
|
|
|
|
|
|
|
auto const fontWidth = 8;
|
|
|
|
auto fontHeight = 16;
|
|
|
|
if (img[imageHandle].font == 8 || img[imageHandle].font == 14)
|
|
|
|
fontHeight = img[imageHandle].font;
|
|
|
|
|
|
|
|
width = fontWidth * img[imageHandle].width;
|
|
|
|
height = fontHeight * img[imageHandle].height;
|
|
|
|
|
|
|
|
pixels.resize(width * height);
|
|
|
|
|
|
|
|
uint8_t fc, bc, *c = img[imageHandle].offset; // set to the first codepoint
|
|
|
|
uint8_t const *builtinFont = nullptr;
|
|
|
|
|
|
|
|
// Render all text to the raw pixel array
|
|
|
|
for (auto y = 0; y < height; y += fontHeight) {
|
|
|
|
for (auto x = 0; x < width; x += fontWidth) {
|
|
|
|
switch (fontHeight) {
|
|
|
|
case 8:
|
|
|
|
builtinFont = &charset8x8[*c][0][0];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 14:
|
|
|
|
builtinFont = &charset8x16[*c][1][0];
|
|
|
|
break;
|
|
|
|
|
|
|
|
default: // 16
|
|
|
|
builtinFont = &charset8x16[*c][0][0];
|
|
|
|
}
|
|
|
|
|
|
|
|
++c; // move to the attribute
|
2023-09-20 15:02:29 +00:00
|
|
|
fc = *c & 0x0F;
|
2023-09-17 11:50:38 +00:00
|
|
|
bc = ((*c >> 4) & 7) + ((*c >> 7) << 3);
|
2023-09-17 06:22:54 +00:00
|
|
|
|
|
|
|
// Inner codepoint rendering loop
|
|
|
|
for (auto dy = y, py = 0; py < fontHeight; dy++, py++) {
|
|
|
|
for (auto dx = x, px = 0; px < fontWidth; dx++, px++) {
|
2023-09-17 08:16:36 +00:00
|
|
|
pixels[width * dy + dx] = image_swap_red_blue(*builtinFont ? palette_256[fc] : palette_256[bc]);
|
2023-09-17 06:22:54 +00:00
|
|
|
++builtinFont;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
++c; // move to the next codepoint
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
width = img[imageHandle].width;
|
|
|
|
height = img[imageHandle].height;
|
|
|
|
|
|
|
|
pixels.resize(width * height);
|
|
|
|
|
|
|
|
if (img[imageHandle].bits_per_pixel == 32) { // BGRA pixels
|
2023-12-29 09:57:27 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Converting BGRA surface to RGBA");
|
2023-09-17 06:22:54 +00:00
|
|
|
|
|
|
|
auto p = img[imageHandle].offset32;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pixels.size(); i++) {
|
|
|
|
pixels[i] = image_swap_red_blue(*p);
|
|
|
|
++p;
|
|
|
|
}
|
|
|
|
} else { // indexed pixels
|
2023-12-29 09:57:27 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Converting BGRA indexed surface to RGBA");
|
2023-09-17 06:22:54 +00:00
|
|
|
auto p = img[imageHandle].offset;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pixels.size(); i++) {
|
|
|
|
pixels[i] = image_swap_red_blue(img[imageHandle].pal[*p]);
|
|
|
|
++p;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-18 18:36:13 +00:00
|
|
|
IMAGE_DEBUG_PRINT("Saving to: %s (%i x %i), %llu pixels, %s", fileName.c_str(), width, height, pixels.size(), formatName[(int)format]);
|
2023-09-17 06:22:54 +00:00
|
|
|
|
|
|
|
switch (format) {
|
2023-09-17 08:16:36 +00:00
|
|
|
case SaveFormat::PNG: {
|
2023-09-18 18:36:13 +00:00
|
|
|
stbi_write_png_compression_level = 100;
|
2023-09-17 06:22:54 +00:00
|
|
|
if (!stbi_write_png(fileName.c_str(), width, height, sizeof(uint32_t), pixels.data(), 0)) {
|
|
|
|
IMAGE_DEBUG_PRINT("stbi_write_png() failed");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
|
2023-09-17 06:22:54 +00:00
|
|
|
}
|
2023-09-17 08:16:36 +00:00
|
|
|
} break;
|
2023-09-17 06:22:54 +00:00
|
|
|
|
2023-09-17 08:16:36 +00:00
|
|
|
case SaveFormat::QOI: {
|
|
|
|
qoi_desc desc;
|
|
|
|
desc.width = width;
|
|
|
|
desc.height = height;
|
|
|
|
desc.channels = sizeof(uint32_t);
|
|
|
|
desc.colorspace = QOI_SRGB;
|
|
|
|
|
|
|
|
if (!qoi_write(fileName.c_str(), pixels.data(), &desc)) {
|
2023-09-17 06:22:54 +00:00
|
|
|
IMAGE_DEBUG_PRINT("qoi_write() failed");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
|
2023-09-17 06:22:54 +00:00
|
|
|
}
|
2023-09-17 08:16:36 +00:00
|
|
|
} break;
|
2023-09-17 06:22:54 +00:00
|
|
|
|
2023-09-17 08:16:36 +00:00
|
|
|
case SaveFormat::BMP: {
|
2023-09-17 06:22:54 +00:00
|
|
|
if (!stbi_write_bmp(fileName.c_str(), width, height, sizeof(uint32_t), pixels.data())) {
|
|
|
|
IMAGE_DEBUG_PRINT("stbi_write_bmp() failed");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
|
2023-09-17 06:22:54 +00:00
|
|
|
}
|
2023-09-17 08:16:36 +00:00
|
|
|
} break;
|
2023-09-17 06:22:54 +00:00
|
|
|
|
2023-09-17 08:16:36 +00:00
|
|
|
case SaveFormat::TGA: {
|
2023-09-17 06:22:54 +00:00
|
|
|
if (!stbi_write_tga(fileName.c_str(), width, height, sizeof(uint32_t), pixels.data())) {
|
|
|
|
IMAGE_DEBUG_PRINT("stbi_write_tga() failed");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
|
2023-09-17 06:22:54 +00:00
|
|
|
}
|
2023-09-17 08:16:36 +00:00
|
|
|
} break;
|
2023-09-17 06:22:54 +00:00
|
|
|
|
2023-09-17 08:16:36 +00:00
|
|
|
case SaveFormat::JPG: {
|
2023-09-17 06:22:54 +00:00
|
|
|
if (!stbi_write_jpg(fileName.c_str(), width, height, sizeof(uint32_t), pixels.data(), 100)) {
|
|
|
|
IMAGE_DEBUG_PRINT("stbi_write_jpg() failed");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
|
2023-09-17 06:22:54 +00:00
|
|
|
}
|
2023-09-17 08:16:36 +00:00
|
|
|
} break;
|
2023-09-17 06:22:54 +00:00
|
|
|
|
2023-09-20 15:02:29 +00:00
|
|
|
case SaveFormat::HDR: {
|
|
|
|
IMAGE_DEBUG_PRINT("Converting RGBA to linear float data");
|
|
|
|
|
|
|
|
const auto HDRComponents = 4;
|
|
|
|
|
|
|
|
std::vector<float> HDRPixels;
|
|
|
|
HDRPixels.resize(pixels.size() * HDRComponents);
|
|
|
|
|
2023-09-20 15:09:56 +00:00
|
|
|
for (size_t j = 0, i = 0; i < pixels.size(); i++) {
|
2023-09-20 15:02:29 +00:00
|
|
|
HDRPixels[j] = pow((pixels[i] & 0xFFu) / 255.0f, 2.2f);
|
|
|
|
++j;
|
|
|
|
HDRPixels[j] = pow(((pixels[i] >> 8) & 0xFFu) / 255.0f, 2.2f);
|
|
|
|
++j;
|
|
|
|
HDRPixels[j] = pow(((pixels[i] >> 16) & 0xFFu) / 255.0f, 2.2f);
|
|
|
|
++j;
|
|
|
|
HDRPixels[j] = (pixels[i] >> 24) / 255.0f;
|
|
|
|
++j;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!stbi_write_hdr(fileName.c_str(), width, height, HDRComponents, HDRPixels.data())) {
|
|
|
|
IMAGE_DEBUG_PRINT("stbi_write_hdr() failed");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_ILLEGAL_FUNCTION_CALL);
|
2023-09-20 15:02:29 +00:00
|
|
|
}
|
|
|
|
} break;
|
|
|
|
|
2023-09-17 06:22:54 +00:00
|
|
|
default:
|
|
|
|
IMAGE_DEBUG_PRINT("Save handler not implemented");
|
2023-12-29 12:27:35 +00:00
|
|
|
error(QB_ERROR_INTERNAL_ERROR);
|
2023-09-17 06:22:54 +00:00
|
|
|
}
|
|
|
|
}
|