redeye 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,650 @@
1
+ %pkg-config gtk+-2.0
2
+ %include gdk-pixbuf/gdk-pixbuf.h
3
+
4
+
5
+ %map VALUE > GdkPixbuf* : GDK_PIXBUF(RVAL2GOBJ(%%))
6
+ %map GdkPixbuf* > VALUE : GOBJ2RVAL(GDK_PIXBUF(%%))
7
+ %map unref_pixbuf > VALUE : unref_pixbuf((%%))
8
+
9
+ %{
10
+
11
+ #define assert(x) if (!(x)) { rb_raise(rb_eRuntimeError, "Assertion: '%s' failed.", #x); }
12
+
13
+ typedef struct {
14
+ char red,green,blue;
15
+ } rgb_t;
16
+
17
+ typedef struct {
18
+ int minX, maxX, minY, maxY;
19
+ int width, height;
20
+ int noPixels, mergeWith;
21
+ } region_info;
22
+
23
+ typedef struct {
24
+ struct {
25
+ int minX, maxX, minY, maxY;
26
+ int width, height;
27
+ } area;
28
+ struct {
29
+ int *data;
30
+ region_info *region;
31
+ int len, size;
32
+ } regions;
33
+ int *mask;
34
+ GdkPixbuf *pixbuf, *preview;
35
+ } redeyeop_t;
36
+
37
+ #define MIN_RED_VAL 20
38
+
39
+ static inline VALUE
40
+ unref_pixbuf(GdkPixbuf *pixbuf)
41
+ {
42
+ volatile VALUE pb = Qnil;
43
+
44
+ pb = GOBJ2RVAL(pixbuf);
45
+
46
+ g_object_unref(pixbuf);
47
+
48
+ return pb;
49
+ }
50
+
51
+ static void identify_possible_redeye_pixels(redeyeop_t *op,
52
+ double green_sensitivity, double blue_sensitivity,
53
+ int min_red_val)
54
+ {
55
+ guchar *data = gdk_pixbuf_get_pixels(op->pixbuf);
56
+ int rowstride = gdk_pixbuf_get_rowstride(op->pixbuf);
57
+ int pixWidth = gdk_pixbuf_get_has_alpha(op->pixbuf) ? 4 : 3;
58
+
59
+ int y, ry = 0, x, rx = 0;
60
+ for ( y = op->area.minY; y < op->area.maxY; y++ )
61
+ {
62
+ guchar *thisLine = data + (rowstride * y);
63
+ guchar *pixel;
64
+
65
+ pixel = thisLine + (op->area.minX * pixWidth);
66
+ rx = 0;
67
+
68
+ for ( x = op->area.minX; x < op->area.maxX; x++ )
69
+ {
70
+
71
+ int r,g,b;
72
+
73
+ r = pixel[0];
74
+ g = pixel[1];
75
+ b = pixel[2];
76
+
77
+ gboolean threshMet;
78
+
79
+ threshMet = (((double)r) > (green_sensitivity * (double)g)) &&
80
+ (((double)r) > (blue_sensitivity * (double)b)) &&
81
+ (r > min_red_val);
82
+
83
+ if(threshMet)
84
+ op->mask[ rx + ry ] = r;
85
+ else
86
+ op->mask[ rx + ry ] = 0; /* MEMZERO should have done its job ? */
87
+
88
+ pixel += pixWidth;
89
+ rx ++;
90
+ }
91
+
92
+ ry += op->area.width;
93
+ }
94
+ }
95
+
96
+
97
+ inline int group_at(redeyeop_t *op, int px, int py)
98
+ {
99
+ int index, region;
100
+
101
+ if (px < 0 || py < 0)
102
+ return 0;
103
+
104
+ index = px + ( py * op->area.width );
105
+
106
+ if (index < 0)
107
+ return 0;
108
+ if (index > (op->area.width * op->area.height))
109
+ return 0;
110
+
111
+ region = op->regions.data[ index ];
112
+ if (region > 0) {
113
+ if (op->regions.region[ region ].mergeWith) {
114
+ return op->regions.region[ region ].mergeWith;
115
+ } else {
116
+ return region;
117
+ }
118
+ } else {
119
+ return 0;
120
+ }
121
+ }
122
+
123
+ #define group_for(x,y) group_at(op, x, y)
124
+
125
+ static void identify_blob_groupings(redeyeop_t *op)
126
+ {
127
+ volatile int next_blob_id = 1, blob_id, y, x;
128
+
129
+
130
+ for( y = 0; y < op->area.height; y++ )
131
+ {
132
+ for ( x = 0; x < op->area.width; x++ )
133
+ {
134
+ if (op->mask[ x + (y * op->area.width) ] > 0) {
135
+ gboolean existing = FALSE;
136
+ int sx, sy, group = 0;
137
+ // Target pixel is true
138
+ blob_id = 0;
139
+
140
+ for (sy = y; sy >= y - 1; sy --) {
141
+ sx = (sy == y) ? x : x + 1;
142
+ for (; sx >= (x - 1); sx --) {
143
+ /*if ((sx >= x) && (sy >= y))
144
+ goto blob_scan_done;*/
145
+
146
+ if (sx >= 0 && sy >= 0)
147
+ group = group_for(sx, sy);
148
+
149
+ if (group) {
150
+ existing = TRUE;
151
+ if (blob_id) {
152
+ int target = MIN(blob_id, group);
153
+ int from = MAX(blob_id, group);
154
+
155
+ if (op->regions.region[target].mergeWith > 0) {
156
+ // Already merged
157
+ target = op->regions.region[target].mergeWith;
158
+ }
159
+ op->regions.region[from].mergeWith = target;
160
+
161
+ // Merge blob_id & group
162
+ }
163
+ blob_id = group;
164
+ }
165
+ }
166
+ }
167
+
168
+ if (blob_id == 0)
169
+ { // Allocate new group
170
+ blob_id = next_blob_id;
171
+ op->regions.region[blob_id].minX = x;
172
+ op->regions.region[blob_id].maxX = x;
173
+ op->regions.region[blob_id].minY = y;
174
+ op->regions.region[blob_id].maxY = y;
175
+ op->regions.region[blob_id].width = 1;
176
+ op->regions.region[blob_id].height = 1;
177
+ op->regions.region[blob_id].noPixels = 1;
178
+ op->regions.region[blob_id].mergeWith = 0;
179
+
180
+ next_blob_id ++;
181
+ op->regions.len = next_blob_id;
182
+
183
+ if (next_blob_id >= op->regions.size) {
184
+ int extra, new_size;
185
+
186
+ /*
187
+ * Realloc in increasingly large chunks to reduce memory fragmentation
188
+ */
189
+ extra = op->regions.size;
190
+ new_size = op->regions.size + extra;
191
+
192
+ REALLOC_N(op->regions.region, region_info, new_size);
193
+
194
+ op->regions.size = new_size;
195
+ }
196
+ }
197
+
198
+ if (existing)
199
+ {
200
+ op->regions.region[blob_id].minX = MIN(x, op->regions.region[blob_id].minX);
201
+ op->regions.region[blob_id].maxX = MAX(x, op->regions.region[blob_id].maxX);
202
+ op->regions.region[blob_id].minY = MIN(y, op->regions.region[blob_id].minY);
203
+ op->regions.region[blob_id].maxY = MAX(y, op->regions.region[blob_id].maxY);
204
+ op->regions.region[blob_id].width = op->regions.region[blob_id].maxX -
205
+ op->regions.region[blob_id].minX + 1;
206
+ op->regions.region[blob_id].height = op->regions.region[blob_id].maxY -
207
+ op->regions.region[blob_id].minY + 1;
208
+ op->regions.region[blob_id].noPixels ++;
209
+ }
210
+
211
+ op->regions.data[ x + (y * op->area.width) ] = blob_id;
212
+ }
213
+ }
214
+ }
215
+ /*FILE *fp = fopen("regions.txt","w");*/
216
+ for (y = 0; y < op->area.height; y++) {
217
+ for (x = 0; x < op->area.width; x++) {
218
+ int g = group_at(op, x, y); // Returns the merged group...
219
+ op->regions.data[ x + (y * op->area.width) ] = g;
220
+ /*
221
+ if (op->regions.len <= 0xf || 1)
222
+ {
223
+ if (g == 0)
224
+ fprintf(fp, " ");
225
+ else
226
+ fprintf(fp, "%x", g);
227
+ }
228
+ else
229
+ {
230
+ if (g == 0)
231
+ fprintf(fp, " ");
232
+ else
233
+ fprintf(fp, "%x ", g);
234
+ }*/
235
+ }
236
+ /*fprintf(fp, "\n");*/
237
+ }
238
+ /*fclose(fp);*/
239
+ }
240
+ #define NO_REGIONS_DEFAULT 20
241
+ #define MIN_ID 1
242
+
243
+
244
+ static redeyeop_t *new_redeye(void)
245
+ {
246
+ redeyeop_t *ptr = ALLOC(redeyeop_t);
247
+ MEMZERO(ptr, redeyeop_t, 1);
248
+ return ptr;
249
+ }
250
+
251
+ static void free_redeye(redeyeop_t *ptr)
252
+ {
253
+ if (ptr->mask)
254
+ free(ptr->mask);
255
+ if (ptr->regions.data);
256
+ free(ptr->regions.data);
257
+ if (ptr->regions.region);
258
+ free(ptr->regions.region);
259
+ if (ptr->pixbuf)
260
+ g_object_unref(ptr->pixbuf);
261
+ if (ptr->preview)
262
+ g_object_unref(ptr->preview);
263
+ free(ptr);
264
+ }
265
+
266
+
267
+ inline gboolean in_region(redeyeop_t *op, int x, int y, int blob_id)
268
+ {
269
+ int index;
270
+
271
+ if ( x < op->area.minX || x > op->area.maxX ||
272
+ y < op->area.minY || y > op->area.maxY )
273
+ return FALSE;
274
+
275
+ index = (x - op->area.minX) + ((y - op->area.minY) * op->area.width);
276
+
277
+ return op->regions.data[index] == blob_id;
278
+ }
279
+
280
+ inline double alpha_level_for_pixel(redeyeop_t *op, int x, int y, int blob_id)
281
+ {
282
+ int j = 0, c = 0, xm, ym;
283
+
284
+ if (in_region(op, x, y, blob_id))
285
+ return 1.0;
286
+
287
+ for ( xm = -2; xm <= 2; xm++ )
288
+ {
289
+ for ( ym = -2; ym <= 2; ym ++ )
290
+ {
291
+ c ++;
292
+ if (xm == 0 && ym == 0)
293
+ continue;
294
+ if (in_region(op, x+xm, y+ym, blob_id))
295
+ j ++;
296
+ }
297
+ }
298
+
299
+ return ((double)j)/((double)c);
300
+ }
301
+
302
+ inline char col(double val)
303
+ {
304
+ if (val < 0) return 0;
305
+ if (val > 255) return 255;
306
+ return val;
307
+
308
+ }
309
+
310
+ static GdkPixbuf *redeye_preview(redeyeop_t *op, gboolean reset)
311
+ {
312
+ int width, height;
313
+ width = op->area.width;
314
+ height = op->area.height;
315
+
316
+ if (width + op->area.minX > gdk_pixbuf_get_width(op->pixbuf)) {
317
+ width = gdk_pixbuf_get_width(op->pixbuf) - op->area.minX;
318
+ }
319
+ if (height + op->area.minY > gdk_pixbuf_get_height(op->pixbuf)) {
320
+ height = gdk_pixbuf_get_height(op->pixbuf) - op->area.minY;
321
+ }
322
+
323
+ if ( op->preview == NULL )
324
+ {
325
+ GdkPixbuf *sub = NULL;
326
+ sub = gdk_pixbuf_new_subpixbuf(op->pixbuf, op->area.minX, op->area.minY,
327
+ width, height);
328
+
329
+ op->preview = gdk_pixbuf_copy(sub);
330
+ g_object_unref(sub);
331
+ } else if (reset) {
332
+ gdk_pixbuf_copy_area(op->pixbuf, op->area.minX, op->area.minY,
333
+ width, height, op->preview, 0, 0);
334
+ }
335
+
336
+ return op->preview;
337
+ }
338
+
339
+ static void desaturate_blob(redeyeop_t *op, int blob_id)
340
+ {
341
+ int y, x;
342
+ int minX, minY, maxX, maxY;
343
+
344
+ minY = MAX(0, op->area.minY + op->regions.region[blob_id].minY - 1);
345
+ maxY = MIN(op->area.maxY + op->regions.region[blob_id].maxY + 1,
346
+ gdk_pixbuf_get_height(op->pixbuf)-1);
347
+ minX = MAX(0, op->area.minX + op->regions.region[blob_id].minX - 1);
348
+ maxX = MIN(op->area.maxX + op->regions.region[blob_id].maxX + 1,
349
+ gdk_pixbuf_get_width(op->pixbuf)-1);
350
+
351
+ guchar *data = gdk_pixbuf_get_pixels(op->pixbuf);
352
+ int rowstride = gdk_pixbuf_get_rowstride(op->pixbuf);
353
+ int pixWidth = gdk_pixbuf_get_has_alpha(op->pixbuf) ? 4 : 3;
354
+
355
+ for ( y = minY; y <= maxY; y++ )
356
+ {
357
+ guchar *thisLine = data + (rowstride * y);
358
+ guchar *pixel;
359
+
360
+ pixel = thisLine + (minX * pixWidth);
361
+
362
+ for ( x = minX; x <= maxX; x++ )
363
+ {
364
+
365
+ double alpha = alpha_level_for_pixel(op, x, y, blob_id);
366
+ int r,g,b,grey;
367
+
368
+ r = pixel[0];
369
+ g = pixel[1];
370
+ b = pixel[2];
371
+
372
+ if (alpha > 0)
373
+ {
374
+ grey = alpha * ((double)( 5 * (double)r + 60 * (double)g + 30 * (double)b)) / 100.0 +
375
+ (1 - alpha) * r;
376
+
377
+ pixel[0] = col((grey * alpha) + (1-alpha) * r);
378
+ pixel[1] = col((grey * alpha) + (1-alpha) * g);
379
+ pixel[2] = col((grey * alpha) + (1-alpha) * b);
380
+ }
381
+
382
+ pixel += pixWidth;
383
+ }
384
+ }
385
+
386
+ }
387
+
388
+ static void highlight_blob(redeyeop_t *op, int blob_id, int colour)
389
+ {
390
+ int y, x;
391
+ int minX, minY, maxX, maxY;
392
+ int hr, hg, hb;
393
+
394
+ hr = (colour >> 16) & 0xff;
395
+ hg = (colour >> 8) & 0xff;
396
+ hb = (colour) & 0xff;
397
+
398
+ minY = MAX(0, op->area.minY - 1);
399
+ maxY = MIN(op->area.maxY + 1, gdk_pixbuf_get_height(op->pixbuf)-1);
400
+ minX = MAX(0, op->area.minX - 1);
401
+ maxX = MIN(op->area.maxX + 1, gdk_pixbuf_get_width(op->pixbuf)-1);
402
+
403
+ guchar *data = gdk_pixbuf_get_pixels(op->pixbuf);
404
+ int rowstride = gdk_pixbuf_get_rowstride(op->pixbuf);
405
+ int pixWidth = gdk_pixbuf_get_has_alpha(op->pixbuf) ? 4 : 3;
406
+
407
+ for ( y = minY; y <= maxY; y++ )
408
+ {
409
+ guchar *thisLine = data + (rowstride * y);
410
+ guchar *pixel;
411
+
412
+ pixel = thisLine + (minX * pixWidth);
413
+
414
+ for ( x = minX; x <= maxX; x++ )
415
+ {
416
+
417
+ double alpha = alpha_level_for_pixel(op, x, y, blob_id);
418
+ int r,g,b;
419
+
420
+ r = (pixel[0]);
421
+ g = (pixel[1]);
422
+ b = (pixel[2]);
423
+
424
+
425
+ if (alpha > 0)
426
+ {
427
+
428
+ pixel[0] = col((1-alpha) * r + (alpha * hr));
429
+ pixel[1] = col((1-alpha) * g + (alpha * hg));
430
+ pixel[2] = col((1-alpha) * b + (alpha * hb));
431
+ }
432
+
433
+ pixel += pixWidth;
434
+ }
435
+ }
436
+
437
+ }
438
+
439
+
440
+ static void preview_blob(redeyeop_t *op, int blob_id, int colour, gboolean reset_preview)
441
+ {
442
+ int y, x;
443
+ int minX, minY, maxX, maxY;
444
+ int hr, hg, hb;
445
+
446
+ redeye_preview(op, reset_preview);
447
+
448
+ hr = (colour >> 16) & 0xff;
449
+ hg = (colour >> 8) & 0xff;
450
+ hb = (colour) & 0xff;
451
+
452
+ minY = 0;
453
+ maxY = gdk_pixbuf_get_height(op->preview)-1;
454
+ minX = 0;
455
+ maxX = gdk_pixbuf_get_width(op->preview)-1;
456
+
457
+ guchar *data = gdk_pixbuf_get_pixels(op->preview);
458
+ int rowstride = gdk_pixbuf_get_rowstride(op->preview);
459
+ int pixWidth = gdk_pixbuf_get_has_alpha(op->preview) ? 4 : 3;
460
+
461
+ for ( y = minY; y <= maxY; y++ )
462
+ {
463
+ guchar *thisLine = data + (rowstride * y);
464
+ guchar *pixel;
465
+
466
+ pixel = thisLine + (minX * pixWidth);
467
+
468
+ for ( x = minX; x <= maxX; x++ )
469
+ {
470
+
471
+ double alpha = alpha_level_for_pixel(op, x + op->area.minX, y + op->area.minY, blob_id);
472
+ int r,g,b;
473
+
474
+ r = (pixel[0]);
475
+ g = (pixel[1]);
476
+ b = (pixel[2]);
477
+
478
+
479
+ if (alpha > 0)
480
+ {
481
+
482
+ pixel[0] = col((1-alpha) * r + (alpha * hr));
483
+ pixel[1] = col((1-alpha) * g + (alpha * hg));
484
+ pixel[2] = col((1-alpha) * b + (alpha * hb));
485
+ }
486
+
487
+ pixel += pixWidth;
488
+ }
489
+ }
490
+
491
+ }
492
+
493
+ %}
494
+
495
+ class RedEye
496
+ struct Region(op, id, minX, minY, maxX, maxY, width, height, noPixels)
497
+ def double:ratio
498
+ int width,height;
499
+ double min,max,ratio;
500
+ width = <{VALUE>int:rb_struct_getmember(self, rb_intern("width"))}>;
501
+ height = <{VALUE>int:rb_struct_getmember(self, rb_intern("height"))}>;
502
+ min = (double)MIN(width,height);
503
+ max = (double)MAX(width,height);
504
+ ratio = (min / max);
505
+ return ratio;
506
+ end
507
+ def double:density
508
+ int noPixels, width, height;
509
+ double density;
510
+
511
+ noPixels = <{VALUE>int:rb_struct_getmember(self, rb_intern("noPixels"))}>;
512
+ width = <{VALUE>int:rb_struct_getmember(self, rb_intern("width"))}>;
513
+ height = <{VALUE>int:rb_struct_getmember(self, rb_intern("height"))}>;
514
+ density = ((double)noPixels / (double)(width * height));
515
+
516
+ return density;
517
+ end
518
+ def gboolean:squareish?(double min_ratio = 0.5, double min_density = 0.5)
519
+ int noPixels, width, height;
520
+ double min, max, ratio, density;
521
+
522
+ noPixels = <{VALUE>int:rb_struct_getmember(self, rb_intern("noPixels"))}>;
523
+ width = <{VALUE>int:rb_struct_getmember(self, rb_intern("width"))}>;
524
+ height = <{VALUE>int:rb_struct_getmember(self, rb_intern("height"))}>;
525
+
526
+ min = (double)MIN(width,height);
527
+ max = (double)MAX(width,height);
528
+ ratio = (min / max);
529
+
530
+ density = ((double)noPixels / (double)(width * height));
531
+
532
+ return ((ratio >= min_ratio) && (density > min_density));
533
+ end
534
+ end
535
+
536
+ def __alloc__
537
+ return Data_Wrap_Struct(self, NULL, free_redeye, new_redeye());
538
+ end
539
+
540
+ def initialize(GdkPixbuf *pixbuf, int minX, int minY, int maxX, int maxY)
541
+ redeyeop_t *op;
542
+
543
+ Data_Get_Struct(self, redeyeop_t, op);
544
+
545
+ op->pixbuf = pixbuf;
546
+ op->preview = NULL;
547
+ g_object_ref(op->pixbuf);
548
+
549
+ op->area.minX = minX;
550
+ op->area.maxX = maxX;
551
+ op->area.minY = minY;
552
+ op->area.maxY = maxY;
553
+ op->area.width = maxX - minX + 1;
554
+ op->area.height = maxY - minY + 1;
555
+
556
+ assert(op->pixbuf != NULL);
557
+ assert(op->area.maxX <= gdk_pixbuf_get_width(op->pixbuf));
558
+ assert(op->area.minX >= 0);
559
+ assert(op->area.minX < op->area.maxX);
560
+ assert(op->area.maxY <= gdk_pixbuf_get_height(op->pixbuf));
561
+ assert(op->area.minY >= 0);
562
+ assert(op->area.minY < op->area.maxY);
563
+
564
+
565
+ op->mask = ALLOC_N(int, op->area.width * op->area.height);
566
+
567
+ op->regions.data = ALLOC_N(int, op->area.width * op->area.height);
568
+
569
+ op->regions.region = ALLOC_N(region_info, NO_REGIONS_DEFAULT);
570
+
571
+ op->regions.len = 0;
572
+ op->regions.size = NO_REGIONS_DEFAULT;
573
+ end
574
+
575
+ def identify_blobs(double green_sensitivity=2.0, double blue_sensitivity=0.0, int min_red_val = MIN_RED_VAL)
576
+ redeyeop_t *op;
577
+
578
+ Data_Get_Struct(self, redeyeop_t, op);
579
+
580
+ MEMZERO(op->mask, int, op->area.width * op->area.height);
581
+ MEMZERO(op->regions.data, int, op->area.width * op->area.height);
582
+
583
+ identify_possible_redeye_pixels(op, green_sensitivity, blue_sensitivity, min_red_val);
584
+ identify_blob_groupings(op);
585
+
586
+ volatile VALUE ary = rb_ary_new2(op->regions.len);
587
+ int i;
588
+ for (i = MIN_ID; i < op->regions.len; i++) {
589
+ region_info *r = &op->regions.region[i];
590
+ /* Ignore CCD noise */
591
+ if (r->noPixels < 2)
592
+ continue;
593
+ rb_ary_push(ary, rb_struct_new(structRegion, self, INT2NUM(i),
594
+ INT2NUM(r->minX), INT2NUM(r->minY), INT2NUM(r->maxX), INT2NUM(r->maxY),
595
+ INT2NUM(r->width), INT2NUM(r->height), INT2NUM(r->noPixels)));
596
+ }
597
+ return ary;
598
+ end
599
+
600
+ def correct_blob(int blob_id)
601
+ redeyeop_t *op;
602
+
603
+ Data_Get_Struct(self, redeyeop_t, op);
604
+
605
+ if (op->regions.len <= blob_id)
606
+ rb_raise(rb_eIndexError, "Only %i blobs in region - %i is invalid", op->regions.len, blob_id);
607
+
608
+
609
+ desaturate_blob(op, blob_id);
610
+ end
611
+
612
+ def highlight_blob(int blob_id, int col = 0x00ff00)
613
+ redeyeop_t *op;
614
+
615
+ Data_Get_Struct(self, redeyeop_t, op);
616
+
617
+ if (op->regions.len <= blob_id)
618
+ rb_raise(rb_eIndexError, "Only %i blobs in region - %i is invalid", op->regions.len, blob_id);
619
+
620
+ highlight_blob(op, blob_id, col);
621
+ end
622
+
623
+ def GdkPixbuf*:preview_blob(int blob_id, int col = 0x00ff00, gboolean reset_preview = TRUE)
624
+ redeyeop_t *op;
625
+
626
+ Data_Get_Struct(self, redeyeop_t, op);
627
+
628
+ if (op->regions.len <= blob_id)
629
+ rb_raise(rb_eIndexError, "Only %i blobs in region - %i is invalid", op->regions.len, blob_id);
630
+
631
+ preview_blob(op, blob_id, col, reset_preview);
632
+
633
+ return op->preview;
634
+ end
635
+ def GdkPixbuf*:preview
636
+ redeyeop_t *op;
637
+
638
+ Data_Get_Struct(self, redeyeop_t, op);
639
+
640
+ return redeye_preview(op, FALSE);
641
+ end
642
+ def GdkPixbuf*:pixbuf
643
+ redeyeop_t *op;
644
+
645
+ Data_Get_Struct(self, redeyeop_t, op);
646
+
647
+ return op->pixbuf;
648
+ end
649
+ end
650
+