63f08657f08c265d3642ce239c3132f8b2558247
[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 <libintl.h>
37 #define _(x) gettext(x)
38
39 #include "slide.h"
40 #include "narrative.h"
41 #include "stylesheet.h"
42 #include "imagestore.h"
43 #include "slide_render_cairo.h"
44 #include "narrative_render_cairo.h"
45 #include "render_cairo_common.h"
46
47 #include "narrative_priv.h"
48
49 const double dummy_h_val = 1024.0;
50
51
52 static double narrative_lcalc(struct length l, double pd)
53 {
54         if ( l.unit == LENGTH_UNIT ) {
55                 return l.len;
56         } else {
57                 return l.len * pd;
58         }
59 }
60
61
62 static void wrap_text(struct narrative_item *item, PangoContext *pc,
63                       Stylesheet *ss, const char *stn, double w,
64                       size_t sel_start, size_t sel_end)
65 {
66         PangoAlignment palignment;
67         PangoRectangle rect;
68         const char *font;
69         PangoFontDescription *fontdesc;
70         enum alignment align;
71         double wrap_w;
72         PangoAttrList *attrs;
73         PangoAttribute *attr;
74         struct colour fgcol;
75         guint16 r, g, b;
76
77         font = stylesheet_get_font(ss, stn, &fgcol, &align);
78         if ( font == NULL ) return;
79         fontdesc = pango_font_description_from_string(font);
80
81         if ( item->align == ALIGN_INHERIT ) {
82                 /* Use value from stylesheet */
83                 palignment = to_pangoalignment(align);
84         } else {
85                 /* Use item-specific value */
86                 palignment = to_pangoalignment(item->align);
87         }
88
89         /* Calculate width of actual text */
90         wrap_w = w - item->space_l - item->space_r;
91
92         /* Set foreground colour */
93         attrs = pango_attr_list_new();
94         r = fgcol.rgba[0] * 65535;
95         g = fgcol.rgba[1] * 65535;
96         b = fgcol.rgba[2] * 65535;
97         attr = pango_attr_foreground_new(r, g, b);
98         pango_attr_list_insert(attrs, attr);
99
100         /* Add attributes for selected text */
101         if ( sel_start > 0 || sel_end > 0 ) {
102                 PangoAttribute *attr;
103                 attr = pango_attr_background_new(42919, 58853, 65535);
104                 attr->start_index = sel_start;
105                 attr->end_index = sel_end;
106                 pango_attr_list_insert(attrs, attr);
107         }
108
109         if ( item->layout == NULL ) {
110                 item->layout = pango_layout_new(pc);
111         }
112         pango_layout_set_width(item->layout, pango_units_from_double(wrap_w));
113         pango_layout_set_alignment(item->layout, palignment);
114         pango_layout_set_font_description(item->layout, fontdesc);
115         pango_layout_set_attributes(item->layout, attrs);
116
117         if ( runs_to_pangolayout(item->layout, item->runs, item->n_runs) ) return;
118
119         pango_layout_get_extents(item->layout, NULL, &rect);
120         item->obj_w = pango_units_to_double(rect.width);
121         item->obj_h = pango_units_to_double(rect.height);
122
123         pango_attr_list_unref(attrs);
124 }
125
126
127 /* Render a thumbnail of the slide */
128 static cairo_surface_t *render_thumbnail(Slide *s, Stylesheet *ss, ImageStore *is,
129                                          int w, int h)
130 {
131         cairo_surface_t *surf;
132         cairo_surface_t *full_surf;
133         cairo_t *cr;
134         double logical_w, logical_h;
135         PangoContext *pc;
136         const int rh = 1024; /* "reasonably big" height for slide */
137         int rw;
138         struct slide_pos sel;
139
140         slide_get_logical_size(s, ss, &logical_w, &logical_h);
141         rw = rh*(logical_w/logical_h);
142
143         sel.para = 0;  sel.pos = 0;  sel.trail = 0;
144
145         /* Render at a reasonably big size.  Rendering to a small surface makes
146          * rounding of text positions (due to font hinting) cause significant
147          * differences between the thumbnail and "normal" rendering. */
148         full_surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, rw, rh);
149         cr = cairo_create(full_surf);
150         cairo_scale(cr, (double)rw/logical_w, (double)rh/logical_h);
151         pc = pango_cairo_create_context(cr);
152         slide_render_cairo(s, cr, is, ss, 0, pango_language_get_default(), pc,
153                            NULL, sel, sel);
154         g_object_unref(pc);
155         cairo_destroy(cr);
156
157         /* Scale down to the actual size of the thumbnail */
158         surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
159         cr = cairo_create(surf);
160         cairo_scale(cr, (double)w/rw, (double)h/rh);
161         cairo_set_source_surface(cr, full_surf, 0.0, 0.0);
162         cairo_paint(cr);
163         cairo_destroy(cr);
164
165         cairo_surface_destroy(full_surf);
166         return surf;
167 }
168
169
170 static void wrap_slide(struct narrative_item *item, Stylesheet *ss, ImageStore *is, int sel_block)
171 {
172         double w, h;
173
174         slide_get_logical_size(item->slide, ss, &w, &h);
175         item->obj_h = 320.0;  /* Actual height of thumbnail */
176         item->obj_w = rint(item->obj_h*w/h);
177
178         if ( item->slide_thumbnail != NULL ) {
179                 cairo_surface_destroy(item->slide_thumbnail);
180         }
181         item->slide_thumbnail = render_thumbnail(item->slide, ss, is,
182                                                  item->obj_w, item->obj_h);
183         item->selected = sel_block;
184 }
185
186
187 size_t narrative_pos_trail_to_offset(Narrative *n, int i, int offs, int trail)
188 {
189         int run;
190         glong char_offs;
191         char *ptr;
192         size_t run_offs;
193         struct narrative_item *item = &n->items[i];
194         int j;
195         size_t prev_len;
196
197         if ( !narrative_item_is_text(n, i) ) return offs;
198
199         run = narrative_which_run(item, offs, &run_offs);
200
201         char_offs = g_utf8_pointer_to_offset(item->runs[run].text,
202                                              item->runs[run].text+run_offs);
203         char_offs += trail;
204         ptr = g_utf8_offset_to_pointer(item->runs[run].text, char_offs);
205
206         prev_len = 0;
207         for ( j=0; j<run; j++ ) prev_len += strlen(item->runs[j].text);
208         return prev_len + ptr - item->runs[run].text;
209 }
210
211
212 static int positions_equal(struct edit_pos a, struct edit_pos b)
213 {
214         if ( a.para != b.para ) return 0;
215         if ( a.pos != b.pos ) return 0;
216         if ( a.trail != b.trail ) return 0;
217         return 1;
218 }
219
220
221 static void sort_positions(struct edit_pos *a, struct edit_pos *b)
222 {
223         if ( a->para > b->para ) {
224                 size_t tpos;
225                 int tpara, ttrail;
226                 tpara = b->para;   tpos = b->pos;  ttrail = b->trail;
227                 b->para = a->para;  b->pos = a->pos;  b->trail = a->trail;
228                 a->para = tpara;    a->pos = tpos;    a->trail = ttrail;
229         }
230
231         if ( (a->para == b->para) && (a->pos > b->pos) )
232         {
233                 size_t tpos = b->pos;
234                 int ttrail = b->trail;
235                 b->pos = a->pos;  b->trail = a->trail;
236                 a->pos = tpos;    a->trail = ttrail;
237         }
238 }
239
240
241 static void wrap_marker(struct narrative_item *item, PangoContext *pc, double w,
242                         int sel_block)
243 {
244         item->obj_w = w - item->space_l - item->space_r;
245         item->obj_h = 20.0;
246         item->selected = sel_block;
247 }
248
249
250 int narrative_wrap_range(Narrative *n, Stylesheet *stylesheet, PangoLanguage *lang,
251                          PangoContext *pc, double w, ImageStore *is,
252                          int min, int max,
253                          struct edit_pos sel_start, struct edit_pos sel_end)
254 {
255         int i;
256         struct length pad[4];
257         int sel_s, sel_e;
258         const char *stn;
259         struct length paraspace[4];
260
261         if ( stylesheet_get_padding(stylesheet, "NARRATIVE", pad) ) return 1;
262         n->space_l = narrative_lcalc(pad[0], w);
263         n->space_r = narrative_lcalc(pad[1], w);
264         n->space_t = narrative_lcalc(pad[2], dummy_h_val);
265         n->space_b = narrative_lcalc(pad[3], dummy_h_val);
266
267         n->w = w;
268         w -= n->space_l + n->space_r;
269
270         sort_positions(&sel_start, &sel_end);
271         if ( min < 0 ) min = 0;
272         if ( max >= n->n_items ) max = n->n_items-1;
273
274         if ( !positions_equal(sel_start, sel_end) ) {
275                 sel_s = narrative_pos_trail_to_offset(n, sel_start.para,
276                                                       sel_start.pos, sel_start.trail);
277                 sel_e = narrative_pos_trail_to_offset(n, sel_end.para,
278                                                       sel_end.pos, sel_end.trail);
279         } else {
280                 sel_s = 0;
281                 sel_e = 0;
282         }
283
284         for ( i=min; i<=max; i++ ) {
285
286                 size_t srt, end;
287                 int sel_block = 0;
288
289                 if ( i >= sel_start.para && i <= sel_end.para ) {
290                         if ( i == sel_start.para ) {
291                                 srt = sel_s;
292                         } else {
293                                 srt = 0;
294                         }
295                         if ( i == sel_end.para ) {
296                                 end = sel_e;
297                         } else {
298                                 end = G_MAXUINT;
299                         }
300                         if ( i > sel_start.para && i < sel_end.para ) {
301                                 end = G_MAXUINT;
302                         }
303                         sel_block = 1;
304                 } else {
305                         srt = 0;
306                         end = 0;
307                 }
308
309                 switch ( n->items[i].type ) {
310
311                         case NARRATIVE_ITEM_TEXT :
312                         stn = "NARRATIVE.TEXT";
313                         break;
314
315                         case NARRATIVE_ITEM_BP :
316                         stn = "NARRATIVE.BP";
317                         break;
318
319                         case NARRATIVE_ITEM_PRESTITLE :
320                         stn = "NARRATIVE.PRESTITLE";
321                         break;
322
323                         case NARRATIVE_ITEM_SLIDE :
324                         stn = "NARRATIVE.SLIDE";
325                         break;
326
327                         case NARRATIVE_ITEM_EOP :
328                         stn = "NARRATIVE.EOP";
329                         break;
330
331                         default :
332                         stn = NULL;
333                         break;
334                 }
335
336                 if ( stylesheet_get_paraspace(stylesheet, stn, paraspace) == 0 ) {
337                         n->items[i].space_l = narrative_lcalc(paraspace[0], w);
338                         n->items[i].space_r = narrative_lcalc(paraspace[1], w);
339                         n->items[i].space_t = narrative_lcalc(paraspace[2], dummy_h_val);
340                         n->items[i].space_b = narrative_lcalc(paraspace[3], dummy_h_val);
341                 }
342
343                 switch ( n->items[i].type ) {
344
345                         case NARRATIVE_ITEM_TEXT :
346                         case NARRATIVE_ITEM_BP :
347                         case NARRATIVE_ITEM_PRESTITLE :
348                         wrap_text(&n->items[i], pc, stylesheet,
349                                   stn, w, srt, end);
350                         break;
351
352                         case NARRATIVE_ITEM_SLIDE :
353                         wrap_slide(&n->items[i], stylesheet, is, sel_block);
354                         break;
355
356                         case NARRATIVE_ITEM_EOP :
357                         wrap_marker(&n->items[i], pc, w, sel_block);
358                         break;
359                 }
360         }
361
362         return 0;
363 }
364
365
366 double narrative_item_get_height(Narrative *n, int i)
367 {
368         return n->items[i].obj_h + n->items[i].space_t + n->items[i].space_b;
369 }
370
371
372 double narrative_get_height(Narrative *n)
373 {
374         int i;
375         double total = 0.0;
376         for ( i=0; i<n->n_items; i++ ) {
377                 total += narrative_item_get_height(n, i);
378         }
379         return total + n->space_t + n->space_b;
380 }
381
382
383 static void draw_slide(struct narrative_item *item, cairo_t *cr)
384 {
385         double x, y;
386
387         cairo_save(cr);
388         cairo_translate(cr, item->space_l, item->space_t);
389
390         x = 0.0;  y = 0.0;
391         cairo_user_to_device(cr, &x, &y);
392         x = rint(x);  y = rint(y);
393         cairo_device_to_user(cr, &x, &y);
394
395         if ( item->selected ) {
396                 cairo_rectangle(cr, x-5.0, y-5.0, item->obj_w+10.0, item->obj_h+10.0);
397                 cairo_set_source_rgb(cr, 0.655, 0.899, 1.0);
398                 cairo_fill(cr);
399         }
400
401         cairo_rectangle(cr, x, y, item->obj_w, item->obj_h);
402         cairo_set_source_surface(cr, item->slide_thumbnail, 0.0, 0.0);
403         cairo_fill(cr);
404
405         cairo_rectangle(cr, x+0.5, y+0.5, item->obj_w, item->obj_h);
406         cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
407         cairo_set_line_width(cr, 1.0);
408         cairo_stroke(cr);
409
410         cairo_restore(cr);
411 }
412
413
414 static void draw_marker(struct narrative_item *item, cairo_t *cr)
415 {
416         double x, y;
417         PangoLayout *layout;
418         PangoFontDescription *fontdesc;
419
420         cairo_save(cr);
421         cairo_translate(cr, item->space_l, item->space_t);
422
423         x = 0.0;  y = 0.0;
424         cairo_user_to_device(cr, &x, &y);
425         x = rint(x);  y = rint(y);
426         cairo_device_to_user(cr, &x, &y);
427
428         if ( item->selected ) {
429                 cairo_rectangle(cr, x-5.0, y-5.0, item->obj_w+10.0, item->obj_h+10.0);
430                 cairo_set_source_rgb(cr, 0.655, 0.899, 1.0);
431                 cairo_fill(cr);
432         }
433
434         cairo_rectangle(cr, x, y, item->obj_w, item->obj_h);
435         cairo_set_source_rgb(cr, 0.8, 0.0, 0.0);
436         cairo_fill(cr);
437
438         layout = pango_cairo_create_layout(cr);
439         pango_layout_set_text(layout, _("End of presentation"), -1);
440
441         fontdesc = pango_font_description_new();
442         pango_font_description_set_family_static(fontdesc, "Sans");
443         pango_font_description_set_style(fontdesc, PANGO_STYLE_ITALIC);
444         pango_font_description_set_absolute_size(fontdesc, 0.9*item->obj_h*PANGO_SCALE);
445         pango_layout_set_font_description(layout, fontdesc);
446         pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
447         pango_layout_set_width(layout, item->obj_w*PANGO_SCALE);
448
449         cairo_move_to(cr, 0.0, 0.0);
450         cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
451         pango_cairo_update_layout(cr, layout);
452         pango_cairo_show_layout(cr, layout);
453         cairo_fill(cr);
454
455         g_object_unref(layout);
456         pango_font_description_free(fontdesc);
457
458         cairo_restore(cr);
459 }
460
461
462 static void draw_text(struct narrative_item *item, cairo_t *cr)
463 {
464         if ( item->layout == NULL ) return;
465
466         cairo_save(cr);
467         cairo_translate(cr, item->space_l, item->space_t);
468
469         //if ( (hpos + cur_h > min_y) && (hpos < max_y) ) {
470                 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
471                 pango_cairo_update_layout(cr, item->layout);
472                 pango_cairo_show_layout(cr, item->layout);
473                 cairo_fill(cr);
474         //} /* else paragraph is not visible */
475
476         cairo_restore(cr);
477 }
478
479
480 int narrative_render_item_cairo(Narrative*n, cairo_t *cr, int i)
481 {
482         switch ( n->items[i].type ) {
483
484                 case NARRATIVE_ITEM_TEXT :
485                 case NARRATIVE_ITEM_PRESTITLE :
486                 draw_text(&n->items[i], cr);
487                 break;
488
489                 case NARRATIVE_ITEM_BP :
490                 draw_text(&n->items[i], cr);
491                 break;
492
493                 case NARRATIVE_ITEM_SLIDE :
494                 draw_slide(&n->items[i], cr);
495                 break;
496
497                 case NARRATIVE_ITEM_EOP :
498                 draw_marker(&n->items[i], cr);
499                 break;
500
501         }
502
503         return 0;
504 }
505
506
507 /* NB You must first call narrative_wrap() */
508 int narrative_render_cairo(Narrative *n, cairo_t *cr, Stylesheet *stylesheet)
509 {
510         int i, r;
511         enum gradient bg;
512         struct colour bgcol;
513         struct colour bgcol2;
514         cairo_pattern_t *patt = NULL;
515
516         r = stylesheet_get_background(stylesheet, "NARRATIVE", &bg, &bgcol, &bgcol2);
517         if ( r ) return 1;
518
519         /* Overall background */
520         cairo_rectangle(cr, 0.0, 0.0, n->w, narrative_get_height(n));
521         switch ( bg ) {
522
523                 case GRAD_NONE:
524                 cairo_set_source_rgb(cr, bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
525                 break;
526
527                 case GRAD_VERT:
528                 patt = cairo_pattern_create_linear(0.0, 0.0, 0.0, narrative_get_height(n));
529                 cairo_pattern_add_color_stop_rgb(patt, 0.0,
530                                                  bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
531                 cairo_pattern_add_color_stop_rgb(patt, 1.0,
532                                                  bgcol2.rgba[0], bgcol2.rgba[1], bgcol2.rgba[2]);
533                 cairo_set_source(cr, patt);
534                 break;
535
536                 case GRAD_HORIZ:
537                 patt = cairo_pattern_create_linear(0.0, 0.0, n->w, 0.0);
538                 cairo_pattern_add_color_stop_rgb(patt, 0.0,
539                                                  bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
540                 cairo_pattern_add_color_stop_rgb(patt, 1.0,
541                                                  bgcol2.rgba[0], bgcol2.rgba[1], bgcol2.rgba[2]);
542                 cairo_set_source(cr, patt);
543                 break;
544
545         }
546         cairo_fill(cr);
547
548         cairo_save(cr);
549         cairo_translate(cr, n->space_l, n->space_t);
550
551         for ( i=0; i<n->n_items; i++ ) {
552                 narrative_render_item_cairo(n, cr, i);
553                 cairo_translate(cr, 0.0, narrative_item_get_height(n, i));
554         }
555
556         cairo_restore(cr);
557
558         return 0;
559 }