Add end of presentation marker
[colloquium.git] / libstorycode / narrative_render_cairo.c
1 /*
2  * narrative_render_cairo.c
3  *
4  * Copyright © 2013-2019 Thomas White <taw@bitwiz.org.uk>
5  *
6  * This file is part of Colloquium.
7  *
8  * Colloquium is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20  *
21  */
22
23
24 #ifdef HAVE_CONFIG_H
25 #include <config.h>
26 #endif
27
28 #include <cairo.h>
29 #include <cairo-pdf.h>
30 #include <pango/pangocairo.h>
31 #include <assert.h>
32 #include <string.h>
33 #include <stdlib.h>
34 #include <math.h>
35
36 #include "slide.h"
37 #include "narrative.h"
38 #include "stylesheet.h"
39 #include "imagestore.h"
40 #include "slide_render_cairo.h"
41 #include "narrative_render_cairo.h"
42
43 #include "narrative_priv.h"
44
45 const double dummy_h_val = 1024.0;
46
47
48 static double lcalc(struct length l, double pd)
49 {
50         if ( l.unit == LENGTH_UNIT ) {
51                 return l.len;
52         } else {
53                 return l.len * pd;
54         }
55 }
56
57
58 static PangoAlignment to_pangoalignment(enum alignment align)
59 {
60         switch ( align ) {
61                 case ALIGN_LEFT : return PANGO_ALIGN_LEFT;
62                 case ALIGN_RIGHT : return PANGO_ALIGN_RIGHT;
63                 case ALIGN_CENTER : return PANGO_ALIGN_CENTER;
64                 default: return PANGO_ALIGN_LEFT;
65         }
66 }
67
68
69 static void wrap_text(struct narrative_item *item, PangoContext *pc,
70                       Stylesheet *ss, const char *stn, double w,
71                       size_t sel_start, size_t sel_end)
72 {
73         PangoAlignment palignment;
74         PangoRectangle rect;
75         const char *font;
76         PangoFontDescription *fontdesc;
77         enum alignment align;
78         double wrap_w;
79         PangoAttrList *attrs;
80         PangoAttribute *attr;
81         struct colour fgcol;
82         guint16 r, g, b;
83
84         font = stylesheet_get_font(ss, stn, &fgcol, &align);
85         if ( font == NULL ) return;
86         fontdesc = pango_font_description_from_string(font);
87
88         if ( item->align == ALIGN_INHERIT ) {
89                 /* Use value from stylesheet */
90                 palignment = to_pangoalignment(align);
91         } else {
92                 /* Use item-specific value */
93                 palignment = to_pangoalignment(item->align);
94         }
95
96         /* Calculate width of actual text */
97         wrap_w = w - item->space_l - item->space_r;
98
99         /* Set foreground colour */
100         attrs = pango_attr_list_new();
101         r = fgcol.rgba[0] * 65535;
102         g = fgcol.rgba[1] * 65535;
103         b = fgcol.rgba[2] * 65535;
104         attr = pango_attr_foreground_new(r, g, b);
105         pango_attr_list_insert(attrs, attr);
106
107         /* Add attributes for selected text */
108         if ( sel_start > 0 || sel_end > 0 ) {
109                 PangoAttribute *attr;
110                 attr = pango_attr_background_new(42919, 58853, 65535);
111                 attr->start_index = sel_start;
112                 attr->end_index = sel_end;
113                 pango_attr_list_insert(attrs, attr);
114         }
115
116         if ( item->layout == NULL ) {
117                 item->layout = pango_layout_new(pc);
118         }
119         pango_layout_set_width(item->layout, pango_units_from_double(wrap_w));
120         pango_layout_set_text(item->layout, item->text, -1);
121         pango_layout_set_alignment(item->layout, palignment);
122         pango_layout_set_font_description(item->layout, fontdesc);
123         pango_layout_set_attributes(item->layout, attrs);
124
125         /* FIXME: Handle *bold*, _underline_, /italic/ etc. */
126         //pango_layout_set_attributes(item->layout, attrs);
127         //pango_attr_list_unref(attrs);
128
129         pango_layout_get_extents(item->layout, NULL, &rect);
130         item->obj_w = pango_units_to_double(rect.width);
131         item->obj_h = pango_units_to_double(rect.height);
132 }
133
134
135 /* Render a thumbnail of the slide */
136 static cairo_surface_t *render_thumbnail(Slide *s, Stylesheet *ss, ImageStore *is,
137                                          int w, int h)
138 {
139         cairo_surface_t *surf;
140         cairo_surface_t *full_surf;
141         cairo_t *cr;
142         double logical_w, logical_h;
143         PangoContext *pc;
144         const int rh = 1024; /* "reasonably big" height for slide */
145         int rw;
146         struct slide_pos sel;
147
148         slide_get_logical_size(s, ss, &logical_w, &logical_h);
149         rw = rh*(logical_w/logical_h);
150
151         sel.para = 0;  sel.pos = 0;  sel.trail = 0;
152
153         /* Render at a reasonably big size.  Rendering to a small surface makes
154          * rounding of text positions (due to font hinting) cause significant
155          * differences between the thumbnail and "normal" rendering. */
156         full_surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, rw, rh);
157         cr = cairo_create(full_surf);
158         cairo_scale(cr, (double)rw/logical_w, (double)rh/logical_h);
159         pc = pango_cairo_create_context(cr);
160         slide_render_cairo(s, cr, is, ss, 0, pango_language_get_default(), pc,
161                            NULL, sel, sel);
162         g_object_unref(pc);
163         cairo_destroy(cr);
164
165         /* Scale down to the actual size of the thumbnail */
166         surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
167         cr = cairo_create(surf);
168         cairo_scale(cr, (double)w/rw, (double)h/rh);
169         cairo_set_source_surface(cr, full_surf, 0.0, 0.0);
170         cairo_paint(cr);
171         cairo_destroy(cr);
172
173         cairo_surface_destroy(full_surf);
174         return surf;
175 }
176
177
178 static void wrap_slide(struct narrative_item *item, Stylesheet *ss, ImageStore *is, int sel_block)
179 {
180         double w, h;
181
182         slide_get_logical_size(item->slide, ss, &w, &h);
183         item->obj_h = 320.0;  /* Actual height of thumbnail */
184         item->obj_w = rint(item->obj_h*w/h);
185
186         if ( item->slide_thumbnail != NULL ) {
187                 cairo_surface_destroy(item->slide_thumbnail);
188         }
189         item->slide_thumbnail = render_thumbnail(item->slide, ss, is,
190                                                  item->obj_w, item->obj_h);
191         item->selected = sel_block;
192 }
193
194
195 static size_t pos_trail_to_offset(Narrative *n, int i, int offs, int trail)
196 {
197         glong char_offs;
198         char *ptr;
199         struct narrative_item *item = &n->items[i];
200
201         if ( !narrative_item_is_text(n, i) ) return offs;
202
203         char_offs = g_utf8_pointer_to_offset(item->text, item->text+offs);
204         char_offs += trail;
205         ptr = g_utf8_offset_to_pointer(item->text, char_offs);
206         return ptr - item->text;
207 }
208
209
210 static int positions_equal(struct edit_pos a, struct edit_pos b)
211 {
212         if ( a.para != b.para ) return 0;
213         if ( a.pos != b.pos ) return 0;
214         if ( a.trail != b.trail ) return 0;
215         return 1;
216 }
217
218
219 static void sort_positions(struct edit_pos *a, struct edit_pos *b)
220 {
221         if ( a->para > b->para ) {
222                 size_t tpos;
223                 int tpara, ttrail;
224                 tpara = b->para;   tpos = b->pos;  ttrail = b->trail;
225                 b->para = a->para;  b->pos = a->pos;  b->trail = a->trail;
226                 a->para = tpara;    a->pos = tpos;    a->trail = ttrail;
227         }
228
229         if ( (a->para == b->para) && (a->pos > b->pos) )
230         {
231                 size_t tpos = b->pos;
232                 int ttrail = b->trail;
233                 b->pos = a->pos;  b->trail = a->trail;
234                 a->pos = tpos;    a->trail = ttrail;
235         }
236 }
237
238
239 static void wrap_marker(struct narrative_item *item, PangoContext *pc, double w,
240                         int sel_block)
241 {
242         item->obj_w = w - item->space_l - item->space_r;
243         item->obj_h = 20.0;
244         item->selected = sel_block;
245 }
246
247
248 int narrative_wrap_range(Narrative *n, Stylesheet *stylesheet, PangoLanguage *lang,
249                          PangoContext *pc, double w, ImageStore *is,
250                          int min, int max,
251                          struct edit_pos sel_start, struct edit_pos sel_end)
252 {
253         int i;
254         struct length pad[4];
255         int sel_s, sel_e;
256         const char *stn;
257         struct length paraspace[4];
258
259         if ( stylesheet_get_padding(stylesheet, "NARRATIVE", pad) ) return 1;
260         n->space_l = lcalc(pad[0], w);
261         n->space_r = lcalc(pad[1], w);
262         n->space_t = lcalc(pad[2], dummy_h_val);
263         n->space_b = lcalc(pad[3], dummy_h_val);
264
265         n->w = w;
266         w -= n->space_l + n->space_r;
267
268         sort_positions(&sel_start, &sel_end);
269         if ( min < 0 ) min = 0;
270         if ( max >= n->n_items ) max = n->n_items-1;
271
272         if ( !positions_equal(sel_start, sel_end) ) {
273                 sel_s = pos_trail_to_offset(n, sel_start.para, sel_start.pos, sel_start.trail);
274                 sel_e = pos_trail_to_offset(n, sel_end.para, sel_end.pos, sel_end.trail);
275         } else {
276                 sel_s = 0;
277                 sel_e = 0;
278         }
279
280         for ( i=min; i<=max; i++ ) {
281
282                 size_t srt, end;
283                 int sel_block = 0;
284
285                 if ( i >= sel_start.para && i <= sel_end.para ) {
286                         if ( i == sel_start.para ) {
287                                 srt = sel_s;
288                         } else {
289                                 srt = 0;
290                         }
291                         if ( i == sel_end.para ) {
292                                 end = sel_e;
293                         } else {
294                                 end = G_MAXUINT;
295                         }
296                         if ( i > sel_start.para && i < sel_end.para ) {
297                                 end = G_MAXUINT;
298                         }
299                         sel_block = 1;
300                 } else {
301                         srt = 0;
302                         end = 0;
303                 }
304
305                 switch ( n->items[i].type ) {
306
307                         case NARRATIVE_ITEM_TEXT :
308                         stn = "NARRATIVE.TEXT";
309                         break;
310
311                         case NARRATIVE_ITEM_BP :
312                         stn = "NARRATIVE.BP";
313                         break;
314
315                         case NARRATIVE_ITEM_PRESTITLE :
316                         stn = "NARRATIVE.PRESTITLE";
317                         break;
318
319                         case NARRATIVE_ITEM_SLIDE :
320                         stn = "NARRATIVE.SLIDE";
321                         break;
322
323                         case NARRATIVE_ITEM_EOP :
324                         stn = "NARRATIVE.EOP";
325                         break;
326                 }
327
328                 if ( stylesheet_get_paraspace(stylesheet, stn, paraspace) == 0 ) {
329                         n->items[i].space_l = lcalc(paraspace[0], w);
330                         n->items[i].space_r = lcalc(paraspace[1], w);
331                         n->items[i].space_t = lcalc(paraspace[2], dummy_h_val);
332                         n->items[i].space_b = lcalc(paraspace[3], dummy_h_val);
333                 }
334
335                 switch ( n->items[i].type ) {
336
337                         case NARRATIVE_ITEM_TEXT :
338                         case NARRATIVE_ITEM_BP :
339                         case NARRATIVE_ITEM_PRESTITLE :
340                         wrap_text(&n->items[i], pc, stylesheet,
341                                   stn, w, srt, end);
342                         break;
343
344                         case NARRATIVE_ITEM_SLIDE :
345                         wrap_slide(&n->items[i], stylesheet, is, sel_block);
346                         break;
347
348                         case NARRATIVE_ITEM_EOP :
349                         wrap_marker(&n->items[i], pc, w, sel_block);
350                         break;
351                 }
352         }
353
354         return 0;
355 }
356
357
358 double narrative_item_get_height(Narrative *n, int i)
359 {
360         return n->items[i].obj_h + n->items[i].space_t + n->items[i].space_b;
361 }
362
363
364 double narrative_get_height(Narrative *n)
365 {
366         int i;
367         double total = 0.0;
368         for ( i=0; i<n->n_items; i++ ) {
369                 total += narrative_item_get_height(n, i);
370         }
371         return total + n->space_t + n->space_b;
372 }
373
374
375 static void draw_slide(struct narrative_item *item, cairo_t *cr)
376 {
377         double x, y;
378
379         cairo_save(cr);
380         cairo_translate(cr, item->space_l, item->space_t);
381
382         x = 0.0;  y = 0.0;
383         cairo_user_to_device(cr, &x, &y);
384         x = rint(x);  y = rint(y);
385         cairo_device_to_user(cr, &x, &y);
386
387         if ( item->selected ) {
388                 cairo_rectangle(cr, x-5.0, y-5.0, item->obj_w+10.0, item->obj_h+10.0);
389                 cairo_set_source_rgb(cr, 0.655, 0.899, 1.0);
390                 cairo_fill(cr);
391         }
392
393         cairo_rectangle(cr, x, y, item->obj_w, item->obj_h);
394         cairo_set_source_surface(cr, item->slide_thumbnail, 0.0, 0.0);
395         cairo_fill(cr);
396
397         cairo_rectangle(cr, x+0.5, y+0.5, item->obj_w, item->obj_h);
398         cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
399         cairo_set_line_width(cr, 1.0);
400         cairo_stroke(cr);
401
402         cairo_restore(cr);
403 }
404
405
406 static void draw_eop(struct narrative_item *item, cairo_t *cr)
407 {
408         double x, y;
409
410         cairo_save(cr);
411         cairo_translate(cr, item->space_l, item->space_t);
412
413         x = 0.0;  y = 0.0;
414         cairo_user_to_device(cr, &x, &y);
415         x = rint(x);  y = rint(y);
416         cairo_device_to_user(cr, &x, &y);
417
418         if ( item->selected ) {
419                 cairo_rectangle(cr, x-5.0, y-5.0, item->obj_w+10.0, item->obj_h+10.0);
420                 cairo_set_source_rgb(cr, 0.655, 0.899, 1.0);
421                 cairo_fill(cr);
422         }
423
424         cairo_rectangle(cr, x, y, item->obj_w, item->obj_h);
425         cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
426         cairo_fill(cr);
427
428         cairo_restore(cr);
429 }
430
431
432 static void draw_text(struct narrative_item *item, cairo_t *cr)
433 {
434         if ( item->layout == NULL ) return;
435
436         cairo_save(cr);
437         cairo_translate(cr, item->space_l, item->space_t);
438
439         //if ( (hpos + cur_h > min_y) && (hpos < max_y) ) {
440                 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
441                 pango_cairo_update_layout(cr, item->layout);
442                 pango_cairo_show_layout(cr, item->layout);
443                 cairo_fill(cr);
444         //} /* else paragraph is not visible */
445
446         cairo_restore(cr);
447 }
448
449
450 int narrative_render_item_cairo(Narrative*n, cairo_t *cr, int i)
451 {
452         switch ( n->items[i].type ) {
453
454                 case NARRATIVE_ITEM_TEXT :
455                 case NARRATIVE_ITEM_PRESTITLE :
456                 draw_text(&n->items[i], cr);
457                 break;
458
459                 case NARRATIVE_ITEM_BP :
460                 draw_text(&n->items[i], cr);
461                 break;
462
463                 case NARRATIVE_ITEM_SLIDE :
464                 draw_slide(&n->items[i], cr);
465                 break;
466
467                 case NARRATIVE_ITEM_EOP :
468                 draw_eop(&n->items[i], cr);
469                 break;
470
471         }
472
473         return 0;
474 }
475
476
477 /* NB You must first call narrative_wrap() */
478 int narrative_render_cairo(Narrative *n, cairo_t *cr, Stylesheet *stylesheet)
479 {
480         int i, r;
481         enum gradient bg;
482         struct colour bgcol;
483         struct colour bgcol2;
484         cairo_pattern_t *patt = NULL;
485
486         r = stylesheet_get_background(stylesheet, "NARRATIVE", &bg, &bgcol, &bgcol2);
487         if ( r ) return 1;
488
489         /* Overall background */
490         cairo_rectangle(cr, 0.0, 0.0, n->w, narrative_get_height(n));
491         switch ( bg ) {
492
493                 case GRAD_NONE:
494                 cairo_set_source_rgb(cr, bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
495                 break;
496
497                 case GRAD_VERT:
498                 patt = cairo_pattern_create_linear(0.0, 0.0, 0.0, narrative_get_height(n));
499                 cairo_pattern_add_color_stop_rgb(patt, 0.0,
500                                                  bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
501                 cairo_pattern_add_color_stop_rgb(patt, 1.0,
502                                                  bgcol2.rgba[0], bgcol2.rgba[1], bgcol2.rgba[2]);
503                 cairo_set_source(cr, patt);
504                 break;
505
506                 case GRAD_HORIZ:
507                 patt = cairo_pattern_create_linear(0.0, 0.0, n->w, 0.0);
508                 cairo_pattern_add_color_stop_rgb(patt, 0.0,
509                                                  bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
510                 cairo_pattern_add_color_stop_rgb(patt, 1.0,
511                                                  bgcol2.rgba[0], bgcol2.rgba[1], bgcol2.rgba[2]);
512                 cairo_set_source(cr, patt);
513                 break;
514
515         }
516         cairo_fill(cr);
517
518         cairo_save(cr);
519         cairo_translate(cr, n->space_l, n->space_t);
520
521         for ( i=0; i<n->n_items; i++ ) {
522                 narrative_render_item_cairo(n, cr, i);
523                 cairo_translate(cr, 0.0, narrative_item_get_height(n, i));
524         }
525
526         cairo_restore(cr);
527
528         return 0;
529 }