C++ – SDL2 resize a surface

csdl-2

We want to create an SDL surface by loading an image with SDL_Image and if the dimensions exceed a limit resize the surface.

The reason we need to do this is on Raspbian SDL throws an error creating a texture from the surface ('Texture dimensions are limited to 2048×2048'). Whilst that's a very large image we don't want users to be concerned about image size, we want to resize it for them. Although we haven't encountered this limit on Windows, we're trying to develop the solution on windows and having issues resizing the texture.

Looking for a solution there have been similar questions…:

Is a custom blitter or SDL_gfx necessary with current SDL2 (those answers pre-date SDL2's 2013 release)? SDLRenderCopyEx doesn't help as you need to generate the texture which is where our problem occurs.

So we tried some of the available blitting functions like SDL_BlitScaled, below is a simple program to render a 2500×2500 PNG with no scaling:

#include <SDL.h>
#include <SDL_image.h>
#include <sstream>
#include <string>

SDL_Texture * get_texture(
    SDL_Renderer * pRenderer,
    std::string image_filename) {
    SDL_Texture * result = NULL;

    SDL_Surface * pSurface = IMG_Load(image_filename.c_str());

    if (pSurface == NULL) {
        printf("Error image load: %s\n", IMG_GetError());
    }
    else {
        SDL_Texture * pTexture = SDL_CreateTextureFromSurface(pRenderer, pSurface);

        if (pTexture == NULL) {
            printf("Error image load: %s\n", SDL_GetError());
        }
        else {
            SDL_SetTextureBlendMode(
                pTexture,
                SDL_BLENDMODE_BLEND);

            result = pTexture;
        }

        SDL_FreeSurface(pSurface);
        pSurface = NULL;
    }

    return result;
}

int main(int argc, char* args[]) {
    SDL_Window * pWindow = NULL;
    SDL_Renderer * pRenderer = NULL;

    // set up
    SDL_Init(SDL_INIT_VIDEO);

    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");

    SDL_Rect screenDimensions;

    screenDimensions.x = 0;
    screenDimensions.y = 0;

    screenDimensions.w = 640;
    screenDimensions.h = 480;

    pWindow = SDL_CreateWindow("Resize Test",
        SDL_WINDOWPOS_UNDEFINED,
        SDL_WINDOWPOS_UNDEFINED,
        screenDimensions.w, 
        screenDimensions.h,
        SDL_WINDOW_SHOWN);

    pRenderer = SDL_CreateRenderer(pWindow,
        -1,
        SDL_RENDERER_ACCELERATED);

    IMG_Init(IMG_INIT_PNG);

    // render
    SDL_SetRenderDrawColor(
        pRenderer,
        0,
        0,
        0,
        0);

    SDL_RenderClear(pRenderer);

    SDL_Texture * pTexture = get_texture(
        pRenderer,
        "2500x2500.png");

    if (pTexture != NULL) {
        SDL_RenderCopy(
            pRenderer,
            pTexture,
            NULL,
            &screenDimensions);

        SDL_DestroyTexture(pTexture);
        pTexture = NULL;
    }

    SDL_RenderPresent(pRenderer);

    // wait for quit
    bool quit = false;

    while (!quit)
    {
        // poll input for quit
        SDL_Event inputEvent;

        while (SDL_PollEvent(&inputEvent) != 0) {
            if ((inputEvent.type == SDL_KEYDOWN) &&
                (inputEvent.key.keysym.sym == 113)) {
                quit = true;
            }
        }
    }

    IMG_Quit();

    SDL_DestroyRenderer(pRenderer);

    pRenderer = NULL;

    SDL_DestroyWindow(pWindow);

    pWindow = NULL;

    return 0;
}

Changing the get_texture function so it identifies a limit and tries to create a new surface:

SDL_Texture * get_texture(
    SDL_Renderer * pRenderer,
    std::string image_filename) {
    SDL_Texture * result = NULL;

    SDL_Surface * pSurface = IMG_Load(image_filename.c_str());

    if (pSurface == NULL) {
        printf("Error image load: %s\n", IMG_GetError());
    }
    else {
        const int limit = 1024;
        int width = pSurface->w;
        int height = pSurface->h;

        if ((width > limit) ||
            (height > limit)) {
            SDL_Rect sourceDimensions;
            sourceDimensions.x = 0;
            sourceDimensions.y = 0;
            sourceDimensions.w = width;
            sourceDimensions.h = height;

            float scale = (float)limit / (float)width;
            float scaleH = (float)limit / (float)height;

            if (scaleH < scale) {
                scale = scaleH;
            }

            SDL_Rect targetDimensions;
            targetDimensions.x = 0;
            targetDimensions.y = 0;
            targetDimensions.w = (int)(width * scale);
            targetDimensions.h = (int)(height * scale);

            SDL_Surface *pScaleSurface = SDL_CreateRGBSurface(
                pSurface->flags,
                targetDimensions.w,
                targetDimensions.h,
                pSurface->format->BitsPerPixel,
                pSurface->format->Rmask,
                pSurface->format->Gmask,
                pSurface->format->Bmask,
                pSurface->format->Amask);

            if (SDL_BlitScaled(pSurface, NULL, pScaleSurface, &targetDimensions) < 0) {
                printf("Error did not scale surface: %s\n", SDL_GetError());

                SDL_FreeSurface(pScaleSurface);
                pScaleSurface = NULL;
            }
            else {
                SDL_FreeSurface(pSurface);

                pSurface = pScaleSurface;
                width = pSurface->w;
                height = pSurface->h;
            }
        }

        SDL_Texture * pTexture = SDL_CreateTextureFromSurface(pRenderer, pSurface);

        if (pTexture == NULL) {
            printf("Error image load: %s\n", SDL_GetError());
        }
        else {
            SDL_SetTextureBlendMode(
                pTexture,
                SDL_BLENDMODE_BLEND);

            result = pTexture;
        }

        SDL_FreeSurface(pSurface);
        pSurface = NULL;
    }

    return result;
}

SDL_BlitScaled fails with an error 'Blit combination not supported' other variations have a similar error:

SDL_BlitScaled(pSurface, NULL, pScaleSurface, NULL)
SDL_BlitScaled(pSurface, &sourceDimensions, pScaleSurface, &targetDimensions)
SDL_LowerBlitScaled(pSurface, &sourceDimensions, pScaleSurface, &targetDimensions) // from the wiki this is the call SDL_BlitScaled makes internally

Then we tried a non-scaled blit… which didn't throw an error but just shows white (not the clear colour or a colour in the image).

SDL_BlitSurface(pSurface, &targetDimensions, pScaleSurface, &targetDimensions)

With that blitting function not working we then tried it with the same image as a bitmap (just exporting the .png as .bmp), still loading the file with SDL_Image and both those functions work with SDL_BlitScaled scaling as expected 😐

Not sure what's going wrong here (we expect and need support for major image file formats like .png) or if this is the recommended approach, any help appreciated!

Best Answer

TL;DR The comment from @kelter pointed me in the right direction and another stack overflow question gave me a solution: it works if you first Blit to a 32bpp surface and then BlitScaled to another 32bpp surface. That worked for 8 and 24 bit depth pngs, 32 bit were invisible again another stack overflow question suggested first filling the surface before blitting.

An updated get_texture function:

SDL_Texture * get_texture(
    SDL_Renderer * pRenderer,
    std::string image_filename) {
    SDL_Texture * result = NULL;

    SDL_Surface * pSurface = IMG_Load(image_filename.c_str());

    if (pSurface == NULL) {
        printf("Error image load: %s\n", IMG_GetError());
    }
    else {
        const int limit = 1024;
        int width = pSurface->w;
        int height = pSurface->h;

        if ((width > limit) ||
            (height > limit)) {
            SDL_Rect sourceDimensions;
            sourceDimensions.x = 0;
            sourceDimensions.y = 0;
            sourceDimensions.w = width;
            sourceDimensions.h = height;

            float scale = (float)limit / (float)width;
            float scaleH = (float)limit / (float)height;

            if (scaleH < scale) {
                scale = scaleH;
            }

            SDL_Rect targetDimensions;
            targetDimensions.x = 0;
            targetDimensions.y = 0;
            targetDimensions.w = (int)(width * scale);
            targetDimensions.h = (int)(height * scale);

            // create a 32 bits per pixel surface to Blit the image to first, before BlitScaled
            // https://stackoverflow.com/questions/33850453/sdl2-blit-scaled-from-a-palettized-8bpp-surface-gives-error-blit-combination/33944312
            SDL_Surface *p32BPPSurface = SDL_CreateRGBSurface(
                pSurface->flags,
                sourceDimensions.w,
                sourceDimensions.h,
                32,
                pSurface->format->Rmask,
                pSurface->format->Gmask,
                pSurface->format->Bmask,
                pSurface->format->Amask);

            if (SDL_BlitSurface(pSurface, NULL, p32BPPSurface, NULL) < 0) {
                printf("Error did not blit surface: %s\n", SDL_GetError());
            }
            else {
                // create another 32 bits per pixel surface are the desired scale
                SDL_Surface *pScaleSurface = SDL_CreateRGBSurface(
                    p32BPPSurface->flags,
                    targetDimensions.w,
                    targetDimensions.h,
                    p32BPPSurface->format->BitsPerPixel,
                    p32BPPSurface->format->Rmask,
                    p32BPPSurface->format->Gmask,
                    p32BPPSurface->format->Bmask,
                    p32BPPSurface->format->Amask);

                // 32 bit per pixel surfaces (loaded from the original file) won't scale down with BlitScaled, suggestion to first fill the surface
                // 8 and 24 bit depth pngs did not require this
                // https://stackoverflow.com/questions/20587999/sdl-blitscaled-doesnt-work
                SDL_FillRect(pScaleSurface, &targetDimensions, SDL_MapRGBA(pScaleSurface->format, 255, 0, 0, 255));

                if (SDL_BlitScaled(p32BPPSurface, NULL, pScaleSurface, NULL) < 0) {
                    printf("Error did not scale surface: %s\n", SDL_GetError());

                    SDL_FreeSurface(pScaleSurface);
                    pScaleSurface = NULL;
                }
                else {
                    SDL_FreeSurface(pSurface);

                    pSurface = pScaleSurface;
                    width = pSurface->w;
                    height = pSurface->h;
                }
            }

            SDL_FreeSurface(p32BPPSurface);
            p32BPPSurface = NULL;
        }

        SDL_Texture * pTexture = SDL_CreateTextureFromSurface(pRenderer, pSurface);

        if (pTexture == NULL) {
            printf("Error image load: %s\n", SDL_GetError());
        }
        else {
            SDL_SetTextureBlendMode(
                pTexture,
                SDL_BLENDMODE_BLEND);

            result = pTexture;
        }

        SDL_FreeSurface(pSurface);
        pSurface = NULL;
    }

    return result;
}

The comment from @kelter had me look more closely at the surface pixel formats, bitmaps were working at 24bpp, pngs were being loaded at 8bpp and not working. Tried changing the target surface to 24 or 32 bpp but that didn't help. We had generated the png with auto-detected bit depth, setting it to 8 or 24 and performing the BlitScaled on a surface with the same bits-per-pixel worked although it didn't work for 32. Googling the blit conversion error lead to the question and answer from @Petruza.

Update Was a bit quick writing up this answer, the original solution handled bmp and 8 and 24 bit pngs but 32 bit pngs weren't rendering. @Retired Ninja answer to another question about Blit_Scaled suggested filling the surface before calling the function and that sorts it, there's another question related to setting alpha on new surfaces that may be relevant to this (particularily if you needed transparency) but filling with a solid colour is enough for me... for now.

Related Topic