Skip to content

Tutorial 2

Vincent Magnin edited this page Jun 11, 2022 · 4 revisions

Drawing an image in a PNG file (without GUI)

Maybe you are frightened to use gtk-fortran because building a GUI is not so easy, or you don't really need it. But you can also make computer graphics in Fortran without any GUI, using the GdkPixbuf library included in GTK.

Simply talking, a pixbuf is a 1D array containing the RGB intensities of each pixel of the image. So pixel(1) is the red intensity (between 0 and 255) of the first pixel (top left), pixel(2) and pixel(3) its green and blue intensities, pixel(4) the red intensity of the next pixel on the same line, and so on. Lines are stored one after the other in the array.

A coloured Sierpinski triangle

This tutorial is based on the gtk-fortran example pixbuf_without_gui.f90 that we will explain block by block. Note that this example does not depend on your GTK version: we will use only the GdkPixbuf part.

That program just calls some C functions from the GdkPixbuf library to create a pixbuf array, then write values in that array, and finally calls a C function to create the PNG file.

About the needed modules

We first state which modules and functions we will use:

program pixbuf_without_gui
  use iso_c_binding, only: c_ptr, c_null_char, c_null_ptr, &
                         & c_f_pointer, c_char, c_int
  use gdk_pixbuf, only: gdk_pixbuf_get_n_channels, gdk_pixbuf_get_pixels, &
                      & gdk_pixbuf_get_rowstride, gdk_pixbuf_new, gdk_pixbuf_savev
  use gtk, only: GDK_COLORSPACE_RGB, FALSE

Of course, we need the iso_c_binding module. But also two gtk-fortran modules: gdk_pixbuf and gtk, which are defined in the files src/gdk-pixbuf-auto.f90, src/gtk.f90 (wich includes src/gtk-auto.inc and src/gtkenums-auto.inc). Note that before gtk-fortran 4.2, gdk_pixbuf_savev was in the gtk_os_dependent module.

The interfaces defined in those files were automatically generated by the cfwrapper.py python script, which parses the header files of the GTK libraries in /usr/include/. But don't bother, it's the maintainer's work!

For example, this is the gdk_pixbuf_new() interface in src/gdk-pixbuf-auto.f90:

! GDK_PIXBUF_AVAILABLE_IN_ALL
!GdkPixbuf *gdk_pixbuf_new (GdkColorspace colorspace, gboolean has_alpha, int bits_per_sample, int width, int height);
function gdk_pixbuf_new(colorspace, has_alpha, bits_per_sample, width, height)&
& bind(c)
  import :: c_ptr, c_int
  type(c_ptr) :: gdk_pixbuf_new
  integer(c_int), value :: colorspace
  integer(c_int), value :: has_alpha
  integer(c_int), value :: bits_per_sample
  integer(c_int), value :: width
  integer(c_int), value :: height
end function

The first commented line means the function is available in all versions of the library, and is not deprecated. The second commented line is the C prototype of the function from which was derived the Fortran interface.

The bind(c) statement means that this is an interface to a C function whose name will be the same as the Fortran function. As can be seen in the C prototype in comments, this function will return a C pointer, so we need the c_ptr type defined in the iso_c_binding intrinsic module. The arguments are here all integers (including the gboolean type) and we use the c_int type. Note also that arguments must be passed by value, except C pointers (and arrays).

The variables

We define our variables. Those whose type has the c_ prefix will be arguments to pass to C functions or their results:

  implicit none
  type(c_ptr) :: my_pixbuf
  character(c_char), dimension(:), pointer :: pixel
  integer(c_int) :: nch, rowstride, pixwidth, pixheight
  integer(c_int) :: cstatus   ! Command status
  double precision, dimension(1:3) :: x, y
  double precision :: xx, yy, diag, r
  integer :: s            ! Triangle vertex number
  integer :: n = 300000   ! Number of points
  integer :: i, p

pixel is a Fortran pointer toward a C array. We use characters because we need one byte unsigned integers.

A pixbuffer to store the pixels of the image

