12a42b2e268fdb4684e84c4fe909baac5768fa15
[rodin/chimara.git] / libchimara / graphics.c
1 #include "graphics.h"
2 #include "chimara-glk-private.h"
3 #include "magic.h"
4
5 #define BUFFER_SIZE (1024)
6
7 extern GPrivate *glk_data_key;
8 void on_size_prepared(GdkPixbufLoader *loader, gint width, gint height, struct image_info *info);
9 void on_pixbuf_closed(GdkPixbufLoader *loader, gpointer data);
10
11 static gboolean image_loaded;
12 static gboolean size_determined;
13
14 static struct image_info*
15 load_image_in_cache(glui32 image, gint width, gint height)
16 {
17         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
18         giblorb_err_t blorb_error = 0;
19         giblorb_result_t resource;
20         GError *pixbuf_error = NULL;
21         guchar *buffer;
22
23         /* Lookup the proper resource */
24         blorb_error = giblorb_load_resource(glk_data->resource_map, giblorb_method_FilePos, &resource, giblorb_ID_Pict, image);
25         if(blorb_error != giblorb_err_None) {
26                 WARNING_S( "Error loading resource", giblorb_get_error_message(blorb_error) );
27                 return NULL;
28         }
29
30         struct image_info *info = g_new0(struct image_info, 1);
31         info->resource_number = image;
32
33         /* Load the resource */
34         GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
35         g_signal_connect( loader, "size-prepared", G_CALLBACK(on_size_prepared), info ); 
36         g_signal_connect( loader, "closed", G_CALLBACK(on_pixbuf_closed), NULL ); 
37
38         /* Scale image if necessary */
39         if(width > 0 && height > 0) {
40                 gdk_pixbuf_loader_set_size(loader, width, height);
41                 info->scaled = TRUE;
42         }
43
44         glk_stream_set_position(glk_data->resource_file, resource.data.startpos, seekmode_Start);
45         buffer = g_malloc( BUFFER_SIZE * sizeof(guchar) );
46
47         guint32 total_read = 0;
48         image_loaded = FALSE;
49         while(total_read < resource.length && !image_loaded) {
50                 guint32 num_read = glk_get_buffer_stream(glk_data->resource_file, (char *) buffer, BUFFER_SIZE);
51
52                 if( !gdk_pixbuf_loader_write(loader, buffer, MIN(BUFFER_SIZE, num_read), &pixbuf_error) ) {
53                         WARNING_S("Cannot read image", pixbuf_error->message);
54                         giblorb_unload_chunk(glk_data->resource_map, image);
55                         gdk_pixbuf_loader_close(loader, &pixbuf_error);
56                         g_free(buffer);
57                         return NULL;
58                 }
59
60                 total_read += num_read;
61         }
62         gdk_pixbuf_loader_close(loader, &pixbuf_error);
63         giblorb_unload_chunk(glk_data->resource_map, resource.chunknum);
64         g_free(buffer);
65
66         /* Wait for the PixbufLoader to finish loading the image */
67         g_mutex_lock(glk_data->resource_lock);
68         while(!image_loaded) {
69                 g_cond_wait(glk_data->resource_loaded, glk_data->resource_lock);
70         }
71         g_mutex_unlock(glk_data->resource_lock);
72
73         /* Store the image in the cache */
74         gdk_threads_enter();
75
76         if( g_slist_length(glk_data->image_cache) >= IMAGE_CACHE_MAX_NUM ) {
77                 struct image_info *head = (struct image_info*) glk_data->image_cache->data;
78                 gdk_pixbuf_unref(head->pixbuf);
79                 g_free(head);
80                 glk_data->image_cache = g_slist_remove_link(glk_data->image_cache, glk_data->image_cache);
81         }
82         info->pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
83         gdk_pixbuf_ref(info->pixbuf);
84         info->width = gdk_pixbuf_get_width(info->pixbuf);
85         info->height = gdk_pixbuf_get_height(info->pixbuf);
86         glk_data->image_cache = g_slist_prepend(glk_data->image_cache, info);
87
88         gdk_threads_leave();
89
90         g_object_unref(loader);
91         return info;
92 }
93
94 void
95 on_size_prepared(GdkPixbufLoader *loader, gint width, gint height, struct image_info *info)
96 {
97         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
98
99         g_mutex_lock(glk_data->resource_lock);
100         info->width = width;
101         info->height = height;
102         size_determined = TRUE;
103         g_cond_broadcast(glk_data->resource_info_available);
104         g_mutex_unlock(glk_data->resource_lock);
105 }
106
107 void
108 on_pixbuf_closed(GdkPixbufLoader *loader, gpointer data)
109 {
110         gdk_threads_enter();
111
112         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
113
114         g_mutex_lock(glk_data->resource_lock);
115         image_loaded = TRUE;
116         g_cond_broadcast(glk_data->resource_loaded);
117         g_mutex_unlock(glk_data->resource_lock);
118
119         gdk_threads_leave();
120 }
121
122
123 void
124 clear_image_cache(struct image_info *data, gpointer user_data)
125 {
126         gdk_pixbuf_unref(data->pixbuf);
127         g_free(data);
128 }
129
130 static struct image_info*
131 image_cache_find(struct image_info* to_find)
132 {
133         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
134         GSList *link = glk_data->image_cache;
135
136         gdk_threads_enter();
137
138         /* Empty cache */
139         if(link == NULL) {
140                 gdk_threads_leave();
141                 printf("Cache miss for image %d\n", to_find->resource_number);
142                 return NULL;
143         }
144
145         /* Iterate over the cache to find the correct image and size */
146         do {
147                 struct image_info *info = (struct image_info*) link->data;
148                 if(info->resource_number == to_find->resource_number) {
149                         /* Check size: are we looking for a scaled version or the original one? */
150                         if(to_find->scaled) {
151                                 if(info->width >= to_find->width && info->height >= to_find->height) {
152                                         gdk_threads_leave();
153                                         printf("Cache hit for image %d\n", to_find->resource_number);
154                                         return info; /* Found a good enough match */
155                                 }
156                         } else {
157                                 if(!info->scaled) {
158                                         gdk_threads_leave();
159                                         printf("Cache hit for image %d\n", to_find->resource_number);
160                                         return info; /* Found a match */
161                                 }
162                         }
163                 }
164         } while( (link = g_slist_next(link)) );
165
166         gdk_threads_leave();
167
168         printf("Cache miss for image %d\n", to_find->resource_number);
169         return NULL; /* No match found */
170 }
171
172 /**
173  * glk_image_get_info:
174  * @image: An image resource number.
175  * @width: Pointer to a location at which to store the image's width.
176  * @height: Pointer to a location at which to store the image's height.
177  *
178  * This gets information about the image resource with the given identifier. It
179  * returns %TRUE if there is such an image, and %FALSE if not. You can also pass
180  * pointers to width and height variables; if the image exists, the variables
181  * will be filled in with the width and height of the image, in pixels. (You can
182  * pass %NULL for either width or height if you don't care about that 
183  * information.)
184  * 
185  * <note><para>
186  *   You should always use this function to measure the size of images when you 
187  *   are creating your display. Do this even if you created the images, and you
188  *   know how big they <quote>should</quote> be. This is because images may be 
189  *   scaled in translating from one platform to another, or even from one 
190  *   machine to another. A Glk library might display all images larger than 
191  *   their original size, because of screen resolution or player preference. 
192  *   Images will be scaled proportionally, but you still need to call 
193  *   glk_image_get_info() to determine their absolute size.
194  * </para></note>
195  * 
196  * Returns: %TRUE if @image is a valid identifier, %FALSE if not.
197  */
198 glui32
199 glk_image_get_info(glui32 image, glui32 *width, glui32 *height)
200 {
201         struct image_info *to_find = g_new0(struct image_info, 1);
202         struct image_info *found;
203         to_find->resource_number = image;
204         to_find->scaled = FALSE; /* we want the original image size */
205
206         if( !(found = image_cache_find(to_find)) ) {
207                 found = load_image_in_cache(image, 0, 0);
208                 if(found == NULL)
209                         return FALSE;
210         }
211
212         if(width != NULL)
213                 *width = found->width;
214         if(width != NULL)
215                 *height = found->height;
216         return TRUE;
217 }
218
219 /**
220  * glk_image_draw:
221  * @win: A graphics or text buffer window.
222  * @image: An image resource number.
223  * @val1: The x coordinate at which to draw the image (if @win is a graphics 
224  * window); or, an <link linkend="chimara-imagealign-InlineUp">image 
225  * alignment</link> constant (if @win is a text window).
226  * @val2: The y coordinate at which to draw the image (if @win is a graphics
227  * window); this parameter is ignored if @win is a text buffer window.
228  *
229  * This draws the given image resource in the given window. The position of the
230  * image is given by @val1 and @val2, but their meaning varies depending on what
231  * kind of window you are drawing in. See <link 
232  * linkend="chimara-Graphics-in-Graphics-Windows">Graphics in Graphics 
233  * Windows</link> and <link linkend="Graphics-in-Text-Buffer-Windows">Graphics 
234  * in Text Buffer Windows</link>.
235  * 
236  * This function returns a flag indicating whether the drawing operation 
237  * succeeded.
238  * <note><para>
239  *   A %FALSE result can occur for many reasons. The image data might be 
240  *   corrupted; the library may not have enough memory to operate; there may be 
241  *   no image with the given identifier; the window might not support image 
242  *   display; and so on.
243  * </para></note>
244  *
245  * Returns: %TRUE if the operation succeeded, %FALSE if not.
246  */
247 glui32
248 glk_image_draw(winid_t win, glui32 image, glsi32 val1, glsi32 val2)
249 {
250         VALID_WINDOW(win, return FALSE);
251         g_return_val_if_fail(win->type == wintype_Graphics, FALSE);
252
253         struct image_info *to_find = g_new0(struct image_info, 1);
254         struct image_info *info;
255         GdkPixmap *canvas;
256
257         /* Lookup the proper resource */
258         to_find->resource_number = image;
259         to_find->scaled = FALSE; /* we want the original image size */
260
261         if( !(info = image_cache_find(to_find)) ) {
262                 info = load_image_in_cache(image, 0, 0);
263                 if(info == NULL)
264                         return FALSE;
265         }
266
267         gdk_threads_enter();
268
269         gtk_image_get_pixmap( GTK_IMAGE(win->widget), &canvas, NULL );
270         if(canvas == NULL) {
271                 WARNING("Could not get pixmap");
272                 return FALSE;
273         }
274
275         gdk_draw_pixbuf( GDK_DRAWABLE(canvas), NULL, GDK_PIXBUF((GdkPixbuf*)info->pixbuf), 0, 0, val1, val2, -1, -1, GDK_RGB_DITHER_NONE, 0, 0 );
276
277         /* Update the screen */
278         gtk_widget_queue_draw(win->widget);
279
280         gdk_threads_leave();
281
282         return TRUE;
283 }
284
285 /**
286  * glk_image_draw_scaled:
287  * @win: A graphics or text buffer window.
288  * @image: An image resource number.
289  * @val1: The x coordinate at which to draw the image (if @win is a graphics 
290  * window); or, an <link linkend="chimara-imagealign-InlineUp">image 
291  * alignment</link> constant (if @win is a text window).
292  * @val2: The y coordinate at which to draw the image (if @win is a graphics
293  * window); this parameter is ignored if @win is a text buffer window.
294  * @width: The width of the image.
295  * @height: The height of the image.
296  *
297  * This is similar to glk_image_draw(), but it scales the image to the given 
298  * @width and @height, instead of using the image's standard size. (You can 
299  * measure the standard size with glk_image_get_info().)
300  * 
301  * If @width or @height is zero, nothing is drawn. Since those arguments are 
302  * unsigned integers, they cannot be negative. If you pass in a negative number,
303  * it will be interpreted as a very large positive number, which is almost 
304  * certain to end badly. 
305  *
306  * Returns: %TRUE if the operation succeeded, %FALSE otherwise.
307  */
308 glui32
309 glk_image_draw_scaled(winid_t win, glui32 image, glsi32 val1, glsi32 val2, glui32 width, glui32 height)
310 {
311         VALID_WINDOW(win, return FALSE);
312         g_return_val_if_fail(win->type == wintype_Graphics, FALSE);
313
314         ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
315         struct image_info *to_find = g_new0(struct image_info, 1);
316         struct image_info *info;
317         struct image_info *scaled_info;
318         GdkPixmap *canvas;
319
320         /* Lookup the proper resource */
321         to_find->resource_number = image;
322         to_find->scaled = TRUE; /* any image size equal or larger than requested will do */
323
324         if( !(info = image_cache_find(to_find)) ) {
325                 info = load_image_in_cache(image, width, height);
326                 if(info == NULL)
327                         return FALSE;
328         }
329
330         gdk_threads_enter();
331
332         gtk_image_get_pixmap( GTK_IMAGE(win->widget), &canvas, NULL );
333         if(canvas == NULL) {
334                 WARNING("Could not get pixmap");
335                 return FALSE;
336         }
337
338         /* Scale the image if necessary */
339         if(info->width != width || info->height != height) {
340                 GdkPixbuf *scaled = gdk_pixbuf_scale_simple(info->pixbuf, width, height, GDK_INTERP_BILINEAR);
341
342                 /* Add the scaled image into the image cache */
343                 scaled_info = g_new0(struct image_info, 1);
344                 scaled_info->resource_number = info->resource_number;
345                 scaled_info->width = gdk_pixbuf_get_width(scaled);
346                 scaled_info->height = gdk_pixbuf_get_width(scaled);
347                 scaled_info->pixbuf = scaled;
348                 scaled_info->scaled = TRUE;
349                 glk_data->image_cache = g_slist_prepend(glk_data->image_cache, scaled_info);
350
351                 /* Continue working with the scaled version */
352                 info = scaled_info;
353         }
354
355         gdk_draw_pixbuf( GDK_DRAWABLE(canvas), NULL, info->pixbuf, 0, 0, val1, val2, -1, -1, GDK_RGB_DITHER_NONE, 0, 0 );
356
357         /* Update the screen */
358         gtk_widget_queue_draw(win->widget);
359
360         gdk_threads_leave();
361
362         return TRUE;
363 }
364
365 /**
366  * glk_window_set_background_color:
367  * @win: A graphics window.
368  * @color: a 32-bit RGB color value.
369  *
370  * This sets the window's background color. It does not change what is currently
371  * displayed; it only affects subsequent clears and resizes. The initial 
372  * background color of each window is white.
373  * 
374  * Colors are encoded in a 32-bit value: the top 8 bits must be zero, the next 8
375  * bits are the red value, the next 8 bits are the green value, and the bottom 8
376  * bits are the blue value. Color values range from 0 to 255.
377  * <note><para>
378  *   So <code>0x00000000</code> is black, <code>0x00FFFFFF</code> is white, and 
379  *   <code>0x00FF0000</code> is bright red.
380  * </para></note>
381  * 
382  * <note><para>
383  *   This function may only be used with graphics windows. To set background 
384  *   colors in a text window, use text styles with color hints; see <link 
385  *   linkend="Styles">Styles</link>.
386  * </para></note>
387  */
388 void
389 glk_window_set_background_color(winid_t win, glui32 color) 
390 {
391         VALID_WINDOW(win, return);
392         g_return_if_fail(win->type == wintype_Graphics);
393         
394         win->background_color = color;
395 }
396
397 /**
398  * glk_window_fill_rect:
399  * @win: A graphics window.
400  * @color: A 32-bit RGB color value, see glk_window_set_background_color().
401  * @left: The x coordinate of the top left corner of the rectangle.
402  * @top: The y coordinate of the top left corner of the rectangle.
403  * @width: The width of the rectangle.
404  * @height: The height of the rectangle.
405  *
406  * This fills the given rectangle with the given color. It is legitimate for
407  * part of the rectangle to fall outside the window. If width or height is zero,
408  * nothing is drawn. 
409  */
410 void
411 glk_window_fill_rect(winid_t win, glui32 color, glsi32 left, glsi32 top, glui32 width, glui32 height)
412 {
413         VALID_WINDOW(win, return);
414         g_return_if_fail(win->type == wintype_Graphics);
415
416
417         gdk_threads_enter();
418
419         GdkPixmap *map;
420         gtk_image_get_pixmap( GTK_IMAGE(win->widget), &map, NULL );
421         gdk_draw_rectangle( GDK_DRAWABLE(map), win->widget->style->white_gc, TRUE, left, top, width, height);
422         gtk_widget_queue_draw(win->widget);
423
424         gdk_threads_leave();
425 }
426
427 /**
428  * glk_window_erase_rect:
429  * @win: A graphics window.
430  * @left: The x coordinate of the top left corner of the rectangle.
431  * @top: The y coordinate of the top left corner of the rectangle.
432  * @width: The width of the rectangle.
433  * @height: The height of the rectangle.
434  *
435  * This fills the given rectangle with the window's background color.
436  * 
437  * You can also fill an entire graphics window with its background color by 
438  * calling glk_window_clear().
439  */
440 void
441 glk_window_erase_rect(winid_t win, glsi32 left, glsi32 top, glui32 width, glui32 height)
442 {
443         glk_window_fill_rect(win, win->background_color, left, top, width, height);
444 }
445
446 void glk_window_flow_break(winid_t win)
447 {
448 }
449
450 /*** Called when the graphics window is resized. Resize the backing pixmap if necessary ***/
451 void
452 on_graphics_size_allocate(GtkWidget *widget, GtkAllocation *allocation, winid_t win)
453
454         GdkPixmap *oldmap;
455         gtk_image_get_pixmap( GTK_IMAGE(widget), &oldmap, NULL );
456         gint oldwidth = 0;
457         gint oldheight = 0;
458  
459         /* Determine whether a pixmap exists with the correct size */
460         gboolean needs_resize = FALSE;
461         if(oldmap == NULL)
462                 needs_resize = TRUE;
463         else {
464                 gdk_drawable_get_size( GDK_DRAWABLE(oldmap), &oldwidth, &oldheight );
465                 if(oldwidth != allocation->width || oldheight != allocation->height)
466                         needs_resize = TRUE;
467         }
468
469         if(needs_resize) {
470                 /* Create a new pixmap */
471                 GdkPixmap *newmap = gdk_pixmap_new(widget->window, allocation->width, allocation->height, -1);
472                 gdk_draw_rectangle( GDK_DRAWABLE(newmap), widget->style->white_gc, TRUE, 0, 0, allocation->width, allocation->height);
473
474                 /* Copy the contents of the old pixmap */
475                 if(oldmap != NULL)
476                         gdk_draw_drawable( GDK_DRAWABLE(newmap), widget->style->white_gc, GDK_DRAWABLE(oldmap), 0, 0, 0, 0, oldwidth, oldheight);
477                 
478                 /* Use the new pixmap */
479                 gtk_image_set_from_pixmap( GTK_IMAGE(widget), newmap, NULL );
480                 g_object_unref(newmap);
481         }
482 }
483