From dd710822bf64e75c618357d4cb8887d59c8bdc66 Mon Sep 17 00:00:00 2001
From: Xen <[EMAIL REDACTED]>
Date: Tue, 29 Jul 2025 23:36:28 +0300
Subject: [PATCH] Functions to Save WebP Images (#573)
---
include/SDL3_image/SDL_image.h | 45 +++++++
src/IMG_webp.c | 208 +++++++++++++++++++++++++++++++++
2 files changed, 253 insertions(+)
diff --git a/include/SDL3_image/SDL_image.h b/include/SDL3_image/SDL_image.h
index bde984db..0fd618dc 100644
--- a/include/SDL3_image/SDL_image.h
+++ b/include/SDL3_image/SDL_image.h
@@ -1967,6 +1967,51 @@ extern SDL_DECLSPEC bool SDLCALL IMG_SaveJPG(SDL_Surface *surface, const char *f
*/
extern SDL_DECLSPEC bool SDLCALL IMG_SaveJPG_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio, int quality);
+/**
+ * Save an SDL_Surface into WEBP image data, via an SDL_IOStream.
+ *
+ * If you just want to save to a filename, you can use IMG_SaveWEBP() instead.
+ *
+ * If `closeio` is true, `dst` will be closed before returning, whether this
+ * function succeeds or not.
+ *
+ * \param surface the SDL surface to save.
+ * \param dst the SDL_IOStream to save the image data to.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ * to leave it open.
+ * \param quality between 0 and 100. For lossy, 0 gives the smallest size and
+ * 100 the largest. For lossless, this parameter is the amount
+ * of effort put into the compression: 0 is the fastest but
+ * gives larger files compared to the slowest, but best, 100.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.2.18.
+ *
+ * \sa IMG_SaveWEBP
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveWEBP_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio, float quality);
+
+/**
+ * Save an SDL_Surface into a WEBP image file.
+ *
+ * If the file already exists, it will be overwritten.
+ *
+ * \param surface the SDL surface to save.
+ * \param file path on the filesystem to write the new file to.
+ * \param quality between 0 and 100. For lossy, 0 gives the smallest size and
+ * 100 the largest. For lossless, this parameter is the amount
+ * of effort put into the compression: 0 is the fastest but
+ * gives larger files compared to the slowest, but best, 100.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.2.18.
+ *
+ * \sa IMG_SaveWEBP_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveWEBP(SDL_Surface *surface, const char *file, float quality);
+
/**
* Animated image support
*
diff --git a/src/IMG_webp.c b/src/IMG_webp.c
index db279e07..fd0274b8 100644
--- a/src/IMG_webp.c
+++ b/src/IMG_webp.c
@@ -41,6 +41,7 @@
#endif
#include <webp/decode.h>
#include <webp/demux.h>
+#include <webp/encode.h>
static struct {
int loaded;
@@ -55,6 +56,17 @@ static struct {
void (*WebPDemuxReleaseIterator)(WebPIterator *iter);
uint32_t (*WebPDemuxGetI)(const WebPDemuxer* dmux, WebPFormatFeature feature);
void (*WebPDemuxDelete)(WebPDemuxer* dmux);
+
+ // Encoding functions
+ int (*WebPConfigInitInternal)(WebPConfig *, WebPPreset, float, int);
+ int (*WebPValidateConfig)(const WebPConfig *);
+ int (*WebPPictureInitInternal)(WebPPicture *, int);
+ int (*WebPEncode)(const WebPConfig *, WebPPicture *);
+ void (*WebPPictureFree)(WebPPicture *);
+ int (*WebPPictureImportRGBA)(WebPPicture *, const uint8_t *, int);
+ void (*WebPMemoryWriterInit)(WebPMemoryWriter *);
+ int (*WebPMemoryWrite)(const uint8_t *, size_t, const WebPPicture *);
+ void (*WebPMemoryWriterClear)(WebPMemoryWriter *);
} lib;
#if defined(LOAD_WEBP_DYNAMIC) && defined(LOAD_WEBPDEMUX_DYNAMIC)
@@ -99,6 +111,18 @@ static bool IMG_InitWEBP(void)
FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxReleaseIterator, void (*)(WebPIterator *iter))
FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxGetI, uint32_t (*)(const WebPDemuxer* dmux, WebPFormatFeature feature))
FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxDelete, void (*)(WebPDemuxer* dmux))
+
+ // Encoding functions
+ FUNCTION_LOADER_LIBWEBP(WebPConfigInitInternal, int (*)(WebPConfig *, WebPPreset, float, int))
+ FUNCTION_LOADER_LIBWEBP(WebPValidateConfig, int (*)(const WebPConfig *))
+ FUNCTION_LOADER_LIBWEBP(WebPPictureInitInternal, int (*)(WebPPicture *, int))
+ FUNCTION_LOADER_LIBWEBP(WebPEncode, int (*)(const WebPConfig *, WebPPicture *))
+ FUNCTION_LOADER_LIBWEBP(WebPPictureFree, void (*)(WebPPicture *))
+ FUNCTION_LOADER_LIBWEBP(WebPPictureImportRGBA, int (*)(WebPPicture *, const uint8_t *, int))
+
+ FUNCTION_LOADER_LIBWEBP(WebPMemoryWriterInit, void (*)(WebPMemoryWriter *))
+ FUNCTION_LOADER_LIBWEBP(WebPMemoryWrite, int (*)(const uint8_t*, size_t, const WebPPicture*))
+ FUNCTION_LOADER_LIBWEBP(WebPMemoryWriterClear, void (*)(WebPMemoryWriter *))
}
++lib.loaded;
@@ -465,6 +489,173 @@ IMG_Animation *IMG_LoadWEBPAnimation_IO(SDL_IOStream *src)
return IMG_LoadWEBPAnimation_IO_Internal(src, 0);
}
+
+static const char *GetWebPEncodingErrorStringInternal(WebPEncodingError error_code)
+{
+ switch (error_code) {
+ case VP8_ENC_OK:
+ return "OK";
+ case VP8_ENC_ERROR_OUT_OF_MEMORY:
+ return "Out of memory";
+ case VP8_ENC_ERROR_BITSTREAM_OUT_OF_MEMORY:
+ return "Bitstream out of memory";
+ case VP8_ENC_ERROR_NULL_PARAMETER:
+ return "Null parameter";
+ case VP8_ENC_ERROR_INVALID_CONFIGURATION:
+ return "Invalid configuration";
+ case VP8_ENC_ERROR_BAD_DIMENSION:
+ return "Bad dimension";
+ case VP8_ENC_ERROR_PARTITION0_OVERFLOW:
+ return "Partition 0 overflow";
+ case VP8_ENC_ERROR_PARTITION_OVERFLOW:
+ return "Partition overflow";
+ case VP8_ENC_ERROR_BAD_WRITE:
+ return "Bad write";
+ case VP8_ENC_ERROR_FILE_TOO_BIG:
+ return "File too big";
+ case VP8_ENC_ERROR_USER_ABORT:
+ return "User abort";
+ default:
+ return "Unknown WebP encoding error";
+ }
+}
+
+bool IMG_SaveWEBP_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio, float quality)
+{
+ WebPConfig config;
+ WebPPicture pic;
+ WebPMemoryWriter writer;
+ SDL_Surface *converted_surface = NULL;
+ bool ret = true;
+ const char *error = NULL;
+ bool pic_initialized = false;
+ bool memorywriter_initialized = false;
+ bool converted_surface_locked = false;
+
+ if (!surface || !dst) {
+ error = "Invalid input surface or destination stream.";
+ goto cleanup;
+ }
+
+ if (!IMG_InitWEBP()) {
+ error = SDL_GetError();
+ goto cleanup;
+ }
+
+ if (!lib.WebPConfigInitInternal(&config, WEBP_PRESET_DEFAULT, quality, WEBP_ENCODER_ABI_VERSION)) {
+ error = "Failed to initialize WebPConfig.";
+ goto cleanup;
+ }
+
+ quality = SDL_clamp(quality, 0.0f, 100.0f);
+
+ config.lossless = quality == 100.0f;
+ config.quality = quality;
+
+ // TODO: Take a look if the method 4 fits here for us.
+ config.method = 4;
+
+ if (!lib.WebPValidateConfig(&config)) {
+ error = "Invalid WebP configuration.";
+ goto cleanup;
+ }
+
+ if (!lib.WebPPictureInitInternal(&pic, WEBP_ENCODER_ABI_VERSION)) {
+ error = "Failed to initialize WebPPicture.";
+ goto cleanup;
+ }
+ pic_initialized = true;
+
+ pic.width = surface->w;
+ pic.height = surface->h;
+
+ if (surface->format != SDL_PIXELFORMAT_RGBA32) {
+ converted_surface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
+ if (!converted_surface) {
+ error = SDL_GetError();
+ goto cleanup;
+ }
+ } else {
+ converted_surface = surface;
+ }
+
+ if (SDL_MUSTLOCK(converted_surface)) {
+ if (!SDL_LockSurface(converted_surface)) {
+ error = SDL_GetError();
+ goto cleanup;
+ }
+ converted_surface_locked = true;
+ }
+
+ if (!lib.WebPPictureImportRGBA(&pic, (const uint8_t *)converted_surface->pixels, converted_surface->pitch)) {
+ error = "Failed to import RGBA pixels into WebPPicture.";
+ goto cleanup;
+ }
+
+ if (converted_surface_locked) {
+ SDL_UnlockSurface(converted_surface);
+ converted_surface_locked = false;
+ }
+
+ lib.WebPMemoryWriterInit(&writer);
+ memorywriter_initialized = true;
+ pic.writer = (WebPWriterFunction)lib.WebPMemoryWrite;
+ pic.custom_ptr = &writer;
+
+ if (!lib.WebPEncode(&config, &pic)) {
+ error = GetWebPEncodingErrorStringInternal(pic.error_code);
+ goto cleanup;
+ }
+
+ if (writer.size > 0) {
+ if (SDL_WriteIO(dst, writer.mem, writer.size) != writer.size) {
+ error = "Failed to write all WebP data to destination.";
+ goto cleanup;
+ }
+ } else {
+ error = "No WebP data generated.";
+ goto cleanup;
+ }
+
+cleanup:
+ if (converted_surface_locked) {
+ SDL_UnlockSurface(converted_surface);
+ }
+
+ if (converted_surface && converted_surface != surface) {
+ SDL_DestroySurface(converted_surface);
+ }
+
+ if (pic_initialized) {
+ lib.WebPPictureFree(&pic);
+ }
+
+ if (memorywriter_initialized) {
+ lib.WebPMemoryWriterClear(&writer);
+ }
+
+ if (error) {
+ SDL_SetError("%s", error);
+ ret = false;
+ }
+
+ if (closeio) {
+ SDL_CloseIO(dst);
+ }
+
+ return ret;
+}
+
+bool IMG_SaveWEBP(SDL_Surface *surface, const char *file, float quality)
+{
+ SDL_IOStream *dst = SDL_IOFromFile(file, "wb");
+ if (!dst) {
+ return false;
+ }
+
+ return IMG_SaveWEBP_IO(surface, dst, true, quality);
+}
+
#else
/* See if an image is contained in a data source */
@@ -487,4 +678,21 @@ IMG_Animation *IMG_LoadWEBPAnimation_IO(SDL_IOStream *src)
return NULL;
}
+bool IMG_SaveWEBP_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio, float quality)
+{
+ (void)surface;
+ (void)dst;
+ (void)closeio;
+ (void)quality;
+ return SDL_SetError("SDL_image was not built with WEBP save support");
+}
+
+bool IMG_SaveWEBP(SDL_Surface *surface, const char *file, float quality)
+{
+ (void)surface;
+ (void)file;
+ (void)quality;
+ return SDL_SetError("SDL_image was not built with WEBP save support");
+}
+
#endif /* LOAD_WEBP */