I have recently started using SDL2 in a C++ project, which is mainly used as a training project so I can learn using SDL2.
One of the things I tried, was to write a simple label class. The Idea is that the label is created with given position and dimensions at start. Then when I call my 'setText()' function, the text would be rendered and shown on window. At first I used this approach:
// get a surface from given text
m_surf = TTF_RenderText_Solid(m_font, txt.c_str(), fgCol);
// get a texture from the previous surface
m_texture = SDL_CreateTextureFromSurface(r, m_surf);
// render it in 'r' which is the window renderer
SDL_RenderCopy(r, m_texture, NULL, &rect);
This works for me. So I thought to remove the overhead of creating textures every time text changes, then destroy them, and use SDL_UpdateTexture().
Well, this never worked! It always produces a corrupt output on screen, even though the call is successful:
// Once at ctor create a texture
m_texture = SDL_CreateTexture(r, SDL_PIXELFORMAT_RGBA32,SDL_TEXTUREACCESS_STATIC, rect.w, rect.h);
// ...
// later update when setText() is called get a surface from given text and update texture
m_surf = TTF_RenderText_Solid(m_font, txt.c_str(), fgCol);
rc = SDL_UpdateTexture(m_texture, NULL, m_surf->pixels, m_surf->pitch);
My first thought was that it's something wrong with the pixel formatting... so I also used a format convert before updating:
m_surf = SDL_ConvertSurfaceFormat(ts, SDL_PIXELFORMAT_RGBA32, 0);
where 'ts' is a temp surface where the text was rendered, and constant SDL_PIXELFORMAT_RGBA32 is same as the one I used to create the m_texture.
here is what I get on window when using 'SDL_UpdateTexture()'
Any Ideas ?
Thank you,
Alex
Code Snip:
I have written a compact piece of code that is related to reproduce problem
// label class declaration
class label
{
friend class panel;
public:
label(SDL_Window* const w, TTF_Font* const fnt, SDL_Rect rect);
~label();
int draw(SDL_Rect& rect);
int draw(SDL_Rect& rect,const std::string& txt, SDL_Color fgCol, int quality);
private:
std::string m_txt;
TTF_Font* const m_font = nullptr;
SDL_Surface *m_surf = nullptr;
SDL_Texture *m_texture = nullptr;
SDL_Rect m_rect{0,0,0,0};
SDL_Renderer *m_ren;
SDL_Window *m_wnd;
Uint32 m_pixForm;
};
// label class constructor
label::label(SDL_Window* const w, TTF_Font* const fnt, SDL_Rect rect):m_wnd(w),m_font(fnt),m_surf (nullptr), m_rect(rect)
{
// save window info locally for label object
m_ren =SDL_GetRenderer(m_wnd);
m_pixForm =SDL_GetWindowPixelFormat(m_wnd);
m_texture = SDL_CreateTexture(m_ren, m_pixForm,SDL_TEXTUREACCESS_STATIC, rect.w, rect.h);
m_surf = nullptr;
std::cout << "PixelFormat " << m_pixForm << " : " << SDL_GetPixelFormatName(m_pixForm) << std::endl;
}
// label class methods to render/draw text on screen
int label::setText(const std::string& txt, SDL_Color fgCol, int quality=0)
{
int w,h;
SDL_Surface *ts;
if(m_surf!=nullptr)SDL_FreeSurface(m_surf);
switch(quality)
{
case 1:
{
ts = TTF_RenderText_Shaded(m_font, txt.c_str(), fgCol, SDL_Color{0,0,0,0});
break;
}
case 2:
{
ts = TTF_RenderText_Blended(m_font,txt.c_str(), fgCol);
break;
}
default:
{
ts = TTF_RenderText_Solid(m_font, txt.c_str(), fgCol);
break;
}
}
std::cout << "Before pixFormat ts " << SDL_GetPixelFormatName(ts->format->format)<< std::endl;
m_surf = SDL_ConvertSurfaceFormat(ts, m_pixForm, 0);
std::cout << "After pixFormat surf : " << SDL_GetPixelFormatName(m_surf->format->format) << " ts " << SDL_GetPixelFormatName(ts->format->format)<< std::endl;
SDL_FreeSurface(ts);
TTF_SizeText(m_font, txt.c_str(), &w, &h);
std::cout << "Set text '" << txt << "' w: " << w << " h: " << h << std::endl;
}
int label::draw(SDL_Rect& rect)
{
// HERE is the one that draw the noise on screen!
{
int rc;
rc = SDL_UpdateTexture(m_texture, NULL, m_surf->pixels, m_surf->pitch);
if(rc)
{
std::cout << "Error updating texture " << rc <<std::endl;
return(rc);
}
}
// if it is replaced with the following it works:
/*
{
if(m_texture!=nullptr)SDL_DestroyTexture(m_texture);
m_texture = SDL_CreateTextureFromSurface(r, m_surf);
}
*/
SDL_RenderCopy(m_ren, m_texture, NULL, &rect);
}
int label::draw(SDL_Rect& rect,const std::string& txt, SDL_Color fgCol, int q)
{
setText(txt,fgCol,q);
draw(rect);
}
// main functiom. Init SDL, create window, create label and draw it:
int main( int argc, char * argv[] )
{
SDL_Window *wnd ;
SDL_Renderer *wrend;
int i;
wnd = SDL_CreateWindow("TestWin",SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,500,300,0);
wrend = SDL_CreateRenderer(wnd,-1,SDL_RENDERER_ACCELERATED);
SDL_SetRenderDrawBlendMode(wrend,SDL_BLENDMODE_ADD);
SDL_SetRenderDrawColor(wrend,0,0,0,255);
SDL_RenderClear(wrend);
SDL_Color fcol{255,255,0,128};
SDL_Rect lblBox;
lblBox.h = 120;
lblBox.w = 280;
lblBox.x = 1;
lblBox.y = 1;
label lbl(wnd, getCurrentFont(), lblBox); // create a label (getCurrentFont returns a valid TTF_Font pointer)
lbl.draw(lblBox,std::string("Text Test"), fcol, 2);
SDL_RenderPresent(wrend);
SDL_ShowWindow(wnd);
// .. more code here
}
I tested your code on MacOS Catalina and last version of SDL2 and SDL2_ttf and had segmentation fault.
After debugging I found that in label::setText() you use a quality parameter and if you pass quality=2 (to use the function TTF_RenderText_Blended()), you will end up with a segmentation fault or garbage pixels.
But functions TTF_RenderText_Shaded() and TTF_RenderText_Solid() work well, I think this is directly linked to the fact that with TTF_RenderText_Blended() you have transparency in your surface.
Edit:
After digging a bit more I solved the segmentation fault I had previously by replacing the NULL value for the rect in line :
rc = SDL_UpdateTexture(m_texture, NULL, m_surf->pixels, m_surf->pitch);
by the size of m_surf:
SDL_Rect SurfRect;
SurfRect.x = 0;
SurfRect.y = 0;
SurfRect.w = m_surf->w;
SurfRect.h = m_surf->h;
...
rc = SDL_UpdateTexture(m_texture, SurfRect, m_surf->pixels, m_surf->pitch);
Edit 2 :
I directly look into the source file of the SDL2 to understand what was the difference between my tests and the code done internally by the function SDL_CreateTexture() and you have to add these lines :
int rc = SDL_UpdateTexture(m_texture, &SurfRect, m_surf->pixels, m_surf->pitch);
{
Uint8 r, g, b, a;
SDL_BlendMode blendMode;
SDL_GetSurfaceColorMod(m_surf, &r, &g, &b);
SDL_SetTextureColorMod(m_texture, r, g, b);
SDL_GetSurfaceAlphaMod(m_surf, &a);
SDL_SetTextureAlphaMod(m_texture, a);
if (SDL_HasColorKey(m_surf)) {
/* We converted to a texture with alpha format */
SDL_SetTextureBlendMode(m_texture, SDL_BLENDMODE_BLEND);
} else {
SDL_GetSurfaceBlendMode(m_surf, &blendMode);
SDL_SetTextureBlendMode(m_texture, blendMode);
}
}
Find here my raw debug file (this isn't pretty)
Related
I'm trying to make a simple game engine in C++ and I keep getting a weird error that says "This was 0x35":
I'm using visual studio and SDL2 (although I use it more like SDL). When the error occurs, the program has started to execute and is "Still running" meaning I have to stop it manually. The exception shows up in this function:
void Tab::Draw(int x, int y, SDL_Surface* display, bool setpos = true)
{
if (setpos) {
this->rect->x = x; //This is the line that the exception points to.
this->rect->y = y;
}
SDL_BlitScaled(surf, NULL, display, rect);
}
The variable 'rect' is an SDL_Rect
The variable 'surf' is an SDL_Surface
both variables get initialized here:
Tab::Tab(SDL_Surface* Image, SDL_Rect* Rect) {
surf = Image;
if (Rect != NULL) {
rect = Rect;
}
}
and this is the only code that calls the function Tab::Draw:
void Bar::Draw(SDL_Surface* display)
{
SDL_BlitScaled(image, NULL, display, &rect);
for (int i = 0; i < 20; i++) {
tabs[i]->Draw(0, 0, display, true);
}
}
The bar class is meant to be a line with a bunch of tabs(which I should and will rename to Icon at some point)
Where Bar::Draw gets called:
#include "AppClass.h"
void CApp::OnRender() {
//Draw stuff
SDL_FillRect(window, NULL, SDL_MapRGB(window->format, r, g, b));
main.Draw(window);
ToBlit->Rect.x = mouseX - 32;
ToBlit->Rect.y = mouseY - 32;
ToBlit->Rect.w = 64;
ToBlit->Rect.h = 64;
if (SDL_GetMouseFocus() == display) {
SDL_BlitScaled(ToBlit->Surface, NULL, window, &ToBlit->Rect);
}
//Update
SDL_UpdateWindowSurface(display);
}
I'm working on an image drawing program and I'm getting really confused about colorspaces. The more I read about gamma, the less I know (this wasn't much help).
Internally, the drawing program will store images as 8-bit sRGB and then convert to 16-bit linear for compositing and filtering operations. Perhaps I should store 16-bit linear and then convert to 8-bit sRGB when exporting? I'd love some advice on this. Currently, I'm not really sure what colorspace the RGB pixels are being interpreted as by Qt!
As I understand it, sRGB is close to the colorspace that most monitors use so converting from sRGB to the monitor colorspace doesn't change the image data much. Looking at the sRGB data as if it were in the right colorspace will probably be pretty close.
Goals
The drawing program displays sRGB images. I want to save these sRGB images to a PNG file. When I open this PNG file (with Preview on a Mac) on the same computer that the image was created on, it should look exactly the same as what the artist sees in the drawing program and have the same color values (checked with Digital Color Meter). Different monitors and different operating systems use different colorspaces. These colorspaces might not "fit" the colorspace of the system used to create the image. When I open the PNG on a different monitor or even a different computer, the image should look as similar as possible to the original but probably have different color values.
Experiments
The drawing program seems to be displaying the images correctly (I think). The problem is the PNG. I'm using Qt and saving the image with QImage::save. I'm willing to use libPNG if I need more control.
For testing, I'm drawing a 5x5 image with the color values 0 63 127 191 255 for red and green.
Drawing program screenshots
When I sample the image rendered by the drawing program with Digital Color Meter, the pixel values are unchanged. The pixel at 3,3 sampled with DCM is 191 191 0 as it should be. There is a clear contrast between each of the pixels.
When I take a screenshot, the pixel values in the screenshot file are different. The pixel at 3,3 sampled with DCM when viewing in Preview is 192 191 0. The pixel at 3,3 stored in the file is 140 126 4. I should note that the screenshot file has an sRGB chunk with a rendering intent perceptual.
When I crop the screenshot to a 5x5 image using Preview, the sRGB chunk is replaced with the gAMA and cHRM chunks that correspond to sRGB (I used pngcheck).
gAMA
0.45455
cHRM
White x = 0.3127 y = 0.329, Red x = 0.64 y = 0.33
Green x = 0.3 y = 0.6, Blue x = 0.15 y = 0.06
Both versions have the same pixel values stored in the file. They also have the same pixel values when sampled with DCM when viewing in Preview.
Below is the cropped screenshot (it's really tiny!).
The best way of saving a drawing seems to be taking a screenshot but even that's not perfect.
Test programs
Qt program
#include <QtGui/qimage.h>
int main() {
QImage image{5, 5, QImage::Format_RGB32};
const int values[5] = {0, 63, 127, 191, 255};
for (int r = 0; r != 5; ++r) {
for (int g = 0; g != 5; ++g) {
image.setPixel(g, r, qRgb(values[r], values[g], 0));
}
}
image.save("qt.png");
}
This program produces the same output as the libpng program except that Qt adds a pHYs chunk. The output looks similar to the desired output but there is less contrast between the pixels and the pixel values are significantly off.
Libpng program
#include <cmath>
#include <iostream>
#include <libpng16/png.h>
png_byte srgb_lut[256];
void initLut(const double exponent) {
for (int i = 0; i != 256; ++i) {
srgb_lut[i] = std::round(std::pow(i / 255.0, exponent) * 255.0);
}
}
int main() {
std::FILE *file = std::fopen("libpng.png", "wb");
if (!file) {
std::cerr << "Failed to open file\n";
return 1;
}
png_structp pngPtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!pngPtr) {
std::fclose(file);
std::cout << "Failed to initialize png write struct\n";
return 1;
}
png_infop infoPtr = png_create_info_struct(pngPtr);
if (!infoPtr) {
png_destroy_write_struct(&pngPtr, nullptr);
std::fclose(file);
std::cout << "Failed to initialize png info struct\n";
return 1;
}
if (setjmp(png_jmpbuf(pngPtr))) {
png_destroy_write_struct(&pngPtr, &infoPtr);
std::fclose(file);
std::cout << "Failed to set jmp buf\n";
return 1;
}
png_init_io(pngPtr, file);
png_set_IHDR(
pngPtr,
infoPtr,
5,
5,
8,
PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT
);
//png_set_gAMA_fixed(pngPtr, infoPtr, 100000);
//png_set_sRGB_gAMA_and_cHRM(pngPtr, infoPtr, PNG_sRGB_INTENT_PERCEPTUAL);
//png_set_sRGB(pngPtr, infoPtr, PNG_sRGB_INTENT_PERCEPTUAL);
//initLut(2.2);
//initLut(1.0/2.2);
initLut(1.0);
png_bytep rows[5];
png_color imageData[5][5];
const png_byte values[5] = {0, 63, 127, 191, 255};
for (int r = 0; r != 5; ++r) {
for (int g = 0; g != 5; ++g) {
imageData[r][g] = {srgb_lut[values[r]], srgb_lut[values[g]], 0};
}
rows[r] = reinterpret_cast<png_bytep>(&imageData[r][0]);
}
png_set_rows(pngPtr, infoPtr, rows);
png_write_png(pngPtr, infoPtr, PNG_TRANSFORM_IDENTITY, nullptr);
png_destroy_write_struct(&pngPtr, &infoPtr);
std::fclose(file);
}
As I said the previous section, the output is similar to the desired output but there is reduced contrast. The pixel at 3,3 sampled with DCM when viewing in Preview is 186 198 0 which is way off.
I'm really glad that Qt is producing the same output as libpng even though the output isn't what I want it to be. It means that I could switch to libpng if I needed to.
Sample program
This program samples a pixel from a PNG. I'm pretty sure it doesn't do any color space conversion and just gives me the value stored in the file.
#include <iostream>
#include <libpng16/png.h>
int main(int argc, char **argv) {
if (argc != 4) {
std::cout << "sample <file> <x> <y>\n";
return 1;
}
const int x = std::atoi(argv[2]);
const int y = std::atoi(argv[3]);
if (x < 0 || y < 0) {
std::cerr << "Coordinates out of range\n";
return 1;
}
std::FILE *file = std::fopen(argv[1], "rb");
if (!file) {
std::cerr << "Failed to open file\n";
return 1;
}
png_structp pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!pngPtr) {
std::fclose(file);
std::cerr << "Failed to initialize read struct\n";
return 1;
}
png_infop infoPtr = png_create_info_struct(pngPtr);
if (!infoPtr) {
png_destroy_read_struct(&pngPtr, nullptr, nullptr);
std::fclose(file);
std::cerr << "Failed to initialize info struct\n";
return 1;
}
if (setjmp(png_jmpbuf(pngPtr))) {
// Pssh, who needs exceptions anyway?
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
std::fclose(file);
std::cerr << "Failed to set jmp buf\n";
return 1;
}
png_init_io(pngPtr, file);
// Does this prevent libpng from changing the color values?
png_set_gamma(pngPtr, PNG_GAMMA_LINEAR, PNG_GAMMA_LINEAR);
png_read_png(pngPtr, infoPtr, PNG_TRANSFORM_STRIP_ALPHA, nullptr);
png_bytepp rows = png_get_rows(pngPtr, infoPtr);
const int width = png_get_image_width(pngPtr, infoPtr);
const int height = png_get_image_height(pngPtr, infoPtr);
if (x >= width || y >= height) {
// Pssh, who needs RAII anyway?
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
std::fclose(file);
std::cerr << "Coordinates out of range\n";
return 1;
}
png_bytep row = rows[y];
for (int c = 0; c != 3; ++c) {
std::cout << static_cast<int>(row[x * 3 + c]) << ' ';
}
std::cout << '\n';
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
std::fclose(file);
}
What I'm really trying to do
I need to modify the libpng test program so that the pixel values are 0 63 127 191 255 when opened with Preview and sampled with Digital Color Meter. Sounds like a simple task but it most definitely isn't. There's some commented code in the libpng test program of things I've tried. None of them produces the desired output. What's really frustrating is that Chrome and Preview produce different results. I have no idea which is correct or closest to correct or what "correct" even means.
The more I read about this, the more I think I should just settle for "oh well, it's noticeably wrong but I guess it's good enough *sigh*".
Viewing Experiments
I wrote two identical programs for viewing PNGs. They both produce the desired output (sampling with DCM returns 0 63 127 191 255).
Qt Viewer
#include <iostream>
#include <QtWidgets/qlabel.h>
#include <QtWidgets/qmainwindow.h>
#include <QtWidgets/qapplication.h>
int main(int argc, char **argv) {
if (argc != 2) {
std::cerr << "qt_render <file>\n";
return EXIT_FAILURE;
}
QImage image{argv[1]};
if (image.isNull()) {
std::cerr << "Failed to load image\n";
return EXIT_FAILURE;
}
image = image.scaled(image.size() * 64);
QApplication app{argc, argv};
QMainWindow window;
window.setWindowTitle(argv[1]);
window.setFixedSize(image.size());
QLabel label{&window};
QPixmap pixmap;
if (!pixmap.convertFromImage(image)) {
std::cerr << "Failed to convert surface to texture\n";
return EXIT_FAILURE;
}
label.setPixmap(pixmap);
label.setFixedSize(image.size());
window.show();
return app.exec();
}
SDL2 Libpng Viewer
#include <iostream>
#include <SDL2/SDL.h>
#include <libpng16/png.h>
template <typename... Args>
[[noreturn]] void fatalError(Args &&... args) {
(std::cerr << ... << args) << '\n';
throw std::exception{};
}
void checkErr(const int errorCode) {
if (errorCode != 0) {
fatalError("Error: ", SDL_GetError());
}
}
template <typename T>
T *checkNull(T *ptr) {
if (ptr == nullptr) {
fatalError("Error: ", SDL_GetError());
} else {
return ptr;
}
}
struct FileCloser {
void operator()(std::FILE *file) const noexcept {
std::fclose(file);
}
};
using File = std::unique_ptr<std::FILE, FileCloser>;
File openFile(const char *path, const char *mode) {
std::FILE *file = std::fopen(path, mode);
if (!file) {
fatalError("Failed to open file");
} else {
return File{file};
}
}
struct WindowDestroyer {
void operator()(SDL_Window *window) const noexcept {
SDL_DestroyWindow(window);
}
};
using Window = std::unique_ptr<SDL_Window, WindowDestroyer>;
struct SurfaceDestroyer {
void operator()(SDL_Surface *surface) const noexcept {
SDL_FreeSurface(surface);
}
};
using Surface = std::unique_ptr<SDL_Surface, SurfaceDestroyer>;
struct TextureDestroyer {
void operator()(SDL_Texture *texture) const noexcept {
SDL_DestroyTexture(texture);
}
};
using Texture = std::unique_ptr<SDL_Texture, TextureDestroyer>;
struct RendererDestroyer {
void operator()(SDL_Renderer *renderer) const noexcept {
SDL_DestroyRenderer(renderer);
}
};
using Renderer = std::unique_ptr<SDL_Renderer, RendererDestroyer>;
class SurfaceLock {
public:
explicit SurfaceLock(SDL_Surface *surface)
: surface{surface} {
SDL_LockSurface(surface);
}
~SurfaceLock() {
SDL_UnlockSurface(surface);
}
private:
SDL_Surface *surface;
};
Surface createSurface(png_structp pngPtr, png_infop infoPtr) {
const png_bytepp rows = png_get_rows(pngPtr, infoPtr);
const int width = png_get_image_width(pngPtr, infoPtr);
const int height = png_get_image_height(pngPtr, infoPtr);
Surface surface = Surface{checkNull(SDL_CreateRGBSurfaceWithFormat(
0, width, height, 24, SDL_PIXELFORMAT_RGB24
))};
{
SurfaceLock lock{surface.get()};
for (int y = 0; y != height; ++y) {
uint8_t *dst = static_cast<uint8_t *>(surface->pixels);
dst += y * surface->pitch;
std::memcpy(dst, rows[y], width * 3);
}
}
return surface;
}
void doMain(int argc, char **argv) {
if (argc != 2) {
fatalError("sdl_render <file>");
}
File file = openFile(argv[1], "rb");
png_structp pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!pngPtr) {
fatalError("Failed to initialize read struct\n");
}
png_infop infoPtr = png_create_info_struct(pngPtr);
if (!infoPtr) {
png_destroy_read_struct(&pngPtr, nullptr, nullptr);
fatalError("Failed to initialize info struct\n");
}
if (setjmp(png_jmpbuf(pngPtr))) {
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
fatalError("Failed to set jmp buf");
}
png_init_io(pngPtr, file.get());
png_read_png(pngPtr, infoPtr, PNG_TRANSFORM_STRIP_ALPHA, nullptr);
Surface surface = createSurface(pngPtr, infoPtr);
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
checkErr(SDL_Init(SDL_INIT_VIDEO));
std::atexit(SDL_Quit);
Window window = Window{checkNull(SDL_CreateWindow(
argv[1], SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, surface->w * 64, surface->h * 64, 0
))};
Renderer renderer = Renderer{checkNull(SDL_CreateRenderer(
window.get(), -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
))};
Texture texture = Texture{checkNull(SDL_CreateTextureFromSurface(
renderer.get(), surface.get()
))};
surface.reset();
while (true) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
return;
}
}
SDL_RenderCopy(renderer.get(), texture.get(), nullptr, nullptr);
SDL_RenderPresent(renderer.get());
}
}
int main(int argc, char **argv) {
try {
doMain(argc, argv);
} catch (...) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
I'm tempted to write an SDL2, OpenGL, Libpng viewer just to be sure but OpenGL is kind of a hassle. My application is aimed at making sprites and textures for games so if it works with the SDL2 render API and OpenGL then I guess everything is fine. I haven't done any experiments with external monitors yet. Putting sRGB, gAMA and cHRM chunks in the PNG doesn't have any effect on the output of either viewer. I'm not sure if this is good or bad. On the surface, it looks like my problem has just disappeared. I'd still like someone to explain my observations.
ColorSync Utility
I've discovered a new tool and now I think I know what's going on...
This can be accomplished with ImageMagick.
FYI different photo editing programs manage PNG metadata differently, so be aware of this. For example, Mac's Preview app automatically attaches a sRGB IEC61966-2.1 color profile to edited PNG files that do not have an existing color profile. This behavior not only changes the colors, but also impacts the file size.
To make image file comparisons, I use the following ImageMagick command (for each file):
magick identify -verbose insert/image/filepath/here.png
Also, if you want to strip all PNG metadata, use the following ImageMagick command:
convert -strip insert/original/image/filepath/here.png insert/NEW/image/filepath/here2.png
I'm having trouble displaying an image (PNG extracted with libpng) into an XCB window, it is always entirely empty/white. I'm pretty sure the PNG extraction is correct since I can perfectly re-write it into another file.
I've tried everything I found (explanations, guides, documentation) and I'm running out of ideas:
Creating an xcb_pixmap_t calling xcb_create_pixmap_from_bitmap_data() with the data taken from the PNG, then calling xcb_copy_area() into the EXPOSE part of the event loop.
Creating an xcb_image_t* calling xcb_image_create_from_bitmap_data() then trying to map it to the window with xcb_image_put(). I've even tried to display each pixel with xcb_image_put_pixel(), but without success.
Code sample:
xcb_pixmap_t pixmap = xcb_create_pixmap_from_bitmap_data(
connection, // xcb_connect(0, 0) (type: xcb_connection_t*)
window, // xcb_generate_id(connection) (type: xcb_window_t)
img.getData(), // uint8_t*
img.getWidth(), // 128
img.getHeight(), // 128
img.getBitDepth(), // 8
screen->black_pixel, // screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data (type: xcb_screen_t*)
screen->white_pixel,
nullptr);
// "img" is an instance of my own custom class, result of PNG reading
xcb_image_t* image = xcb_image_create_from_bitmap_data(
img.getData(),
img.getWidth(),
img.getHeight()); // image->data seems fine
xcb_image_put(connection,
window,
graphicsContext,
image, 0, 0, 0); // This does nothing
for (unsigned int i = 0; i < screen->height_in_pixels; ++i)
for (unsigned int j = 0; j < screen->width_in_pixels; ++j)
xcb_image_put_pixel(image, j, i, 0); // Displays nothing
[...]
// Into event loop
case XCB_EXPOSE: {
xcb_expose_event_t* exposeEvent = reinterpret_cast<xcb_expose_event_t*>(event);
xcb_copy_area(connection,
pixmap,
window,
graphicsContext,
exposeEvent->x, exposeEvent->y, // Top left x & y coordinates of the source's region to copy
exposeEvent->x, exposeEvent->y, // Top left x & y coordinates of the destination's region to copy to
exposeEvent->width,
exposeEvent->height);
xcb_flush(connection);
break;
}
From the examples I found I saw that it didn't need a colormap, but could that be the case? Could anyone tell me where I've gone wrong?
I threw together a simple xcb image viewer about 4 years ago, but just noticed this question, so apologies for the necromancy.
It uses xcb_image, stb_image and nanosvg, but compiles to a relatively small static binary (with a musl or uclibc toolchain)
#include <xcb/xcb.h>
#include <xcb/xcb_image.h>
#define STBI_NO_HDR
#define STBI_NO_LINEAR
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define NANOSVG_IMPLEMENTATION
#include "nanosvg.h"
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvgrast.h"
int main(int argc, char **argv){
xcb_connection_t *c = xcb_connect(0, 0);
xcb_screen_t *s = xcb_setup_roots_iterator(xcb_get_setup(c)).data;
int w, h, n,
depth = s->root_depth,
win_class = XCB_WINDOW_CLASS_INPUT_OUTPUT,
format = XCB_IMAGE_FORMAT_Z_PIXMAP;
xcb_colormap_t colormap = s->default_colormap;
xcb_drawable_t win = xcb_generate_id(c);
xcb_gcontext_t gc = xcb_generate_id(c);
xcb_pixmap_t pixmap = xcb_generate_id(c);
xcb_generic_event_t *ev;
xcb_image_t *image;
NSVGimage *shapes = NULL;
NSVGrasterizer *rast = NULL;
char *data = NULL;
unsigned *dp;
size_t i, len;
uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK,
value_mask = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS,
values[] = { s->black_pixel, value_mask };
if (argc<2) return -1;
if ((data = stbi_load(argv[1], &w, &h, &n, 4)))
;
else if ((shapes = nsvgParseFromFile(argv[1], "px", 96.0f))) {
w = (int)shapes->width;
h = (int)shapes->height;
rast = nsvgCreateRasterizer();
data = malloc(w*h*4);
nsvgRasterize(rast, shapes, 0,0,1, data, w, h, w*4);
}else return -1;
for(i=0,len=w*h,dp=(unsigned *)data;i<len;i++) //rgba to bgra
dp[i]=dp[i]&0xff00ff00|((dp[i]>>16)&0xFF)|((dp[i]<<16)&0xFF0000);
xcb_create_window(c,depth,win,s->root,0,0,w,h,1,win_class,s->root_visual,mask,values);
xcb_create_pixmap(c,depth,pixmap,win,w,h);
xcb_create_gc(c,gc,pixmap,0,NULL);
image = xcb_image_create_native(c,w,h,format,depth,data,w*h*4,data);
xcb_image_put(c, pixmap, gc, image, 0, 0, 0);
xcb_image_destroy(image);
xcb_map_window(c, win);
xcb_flush(c);
while ((ev = xcb_wait_for_event(c))) {
switch (ev->response_type & ~0x80){
case XCB_EXPOSE: {
xcb_expose_event_t *x = (xcb_expose_event_t *)ev;
xcb_copy_area(c,pixmap,win,gc,x->x,x->y,x->x,x->y,x->width,x->height);
xcb_flush(c);
}break;
case XCB_BUTTON_PRESS: goto end;
default: break;
}
}
end:
xcb_free_pixmap(c, pixmap);
xcb_disconnect(c);
return 0;
}
I was following Lazy Foo' tutorial to create text using ttf font, and everything was fine, but I needed to create several text lines in several different places with different font size and color, so I decided to use vector. Here is my code of TextTexture (mostly copy of Lazy Foo tutorial):
#ifndef TEXT_TEXTURE_HPP
#define TEXT_TEXTURE_HPP
#include "graphics.hpp"
#include "vector2.hpp"
#include <SDL2/SDL_ttf.h>
#include <string>
class TextTexture {
public:
TextTexture(
Graphics& graphics,
TTF_Font* font,
std::string textureText,
SDL_Color textColor,
Vector2 coordinates
);
~TextTexture();
void draw( Graphics& graphics );
private:
SDL_Texture* mTexture;
int mWidth;
int mHeight;
int mX;
int mY;
};
#endif // TEXT_TEXTURE_HPP
And .cpp file for it:
#include "text_texture.hpp"
#include "vector2.hpp"
#include <iostream>
#include <unistd.h>
TextTexture::TextTexture (
Graphics& graphics,
TTF_Font* font,
std::string textureText,
SDL_Color textColor,
Vector2 coordinates
) :
mTexture(NULL),
mWidth(0),
mHeight(0),
mX(0),
mY(0)
{
//Render temp surface
SDL_Surface* tempSurface = TTF_RenderUTF8_Blended (font, textureText.c_str(), textColor);
if ( tempSurface == NULL ) {
std::cout << "Unable to render text surface! SDL_ttf Error: " << TTF_GetError() << std::endl;
} else {
this -> mTexture = SDL_CreateTextureFromSurface(graphics.getRenderer(), tempSurface);
if ( this -> mTexture == NULL ) {
std::cout << "Unable to create texture from rendered text! SDL Error: " << SDL_GetError() << std::endl;
} else {
//Get image dimensions
mWidth = tempSurface -> w;
mHeight = tempSurface -> h;
// Get coordinates
this -> mX = coordinates.getX();
this -> mY = coordinates.getY();
}
SDL_FreeSurface (tempSurface);
tempSurface = NULL;
}
}
TextTexture::~TextTexture() {
//Free texture if it exists
if ( mTexture != NULL ) {
SDL_DestroyTexture( mTexture );
}
mTexture = NULL;
mWidth = 0;
mHeight = 0;
}
// FIXME somewhy affects previous dest rects
void TextTexture::draw (Graphics& graphics) {
//Set rendering space and render to screen
SDL_Rect destinationRectangle = { mX, mY, this -> mWidth, this -> mHeight };
//Render to screen
graphics.blitSurface( mTexture, NULL, &destinationRectangle );
}
I created simple Text Manager to handle vector of texts:
#ifndef TEXT_MANAGER_HPP
#define TEXT_MANAGER_HPP
#include "graphics.hpp"
#include "text_texture.hpp"
#include "vector2.hpp"
#include <string>
#include <vector>
enum fontSize {
SMALL = 16,
NORMAL = 32,
BIG = 48,
TITLE = 72
};
enum fontColor {
WHITE,
ORANGE,
BLACK
};
class TextManager {
public:
TextManager(Graphics& graphics);
~TextManager();
void addText(std::string, fontSize, fontColor, Vector2);
void draw();
void clearText();
private:
Graphics& graphics;
std::vector <TextTexture> gText;
};
#endif // TEXT_MANAGER_HPP
and .cpp file:
#include "text_manager.hpp"
#include <iostream>
TextManager::TextManager(Graphics& graphics) :
graphics(graphics)
{}
TextManager::~TextManager() {}
void TextManager::addText(std::string text, fontSize size, fontColor color, Vector2 coordinates) {
TTF_Font* tempFont = TTF_OpenFont( "resources/fonts/pixel.ttf", fontSize::TITLE );
SDL_Color tempColor = { 255, 255, 255 };
// Switch removed for shorter code
this -> gText.emplace_back(graphics, tempFont, text, tempColor, coordinates);
TTF_CloseFont(tempFont);
tempFont = NULL;
}
// FIXME
void TextManager::draw() {
std::vector<TextTexture>::iterator it;
for(it = gText.begin(); it != gText.end(); ++it) {
it -> draw(graphics);
}
}
void TextManager::clearText() {
gText.clear();
}
But when I start the application, I see something like this:
Second string is printed, but font and bonding rectangle of first line is saved, hovewer
Later I added input handler that added second line of text after pressing a button, and when there is only one line of text, everything fine, but when you add second, something weird is beginning - sometimes first text disappears, sometimes 'both' of them is shoved. As I understand, second surface of text somehow affects first one, by copying it texture on the place of the first's destination.
Here is my graphics.blitSurface, if it will help:
void Graphics::blitSurface(SDL_Texture* texture, SDL_Rect* sourceRectangle, SDL_Rect* destinationRectangle)
{
SDL_RenderCopy ( this -> _renderer, texture, sourceRectangle, destinationRectangle );
}
Where is my mistake? Sorry for bad english, I hope you will get my problem.
I figured it out somehow randomly. The thing is that when I adding object to vector, it's calls a destructor.
Here is why:
Why does my class's destructor get called when I add instances to a vector?
So I've been practicing/making a quick game for the past 6 hours, then something stumped me.
The game had an integer, Score, which would be added up with one every time an ammo hits an alien.
int Score;
stringstream sstr;
sstr << Score;
string str1 = sstr.str();
TTF_Font* Sans = NULL;
Sans = TTF_OpenFont("Sans.ttf", 24);
SDL_Color White = {255, 255, 255};
SDL_Surface* surfaceMessage = NULL;
surfaceMessage = TTF_RenderText_Solid(Sans, str1.c_str(), White);
SDL_Texture* Message = NULL;
Message = SDL_CreateTextureFromSurface(renderer, surfaceMessage);
SDL_Rect Message_rect;
Message_rect.x = 0;
Message_rect.y = 0;
Message_rect.w = 100;
Message_rect.h = 100;
//UPDATE/GAMELOOP AREA, I DIDN'T REALLY PASTE THE WHOLE PART
SDL_RenderCopy(renderer, Message, NULL, &Message_rect);
Now I've been trying different roundabouts as to how to update the texture, Message.
I made a cout check to check if I did hit an alien and what my current score is, it appears perfectly fine, but the rendered texture, Message won't move from 0.
I created a texture from the surface (the message) because I mostly prefer textures and I don't have any surface since in my current knowledge, you'd at least need a filled surface where you could blitz this
And another question, I'm planning to make a dialogue heavy game, is there another way of doing the texts? I've got a strong feeling that I'm doing it wrong.
Minimal runnable example
The counter gets updated every second.
Ubuntu 16.10, SDL 2.0.4:
sudo apt-get install libsdl2-dev libsdl2-ttf-dev
./main /path/to/my.ttf
This method is easy to integrate, but not very efficient as it re-rasters and re-creates textures all the time. If you also want efficiency, see: Rendering fonts and text with SDL2 efficiently I get 4k FPS, so it might be fine for simple applications.
GitHub upstream with a ttf file to test with: https://github.com/cirosantilli/cpp-cheat/blob/d36527fe4977bb9ef4b885b1ec92bd0cd3444a98/sdl/ttf.c:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#define COMMON_COLOR_MAX 255
#define COMMON_WINDOW_WIDTH 500
#define COMMON_WINDOW_HEIGHT (COMMON_WINDOW_WIDTH)
double common_get_secs(void) {
struct timespec ts;
timespec_get(&ts, TIME_UTC);
return ts.tv_sec + (1e-9 * ts.tv_nsec);
}
const double COMMON_FPS_GRANULARITY_S = 0.5;
double common_fps_last_time_s;
unsigned int common_fps_nframes;
void common_fps_init() {
common_fps_nframes = 0;
common_fps_last_time_s = common_get_secs();
}
void common_fps_update_and_print() {
double dt, current_time_s;
current_time_s = common_get_secs();
common_fps_nframes++;
dt = current_time_s - common_fps_last_time_s;
if (dt > COMMON_FPS_GRANULARITY_S) {
printf("FPS = %f\n", common_fps_nframes / dt);
common_fps_last_time_s = current_time_s;
common_fps_nframes = 0;
}
}
#define MAX_STRING_LEN 4
/*
- x, y: upper left corner of string
- rect output Width and height contain rendered dimensions.
*/
void render_text(
SDL_Renderer *renderer,
int x,
int y,
const char *text,
TTF_Font *font,
SDL_Rect *rect,
SDL_Color *color
) {
SDL_Surface *surface;
SDL_Texture *texture;
surface = TTF_RenderText_Solid(font, text, *color);
texture = SDL_CreateTextureFromSurface(renderer, surface);
rect->x = x;
rect->y = y;
rect->w = surface->w;
rect->h = surface->h;
/* This is wasteful for textures that stay the same.
* But makes things less stateful and easier to use.
* Not going to code an atlas solution here... are we? */
SDL_FreeSurface(surface);
SDL_RenderCopy(renderer, texture, NULL, rect);
SDL_DestroyTexture(texture);
}
int main(int argc, char **argv) {
SDL_Color color;
SDL_Event event;
SDL_Rect rect;
SDL_Renderer *renderer;
SDL_Window *window;
char *font_path, text[MAX_STRING_LEN];
/* CLI arguments. */
if (argc == 1) {
font_path = "FreeSans.ttf";
} else if (argc == 2) {
font_path = argv[1];
} else {
fprintf(stderr, "error: too many arguments\n");
exit(EXIT_FAILURE);
}
/* initialize variables. */
color.r = COMMON_COLOR_MAX;
color.g = COMMON_COLOR_MAX;
color.b = COMMON_COLOR_MAX;
color.a = COMMON_COLOR_MAX;
/* Init window. */
SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(
COMMON_WINDOW_WIDTH,
COMMON_WINDOW_WIDTH,
0,
&window,
&renderer
);
/* Init TTF. */
TTF_Init();
TTF_Font *font = TTF_OpenFont(font_path, 24);
if (font == NULL) {
fprintf(stderr, "error: font not found\n");
exit(EXIT_FAILURE);
}
/* Main loop. */
common_fps_init();
while (1) {
if (SDL_PollEvent(&event) && event.type == SDL_QUIT) {
break;
}
/* Use TTF. */
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
render_text(renderer, 0, 0, "hello", font, &rect, &color);
render_text(renderer, 0, rect.y + rect.h, "world", font, &rect, &color);
snprintf(text, MAX_STRING_LEN, "%u", (unsigned int)(time(NULL) % 1000));
render_text(renderer, 0, rect.y + rect.h, text, font, &rect, &color);
SDL_RenderPresent(renderer);
common_fps_update_and_print();
}
/* Cleanup. */
TTF_Quit();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return EXIT_SUCCESS;
}
Well, obviously you need to recreate texture from surface with new text each time your score changes. That is not very efficient for texts that change frequently (since you create/destroy a lot of surfaces/textures), but can be fine for small games (since modern computers are very powerful).
But generally, as mentioned in comments, for this case font atlases are used with combination of custom text renderers. The trick is to store all characters in one texture and render its regions multiple times to produce necessary text. The AngelCode BMFont is popuar tool for creating font atlases.
For maximum performance both approaches are used in combination: precreated textures for static text, and font atlases for dynamic text.