Search code examples
c++transparencyfltk

How to get image with transparent background from FLTK Fl__Image__Surface?


I want to draw string or char (offscreen) and use it as Fl_Image or Fl_RGB_Image. Based on this link I can do that easily with Fl__Image__Surface. The problem with Fl__Image__Surface is that it does not support transparency when I convert its output to image (Fl_RGB_Image) using image() method. So is there any way I can achieve this? I can do that on Java Swing with BufferedImage, also in Android with Canvas by creating Bitmap with Bitmap.Config.ARGB_8888.


Solution

  • If you prefer to do it manually, you can try the following:

    #include <FL/Enumerations.H>
    #include <FL/Fl.H>
    #include <FL/Fl_Box.H>
    #include <FL/Fl_Device.H>
    #include <FL/Fl_Double_Window.H>
    #include <FL/Fl_Image.H>
    #include <FL/Fl_Image_Surface.H>
    #include <FL/fl_draw.H>
    #include <cassert>
    #include <vector>
    
    Fl_RGB_Image *get_image(int w, int h) {
        // draw image on surface
        auto img_surf = new Fl_Image_Surface(w, h);
        Fl_Surface_Device::push_current(img_surf);
        // We'll use white to mask 255, 255, 255, see the loop
        fl_color(FL_WHITE);
        fl_rectf(0, 0, w, h);
        fl_color(FL_BLACK);
        fl_font(FL_HELVETICA_BOLD, 20);
        fl_draw("Hello", 100, 100);
        auto image = img_surf->image();
        delete img_surf;
        Fl_Surface_Device::pop_current();
        return image;
    }
    
    Fl_RGB_Image *get_transparent_image(const Fl_RGB_Image *image) {
        assert(image);
        // make image transparent
        auto data = (const unsigned char*)(*image->data());
        auto len = image->w() * image->h() * image->d(); // the depth is by default 3
        std::vector<unsigned char> temp;
        for (size_t i = 0; i < len; i++) {
            if (i > 0 && i % 3 == 0) {
                // check if the last 3 vals are the rgb values of white, add a 0 alpha
                if (data[i] == 255 && data[i - 1] == 255 && data[i - 2] == 255)
                    temp.push_back(0);
                else
                    // add a 255 alpha, making the black opaque
                    temp.push_back(255);
                temp.push_back(data[i]);
            } else {
                temp.push_back(data[i]);
            }
        }
        temp.push_back(0);
        assert(temp.size() == image->w() * image->h() * 4);
        auto new_image_data = new unsigned char[image->w() * image->h() * 4];
        memcpy(new_image_data, temp.data(), image->w() * image->h() * 4);
        auto new_image = new Fl_RGB_Image(new_image_data, image->w(), image->h(), 4); // account for alpha
        return new_image;
    }
    
    int main() {
        auto win = new Fl_Double_Window(400, 300);
        auto box = new Fl_Box(0, 0, 400, 300);
        win->end();
        win->show();
        auto image = get_image(box->w(), box->h());
        auto transparent_image = get_transparent_image(image);
        delete image;
        box->image(transparent_image);
        box->redraw();
        return Fl::run();
    }
    

    The idea is that an Fl_Image_Surface gives an Fl_RGB_Image with 3 channels (r, g, b), no alpha. We manually add the alpha by creating a temporary vector, querying the data (can be optimized if you know the colors you're using by only checking data[i] == 255. The vector is an RAII type whose life ends at the end of the scope, so we just mempcy the data from the vector to a long-lived unsigned char array that we pass to an Fl_RGB_Image specifying the depth to be 4, accounting for alpha.

    The other option is to use an external library like CImg (single header lib) to draw text into an image buffer and then pass that buffer to Fl_RGB_Image.