In my TexturWrapper class I need a method to save the main-texture or a part of it to a PNG-file.
IMG_SavePNG is just able to save a SDL_Surface to a file, not a SDL_Texture.
The main-texture of my class can be SDL_TEXTUREACCESS_STREAMING or SDL_TEXTUREACCESS_TARGET. Because of the Target possibility I can not access the Texture pixels directly with the SDL_LockTexture. But we need the Pixel-Data to create a Surface out of a Texture.
...in theory it works my way, BUT I can not get the correct colors. For example one of my testfiles have in the original the backgroundcolor cyan, which means in SDL_Color: {0, 255, 255, 255} but the png-file background is yellow. The SDL_Color yellow would be {255, 255, 0 , 255}. So I swapped the mask for the red and blue channels first. Interesting is that it still was a yellow background after that change. I tried other combinations with no effect. Some returned for sure the SDL_Error "unknown pixelformat".
I returned the mask to normal and wrote a function to change the Pixels of the surface manually:
void rearrangePixels(Uint8* pixelData, int width, int height, Uint32 rmask, Uint32 gmask, Uint32 bmask, Uint32 amask)
These are all the possible combinations of R, G, B and A:
RGBA
RGAB
RBGA
RBAG
RAGB
RABG
GRBA
GRAB
GBRA
GBAR
GARB
GABR
BRGA
BRAG
BGRA
BGAR
BARG
BAGR
ARGB
ARBG
AGRB
AGBR
ABRG
ABGR
...I got just 2 different png-file products. 1. Yellow background or 2. 100% transparent and so invisible picture.
I tried to solve this problem with Google and heavy use of phind.com for a month now.
I have tried to find a solution in older posts and answers in stackoverflow too and I have merged some code from other users, like user1902824 in my code (even I don't have a "SDL_CreateRGBSurfaceWithFormatFrom" in my version of SDL2)...
But my problem still persists.
I am working with C4droid, directly on Android in C++17 and with SDL2 + SDL_Image. My only Hardware is a 3-4 years old Tablet-PC from Huawaii with 3GB Ram and Android-8. That's why C4droid can not be in the Newest version and so my SDL2.
EDIT:
I really tried everything i found or could think about.
And maybe some other I don't remember just now.
I'm not sure, but I believe that SDL_RenderReadPixels returns different data, depending on the system and/or hardware!?
By the way, here is an original image I used: Original colors When I load the file to a surface and save it directly with IMG_SavePNG it is identical!
This is the one I get, when I used SDL_RenderReadPixels first as in my code: Colors after the SDL_RenderReadPixels function
Finally (and most surprisingly for me) is this the picture I get, when I use my rearrangePixels method with no matter what combination of channels: Colors after my rearrangePixels method
Here is a reduced and corrected version of my Code (you can copy and paste the whole code step by step in just one cpp file to compile it with c4droid (you will need to change a path to an image)):
First, the initialization of the variables, SDL2, SDL_Image and a few convenience functions:
#include <SDL2/SDL.h>
#include <SDL_image.h>
#include <string>
// Global variables
static SDL_Window *myWindow;
static SDL_Renderer *myRenderer;
// call a message box
void MessageBox( std::string header , std::string message )
{
SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_INFORMATION , header.c_str() , message.c_str() , NULL );
}
// clean up
void sdlExit( void )
{
SDL_DestroyWindow( myWindow );
IMG_Quit();
SDL_DestroyRenderer( myRenderer );
SDL_Quit();
return;
}
// returns the aktual display mode in a sdl_rect
SDL_Rect GetDisplayMode()
{
SDL_DisplayMode displayM;
SDL_GetCurrentDisplayMode(0, &displayM);
// get a percent of the display hight
int percentY = displayM.h / 100;
// trick the android bars out for fullscreen immersive-mode
int adjustedY = - (percentY * 3); // Set y-coordinate 3% below the upperbound
int adjustedHeight = displayM.h + (percentY * 6); // hight + 6%
SDL_Rect displayMode = {0, adjustedY, displayM.w, adjustedHeight};
return displayMode;
}
// Initialize SDL2, SDL_Image and open a window
bool sdlInit( const char* title)
{
// Initialize SDL2
if ( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER ) != 0 ) {
char txt[ 64 ];
sprintf( txt , "Could not initialisieren SDL2: %s.\n" , SDL_GetError() );
MessageBox( "In Function sdlInit:" , txt );
return false;
}
atexit( SDL_Quit );
// Device display mode
SDL_Rect displayMode = GetDisplayMode();
// the window:
myWindow = SDL_CreateWindow(
title, // title "..."
SDL_WINDOWPOS_UNDEFINED, // initialize x position
SDL_WINDOWPOS_UNDEFINED, // initialize y position
displayMode.w, // width, in pixel
displayMode.h, // hight, in pixel
SDL_WINDOW_FULLSCREEN_DESKTOP ); // windowmode or 0
// Start Immersive Full-Screen-Mode
SDL_SetHint(SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH, "1");
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "1");
SDL_SetWindowFullscreen(myWindow, SDL_WINDOW_FULLSCREEN_DESKTOP);
// Check window
if ( myWindow == NULL ) {
char txt[ 64 ];
sprintf( txt , "Could not create window: %s\n" , SDL_GetError() );
MessageBox( "In Function sdlInit:" , txt );
return false;
}
// Initialize Renderer,
myRenderer = SDL_CreateRenderer( myWindow , -1 , SDL_RENDERER_ACCELERATED );
SDL_SetHint( SDL_HINT_RENDER_SCALE_QUALITY , "linear" );
SDL_RenderSetLogicalSize( myRenderer , displayMode.w , displayMode.h );
// Check Renderer
if ( myRenderer == NULL ) {
char txt[ 64 ];
sprintf( txt , "Could not initialize renderer: %s\n" , SDL_GetError() );
MessageBox( "In Function sdlInit:" , txt );
return false;
}
// Set blending
SDL_SetRenderDrawBlendMode( myRenderer , SDL_BLENDMODE_BLEND );
// Initialize SDL_Img--------------------------------F1.2
int imgFlags = IMG_INIT_JPG | IMG_INIT_PNG | IMG_INIT_TIF;
int initted = IMG_Init( imgFlags );
if (( initted & imgFlags ) != imgFlags ) {
char txt[ 64 ];
sprintf( txt , "IMG_Init: Failed to init required support: %s\n", IMG_GetError() );
MessageBox( "In Function sdlInit:" , txt );
return false;
}
return true;
}
Now the TexturWrapper class body:
class TexturWrapper
{
public:
// Init Variables
TexturWrapper();
// Destructor
~TexturWrapper();
// Load ...with colorKey?
bool Laden( std::string pfad , bool CKAktiv = 0 , unsigned char CKr = 0 , unsigned char CKg = 0 , unsigned char CKb = 0 );
// Blank-Texture
bool Blank( int BREITE , int HOEHE , char ta ); // (w, h, 's' = streaming and 't' = target)
// Size of Image
int getBreite(); //width
int getHoehe(); //highth
// Set as Rendertarget
void Renderziel();
// Reset Rendertarget
void RenderzielReset();
// Render Texture (texture-clip) to specific place in specific angle in specific size
void Rendern( int x , int y , SDL_Rect* clip = NULL , double angle = 0.0 , SDL_Point* center = NULL , SDL_RendererFlip flip = SDL_FLIP_NONE , SDL_Rect* rQuad = NULL , bool centerScale = true);
// Pixel-Manipulation
SDL_Surface* flip_surface_vertical(SDL_Surface* sfc);
// Put ImageTextur (ImageTextur - clip) to a file
void SaveImage(const char *filename, SDL_Rect* clip = NULL, SDL_Rect* rQuad = NULL);
//For use if the Rendersize is adjustet (adjustRectToPreserveAspectRatio)
int getPositionX();
int getPositionY();
int getRenderW();
int getRenderH();
// destroy just texture
void free();
// Object clear
void leeren();
private:
// tried to get a correction of the result from SDL_RenderReadPixels()
void rearrangePixels(Uint8* pixelData, int width, int height, Uint32 rmask, Uint32 gmask, Uint32 bmask, Uint32 amask) {
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
// pointer to one pixel
Uint8* pixel = pixelData + y * width * 4 + x * 4;
// extract pixeldata
Uint32 pixelValue = *(Uint32*)pixel;
// pixelValue = pixelValue << 8; // Ignore first byte if this would be the problem
Uint8 r = (pixelValue & rmask) >> 24;
Uint8 g = (pixelValue & gmask) >> 16;
Uint8 b = (pixelValue & bmask) >> 8;
Uint8 a = pixelValue & amask;
// invert colors ? tried the channels even separatet or in combination of 2 out
// r = 255 - r;
// g = 255 - g;
// b = 255 - b;
// new order of the color-channels
*pixel = (a << 24) | (r << 16) | (g << 8) | b;
}
}
}
void adjustRectToPreserveAspectRatio(SDL_Rect* srcRect, SDL_Rect* dstRect) {
float srcAspectRatio = (float)srcRect->w / srcRect->h;
float dstAspectRatio = (float)dstRect->w / dstRect->h;
if (srcAspectRatio > dstAspectRatio) {
// if texture.w > destination.w, change hight
dstRect->h = dstRect->w / srcAspectRatio;
} else if (srcAspectRatio < dstAspectRatio) {
// if texture.h > destination.h, change width
dstRect->w = dstRect->h * srcAspectRatio;
}
}
// Main-Texture
SDL_Texture* ImageTextur;
// Renderdestination
SDL_Rect renderQuad = { 0 , 0 , 0 , 0 };
// The pixeldata if Lock is possible
void* Pixels;
int Pitch;
// Image dimensions
int ImageBreite;
int ImageHoehe;
// ColorKey
bool CKAktiv;
unsigned char CKr;
unsigned char CKg;
unsigned char CKb;
};
Here the implementation:
TexturWrapper::TexturWrapper()
{
// Initialise Variables
ImageTextur = NULL;
ImageBreite = 0;
ImageHoehe = 0;
Pixels = NULL;
Pitch = 0;
}
TexturWrapper::~TexturWrapper()
{
leeren();
}
bool TexturWrapper::Laden( std::string pfad , bool CKAktiv , unsigned char CKr , unsigned char CKg , unsigned char CKb )
{
free();
// New Texture
SDL_Texture* NeueTextur = NULL;
// Load Image from path
SDL_Surface* FileSurface = IMG_Load( pfad.c_str() );
if( FileSurface == NULL )
{
char txt[ 64 ];
sprintf( txt , "Could not load image: %s! SDL_image Error: %s\n" , pfad.c_str() , IMG_GetError() );
MessageBox( "In class TexturWrapper:" , txt );
exit( EXIT_FAILURE );
}
else
{
SDL_Surface* formattedSurface = SDL_ConvertSurfaceFormat( FileSurface , SDL_PIXELFORMAT_RGBA8888 , 0 );
NeueTextur = SDL_CreateTexture( myRenderer , SDL_PIXELFORMAT_RGBA8888 , SDL_TEXTUREACCESS_STREAMING , formattedSurface->w , formattedSurface->h );
ImageBreite = formattedSurface->w;
ImageHoehe = formattedSurface->h;
SDL_SetTextureBlendMode( NeueTextur , SDL_BLENDMODE_BLEND );
SDL_LockTexture( NeueTextur , &formattedSurface->clip_rect , &Pixels , &Pitch );
memcpy( Pixels , formattedSurface->pixels , formattedSurface->pitch * formattedSurface->h );
// Pixels-Data into editable Format
Uint32* pixels = (Uint32*)Pixels;
int pixelCount = ( Pitch / 4 ) * ImageHoehe;
// ColorKey Image
if ( CKAktiv == 1 ) {
// Map Colors
Uint32 colorKey = SDL_MapRGB( formattedSurface->format , CKr , CKg , CKb );
Uint32 transparent = SDL_MapRGBA( formattedSurface->format , 0x00 , 0xFF , 0xFF , 0x00 );
// ColorKey pixels
for( int i = 0 ; i < pixelCount ; ++i ) {
if( pixels[ i ] == colorKey ) {
pixels[ i ] = transparent;
}
}
}
SDL_UnlockTexture( NeueTextur );
Pixels = NULL;
// clean up
SDL_FreeSurface( formattedSurface );
SDL_FreeSurface( FileSurface );
}
ImageTextur = NeueTextur;
NeueTextur = NULL;
SDL_DestroyTexture(NeueTextur);
CKAktiv = 0;
CKr = 0;
CKg = 0;
CKb = 0;
return ImageTextur != NULL;
}
void TexturWrapper::free()
{
if( ImageTextur != NULL )
{
SDL_DestroyTexture( ImageTextur );
ImageTextur = NULL;
ImageBreite = 0;
ImageHoehe = 0;
Pixels = NULL;
Pitch = 0;
}
}
void TexturWrapper::leeren()
{
SDL_DestroyTexture( ImageTextur );
ImageTextur = NULL;
ImageBreite = 0;
ImageHoehe = 0;
Pixels = NULL;
Pitch = 0;
CKAktiv = 0;
CKr = 0;
CKg = 0;
CKb = 0;
}
void TexturWrapper::Rendern( int x , int y , SDL_Rect* clip , double angle , SDL_Point* center , SDL_RendererFlip flip , SDL_Rect* rQuad , bool centerScale )
{
if (rQuad != NULL)
{
//the picture
SDL_Rect* TextureQuad = new SDL_Rect;
TextureQuad->x = 0;
TextureQuad->y = 0;
TextureQuad->w = ImageBreite;
TextureQuad->h = ImageHoehe;
// Render area
renderQuad.x = rQuad->x;
renderQuad.y = rQuad->y;
renderQuad.w = rQuad->w;
renderQuad.h = rQuad->h;
// scale, by keeping proportions
adjustRectToPreserveAspectRatio(TextureQuad, &renderQuad);
if (centerScale) {
// center picture in the middle on to renderpoint
renderQuad.x -= renderQuad.w / 2;
}
delete TextureQuad;
}
else {
// Set render position and size on screen
renderQuad = { x , y , ImageBreite , ImageHoehe };
}
// Set Clip_Rendering Dimensions and fit target-rect to it if clip exists
if (clip != NULL)
{
renderQuad.w = clip->w;
renderQuad.h = clip->h;
adjustRectToPreserveAspectRatio(clip, &renderQuad);
}
SDL_RenderCopyEx( myRenderer , ImageTextur , clip , &renderQuad , angle , center , flip );
}
int TexturWrapper::getBreite()
{
return ImageBreite;
}
int TexturWrapper::getHoehe()
{
return ImageHoehe;
}
int TexturWrapper::getPositionX()
{
return renderQuad.x;
}
int TexturWrapper::getPositionY()
{
return renderQuad.y;
}
int TexturWrapper::getRenderW()
{
return renderQuad.w;
}
int TexturWrapper::getRenderH()
{
return renderQuad.h;
}
void TexturWrapper::Renderziel()
{
SDL_SetRenderTarget( myRenderer , ImageTextur );
}
void TexturWrapper::RenderzielReset()
{
SDL_SetRenderTarget( myRenderer , NULL );
}
bool TexturWrapper::Blank( int b , int h , char ta )
{
// Blank Texture for Streaming....
if ( ta == 's' ) {
ImageTextur = SDL_CreateTexture( myRenderer , SDL_PIXELFORMAT_RGBA8888 , SDL_TEXTUREACCESS_STREAMING , b , h );
}
else if ( ta == 't' ) { // ...or Target access
ImageTextur = SDL_CreateTexture( myRenderer , SDL_PIXELFORMAT_RGBA8888 , SDL_TEXTUREACCESS_TARGET , b , h );
}
if( ImageTextur == NULL )
{
char txt[ 64 ];
sprintf( txt , "Could not create blank texture! SDL Error: %s\n" , SDL_GetError() );
MessageBox( "In class TexturWrapper:" , txt );
exit( EXIT_FAILURE );
}
else
{
ImageBreite = b;
ImageHoehe = h;
}
Renderziel();
SDL_SetRenderDrawColor(myRenderer, 0x00, 0x00, 0x00, 0x00);
SDL_RenderClear(myRenderer);
RenderzielReset();
SDL_SetTextureBlendMode( ImageTextur , SDL_BLENDMODE_BLEND );
return ImageTextur != NULL;
}
SDL_Surface* TexturWrapper::flip_surface_vertical(SDL_Surface* sfc)
{
SDL_Surface* result = SDL_CreateRGBSurface(sfc->flags, sfc->w, sfc->h,
sfc->format->BytesPerPixel * 8, sfc->format->Rmask, sfc->format->Gmask,
sfc->format->Bmask, sfc->format->Amask);
const auto pitch = sfc->pitch;
const auto pxlength = pitch*(sfc->h - 1);
auto pixels = static_cast<unsigned char*>(sfc->pixels) + pxlength;
auto rpixels = static_cast<unsigned char*>(result->pixels) ;
for(auto line = 0; line < sfc->h; ++line) { // copy sfc to result line by line, upside-down
memcpy(rpixels,pixels,pitch);
pixels -= pitch;
rpixels += pitch;
}
return result;
}
This is the method with my problem in it:
// This is the method I try to get to work correctly!
void TexturWrapper::SaveImage(const char* file_name, SDL_Rect* clipRect, SDL_Rect* rQuadRect)
{
// test clipRect and rQuadRect for NULL
SDL_Rect clip = {0, 0, ImageBreite, ImageHoehe};
SDL_Rect rQuad = {0, 0, ImageBreite, ImageHoehe};
if (clipRect) {
clip = *clipRect;
}
if (rQuadRect) {
rQuad = *rQuadRect;
}
SDL_Texture* rQuadTexture = SDL_CreateTexture(myRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, rQuad.w, rQuad.h); // final-size texture
SDL_SetRenderTarget(myRenderer, rQuadTexture); // make it target
SDL_RenderCopy(myRenderer, ImageTextur, &clip, NULL); // render clip to it
// surface in rQuad size
SDL_Surface* surface = SDL_CreateRGBSurface(0, rQuad.w, rQuad.h, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);
// get the pixels of rQuadTexture
SDL_RenderReadPixels(myRenderer, NULL, surface->format->format, surface->pixels, surface->pitch);
SDL_PixelFormat* format = surface->format;
/* // To gether information about the SDL_RenderReadPixels output-format
char txt[ 256 ];
sprintf( txt , "\nBits per pixel: %d\n Bytes per pixel: %d\n Rmask: %08X\n Gmask: %08X\n Bmask: %08X\n Amask: %08X\n" , format->BitsPerPixel, format->BytesPerPixel, format->Rmask, format->Gmask, format->Bmask, format->Amask);
MessageBox( "Das Format:" , txt );
*/
// lock if needed
if (SDL_MUSTLOCK(surface)) {
SDL_LockSurface(surface);
}
// manipulate pixeldata
rearrangePixels((Uint8*)surface->pixels, surface->w, surface->h, format->Rmask, format->Gmask, format->Bmask, format->Amask);
// Unlock if needet
if (SDL_MUSTLOCK(surface)) {
SDL_UnlockSurface(surface);
}
SDL_Surface* flipSurf = flip_surface_vertical( surface ); // because RenderReadPixels returns upsidedown data
IMG_SavePNG(flipSurf, file_name);
SDL_FreeSurface(surface);
SDL_FreeSurface(flipSurf);
SDL_SetRenderTarget(myRenderer, NULL);
}
And finally the main() to test everything:
int main( void )
{
std::string picturefile = "resources/M.png"; // "path to an imagefile.png"
if ( !sdlInit( "Testing TexturWrapper class" ) ) { // Initialize SDL2 and SDL_Image
exit( EXIT_FAILURE );
}
// TexturWrapper class objects
TexturWrapper tw1;
TexturWrapper tw2;
TexturWrapper tw3;
tw1.Laden( picturefile ); // Load the imagefile without color-keying
SDL_Rect screen = GetDisplayMode();
tw1.Rendern( screen.x , screen.y , NULL , 0.0 , NULL , SDL_FLIP_NONE , &screen, false ); // Render it (scaled) to the middle of the screen... // false = renderpoint is not centered (0 = 0 and not -(half of the texture.w))
SDL_RenderPresent( myRenderer ); // Show rendered stuff on the screen
SDL_Delay( 3000 );
tw2.Blank( tw1.getRenderW() , tw1.getRenderH() , 't' ); // 't' = SDL_TEXTUREACCESS_TARGET
tw2.Renderziel(); // This object is now the rendertarget
tw1.Rendern( screen.x , screen.y , NULL , 0.0 , NULL , SDL_FLIP_NONE , &screen , false ); // 'copy' tw1 to tw2
tw2.RenderzielReset(); // Rendertarget = myWindow
tw2.Rendern( screen.x , screen.y , NULL , 0.0 , NULL , SDL_FLIP_NONE , &screen , false ); // show the copy
SDL_RenderPresent( myRenderer ); // Show rendered stuff on the screen
SDL_Delay( 3000 );
SDL_Rect clip = {0, 0, 500, 500}; // if you want to save just a clip of the copy
SDL_Rect rquad = {0, 0, tw2.getBreite()/4, tw2.getHoehe()/4}; // size of the picture.png
tw2.SaveImage( "Testfile.png" );//, &clip, &rquad ); // Save the copy as image
tw3.Laden( "Testfile.png" ); // Load the saved image
tw3.Rendern( screen.x , screen.y , NULL , 0.0 , NULL , SDL_FLIP_NONE , &screen, false ); // Show then loaded
SDL_RenderPresent( myRenderer ); // Show rendered stuff on the screen
SDL_Delay( 3000 );
// clean up
tw1.leeren();
tw2.leeren();
tw3.leeren();
sdlExit();
exit( EXIT_SUCCESS );
}
Well, i dont know why my rearrangePixels method did not worked. nvm!
For my excuse, it was even my own first thought that the r channel and the b channel are swapped and needed just to be swapped again.
But this is exactly what i tried to do with the rearrangePixels method.
The solution is to just swap the channels on a new surface.
Here is the full code of my solution:
void TexturWrapper::SaveImage(const char* file_name, SDL_Rect* clipRect, SDL_Rect* rQuadRect) {
// test clipRect and rQuadRect for NULL
SDL_Rect clip = {0, 0, ImageBreite, ImageHoehe};
SDL_Rect rQuad = {0, 0, ImageBreite, ImageHoehe};
if (clipRect) {
clip = *clipRect;
}
if (rQuadRect) {
rQuad = *rQuadRect;
}
SDL_Texture* rQuadTexture = SDL_CreateTexture(myRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, rQuad.w, rQuad.h); // final-size texture
SDL_SetRenderTarget(myRenderer, rQuadTexture); // make it target
SDL_RenderCopy(myRenderer, ImageTextur, &clip, NULL); // render clip to it
// surface in rQuad size
SDL_Surface* surface = SDL_CreateRGBSurface(0, rQuad.w, rQuad.h, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);
// get the pixels of rQuadTexture
SDL_RenderReadPixels(myRenderer, NULL, surface->format->format, surface->pixels, surface->pitch);
SDL_PixelFormat* format = surface->format;
// lock if needet
if (SDL_MUSTLOCK(surface)) {
SDL_LockSurface(surface);
}
// THIS IS THE SOLUTION!
SDL_Surface* swapped_surface = SDL_CreateRGBSurfaceFrom(
(void*)surface->pixels,
surface->w,
surface->h,
surface->format->BytesPerPixel * 8,
surface->pitch,
format->Bmask, // Rmask = Bmask from the original surface
format->Gmask, // Gmask = Gmask
format->Rmask, // Bmask = Rmask from the original surface
format->Amask // Amask = Amask
);
// Unlock if needet
if (SDL_MUSTLOCK(surface)) {
SDL_UnlockSurface(surface);
}
SDL_Surface* flipSurf = flip_surface_vertical( swapped_surface ); // because RenderReadPixels returns upsidedown data
IMG_SavePNG(flipSurf, file_name);
SDL_FreeSurface(surface);
SDL_FreeSurface(swapped_surface);
SDL_FreeSurface(flipSurf);
SDL_SetRenderTarget(myRenderer, NULL);
}
I am sorry for wasteing your time.