0063: Cairo VII – Draw and Save Images

So, this time we’re going to draw some images and then save them.

Warning: These images are not high art and may or may not cause harm to your artistic sensibilities. (You’ve now been officially cautioned.)

In fact, because all these images are just a rectangle with text overlaid, I’m not even going to go over how they’re drawn. I’ll just say this one thing…

If you want a draw operation to be on top of another—like text overlaid on a rectangle, for instance—foreground draw operations need to be done after background draw operations. You probably sorted that out on your own, but there it is anyway.

Saving Images – Basic Procedure

Every time we save an image, we carry out the same set of operations:

  • find the size of the DrawingArea,
  • grab the full expanse of the DrawingArea’s Surface that we want to save and stuff it into a buffer,
  • set up the save options and option values for the image (this is different for each format), and
  • save the image.

But since each has their own save options, we’ll look at each individually starting with…

Saving a JPeg

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

Here’s the initialization section:

GtkAllocation size;
Pixbuf pixbuf;
string[] jpegOptions, jpegOptionValues;
int xOffset = 0, yOffset = 0;

We used a GtkAllocation object before, the main purpose of which is to get the size allocated to a Widget—in this case, the DrawingArea—so we don’t have to guess.

The Pixbuf is the buffer we’re going to stuff the image into as we prepare to save and the rest are pretty obvious:

  • jpegOptions: an array of save options,
  • jpegOptionValues: an array of values for each of the save options,
  • xOrigin, yOrigin: the offset from the top-left corner of the DrawingArea.

Note: If xOffset and yOffset are non-zero, we won’t be saving the entire image.

Note 2: The last two arguments to the getFromSurface() call (see the onDraw() snippet below), if less than the width and height of the Surface, will also save less than the entire image. Taking these two notes together, I’m sure you arrived at the conclusion that you can save any rectangular area of the Surface you choose.

I haven’t bothered with width and height variables here because these examples all save the entire DrawingArea and so we use the width and height fields from the GtkAllocation as we’ll see in a moment.

The constructor is so mundane as to warrant skipping in this discussion… so we shall.

As for the callback, we’ll leave out the drawing bit and go right for the meat:

bool onDraw(Scoped!Context context, Widget w)
{
	// set up and draw a rectangle
	// (see code file)

	// set up and draw text
	// (see code file)

	getAllocation(size); // grab the widget's size as allocated by its parent
	pixbuf = getFromSurface(context.getTarget(), xOffset, yOffset, size.width, size.height); // the contents of the surface go into the buffer

	// prep and write JPEG file
	jpegOptions = ["quality"]; 
	jpegOptionValues = ["100"];

	if(pixbuf.savev("./rectangle_hw.jpg", "jpeg", jpegOptions, jpegOptionValues))
	{
		writeln("JPEG was successfully saved.");
		
	}

	return(true);
	
} // onDraw()

And here’s what’s happening:

  • get the size of the DrawingArea with getAllocation(size),
  • grab either the entire Surface or a subsurface (by supplying a non-zero offset and/or a width and height less than the Surface’s width and height) and stuff it into a buffer (that’s the Pixbuf),
  • set up the JPeg save options and their values, and
  • call Pixbuf.savev() to save the image.

Save Options for JPeg

  • “icc-profile” - the complete ICC profile encoded into base64 (which, you’ll note, I didn’t bother with)
  • “quality” - 0 … 100
  • “x-dpi” - dots per inch (reasonable values: 50 to 300)
  • “y-dpi” - dots per inch (same as x-dpi: 50 to 300)

And that’s all there is to it.

Saving a PNG

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

Since everything else is the same, let’s skip right to the part of the callback that does the saving:

// prep and write to a PNG
pngOptions = ["x-dpi", "y-dpi", "compression"];
pngOptionValues = ["150", "150", "1"];
	
if(pixbuf.savev("./rectangle_hw.png", "png", pngOptions, pngOptionValues))
{
	writeln("PNG was successfully saved.");
	
}

So the differences here (compared to our first example) are:

  • set up the PNG save options,
  • the values for each, and
  • save with the .png file extension.

Save Options for PNG

  • “x-dpi” dots per inch (reasonable range is 50 to 300)
  • “y-dpi” dots per inch (same as x-dpi: 50 to 300)
  • “tEXt::key” - an ASCII string of length 1 – 79, UTF-8 encoded strings
  • “compression” - 0 … 9
  • “icc-profile” - the complete ICC profile encoded into base64

Saving a TIFF

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

As with the others, just set up the options and save:

tiffOptions = ["bits-per-sample", "compression"];
tiffOptionValues = ["8", "1"];
	
if(pixbuf.savev("./rectangle_hw.tiff", "tiff", tiffOptions, tiffOptionValues))
{
	writeln("TIFF was successfully saved.");
		
}

Save Options for TIFF

  • “bits-per-sample”
    • “1” for saving bi-level CCITTFAX4 images
    • “8” for saving 8-bits per sample
  • “compression”
    • “1” for no compression
    • “2” for Huffman
    • “5” for LZW
    • “7” for JPEG
    • “8” for DEFLATE (see the libtiff documentation and tiff.h for all supported codec values)
  • “icc-profile” - (zero-terminated string) containing a base64 encoded ICC color profile.

Saving a BMP

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

Everything’s the same, but there are no listed options so you have to pass in two empty arrays like this:

if(pixbuf.savev("./rectangle_hw.bmp", "bmp", [], []))
{
	writeln("BMP was successfully saved.");
		
}

Conclusion

And that’s most of what there is to saving images using Cairo. Next time, we’ll start digging into animation.

Comments? Questions? Observations?

Did we miss a tidbit of information that would make this post even more informative? Let's talk about it in the comments.

You can also subscribe via RSS so you won't miss anything. Thank you very much for dropping by.

© Copyright 2023 Ron Tarrant