This pixbuffer has no Alpha channel, only RGB. Those functions were already explained in the Tutorial 1:

  pixwidth  = 800
  pixheight = 800
  my_pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8_c_int, &
                           & pixwidth, pixheight)
  nch = gdk_pixbuf_get_n_channels(my_pixbuf)
  rowstride = gdk_pixbuf_get_rowstride(my_pixbuf)
  print *, "Channels= ", nch, "      Rowstride=", rowstride
  call c_f_pointer(gdk_pixbuf_get_pixels(my_pixbuf), pixel, &
                 & (/pixwidth*pixheight*nch/))
  pixel = char(0)

The last line means the background will be black (intensity 0 for red, green, blue).

An amazing computation algorithm!

The algorithm used to plot the Sierpinski triangle is very simple:

  1. start from any point in the plan.
  2. Choose randomly one vertex of the triangle.
  3. Compute the middle between the current point and that vertex, and light the corresponding pixel (the new current point).
  4. Go to 2.

For more information, see:
https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle#Chaos_game

With the comments, you will easily understand the algorithm:

  ! Diagonal of the image:
  diag = sqrt(real(pixwidth*pixwidth + pixheight*pixheight, kind(0d0)))
  ! Coordinates of the triangle vertices:
  x = (/ pixwidth/2d0,  0d0,                      (pixwidth-1)*1d0        /)
  y = (/ 0d0,           pixheight*sqrt(3d0)/2d0,  pixheight*sqrt(3d0)/2d0 /)
  ! We start at an arbitrary position:
  xx = (x(1) + x(2)) / 2d0
  yy = (y(1) + y(2)) / 2d0

  do i = 1, n
      ! We choose randomly a vertex number (1, 2 or 3):
      call random_number(r)
      s = 1 + int(3*r)
      ! We compute the coordinates of the new point:
      xx = (xx + x(s)) / 2d0
      yy = (yy + y(s)) / 2d0
      ! Position of the corresponding pixel in the pixbuffer:
      p = 1 + nint(xx)*nch + nint(yy)*rowstride
      ! Red, Green, Blue values computed from the distances to vertices:
      pixel(p)   = char(int(255 * sqrt((xx-x(1))**2 + (yy-y(1))**2) / diag))
      pixel(p+1) = char(int(255 * sqrt((xx-x(2))**2 + (yy-y(2))**2) / diag))
      pixel(p+2) = char(int(255 * sqrt((xx-x(3))**2 + (yy-y(3))**2) / diag))
  end do

p is the position in the 1D array of the red byte of the (xx,yy) pixel. The red, green, blue values are computed to obtain beautiful colour gradients.

Saving a PixBuf in a PNG file

We just need to call the gdk_pixbuf_savev() function:

https://docs.gtk.org/gdk-pixbuf/method.Pixbuf.savev.html

  cstatus = gdk_pixbuf_savev(my_pixbuf, "sierpinski_triangle.png"//c_null_char,&
              & "png"//c_null_char, c_null_ptr, c_null_ptr, c_null_ptr)
end program pixbuf_without_gui

It's also possible to use other formats like JPEG, but you may need more work to pass the arguments (compression ratio, etc.) PNG is easier, you just need to pass the "png" string and null pointer values.

That's all folks!

Let's compile and run the program:

$ gfortran pixbuf_without_gui.f90 $(pkg-config --cflags --libs gtk-4-fortran) && ./a.out
 Channels=            3       Rowstride=        2400

As expected, the number of channels is 3 (RGB) and the rowstride is 2400 (3 bytes x 800 pixels). But you should always use the gdk_pixbuf_get_rowstride() function because the documentation says: "There may be padding at the end of a row. The 'rowstride' value of a pixbuf, as returned by gdk_pixbuf_get_rowstride(), indicates the number of bytes between rows."

No window opens, but you will find a new sierpinski_triangle.png file in the same directory:

Conclusion

Congratulations! In 45 lines of Fortran code you have created a beautiful picture of a fractal object and saved it to a PNG file. If you don't need a Graphical User Interface and just want to obtain a picture of your scientific results, you now have a template!

Clone this wiki locally