Take paraspace into account in cursor calculations
[colloquium.git] / libstorycode / slide_render_cairo.c
1 /*
2  * render.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 "render_cairo_common.h"
45
46 #include "slide_priv.h"
47
48
49 static int slide_positions_equal(struct slide_pos a, struct slide_pos b)
50 {
51         if ( a.para != b.para ) return 0;
52         if ( a.pos != b.pos ) return 0;
53         if ( a.trail != b.trail ) return 0;
54         return 1;
55 }
56
57
58 static int slide_which_run(struct slide_text_paragraph *para, size_t item_offs,
59                            size_t *run_offs)
60 {
61         int run;
62         size_t pos = 0;
63
64         assert(para->n_runs > 0);
65
66         for ( run=0; run<para->n_runs; run++ ) {
67                 size_t npos = pos + strlen(para->runs[run].text);
68                 if ( npos >= item_offs ) break;
69                 pos = npos;
70         }
71         if ( run_offs != NULL ) {
72                 *run_offs = item_offs - pos;
73         }
74         return run;
75 }
76
77
78 static int slide_item_is_text(SlideItem *item)
79 {
80         switch ( item->type ) {
81
82                 case SLIDE_ITEM_TEXT : return 1;
83                 case SLIDE_ITEM_IMAGE : return 0;
84                 case SLIDE_ITEM_FOOTER : return 0;
85                 case SLIDE_ITEM_SLIDETITLE : return 1;
86                 case SLIDE_ITEM_PRESTITLE : return 1;
87
88                 default :
89                 fprintf(stderr, "Please update slide_item_is_text\n");
90                 return 0;
91         }
92 }
93
94
95 size_t slide_pos_trail_to_offset(SlideItem *item, int para_num, size_t offs, int trail)
96 {
97         glong char_offs;
98         char *ptr;
99         int run;
100         size_t run_offs;
101         size_t prev_len;
102         int j;
103         struct slide_text_paragraph *para;
104
105         if ( !slide_item_is_text(item) ) return offs;
106
107         para = &item->paras[para_num];
108         run = slide_which_run(para, offs, &run_offs);
109
110         char_offs = g_utf8_pointer_to_offset(para->runs[run].text,
111                                              para->runs[run].text+run_offs);
112         char_offs += trail;
113         ptr = g_utf8_offset_to_pointer(para->runs[run].text, char_offs);
114
115         prev_len = 0;
116         for ( j=0; j<run; j++ ) prev_len += strlen(para->runs[j].text);
117         return prev_len + ptr - para->runs[run].text;
118 }
119
120
121 static void do_background(Stylesheet *ss, const char *stn, cairo_t *cr,
122                           double x, double y, double w, double h)
123 {
124         enum gradient bg;
125         struct colour bgcol;
126         struct colour bgcol2;
127         cairo_pattern_t *patt = NULL;
128
129         if ( stylesheet_get_background(ss, stn, &bg, &bgcol, &bgcol2) ) return;
130
131         cairo_save(cr);
132         cairo_translate(cr, x, y);
133         cairo_rectangle(cr, 0.0, 0.0, w, h);
134
135         switch ( bg ) {
136
137                 case GRAD_NONE:
138                 cairo_set_source_rgba(cr, bgcol.rgba[0], bgcol.rgba[1],
139                                            bgcol.rgba[2], bgcol.rgba[3]);
140                 break;
141
142                 case GRAD_VERT:
143                 patt = cairo_pattern_create_linear(0.0, 0.0, 0.0, h);
144                 cairo_pattern_add_color_stop_rgba(patt, 0.0,
145                                                   bgcol.rgba[0], bgcol.rgba[1],
146                                                   bgcol.rgba[2], bgcol.rgba[3]);
147                 cairo_pattern_add_color_stop_rgba(patt, 1.0,
148                                                   bgcol2.rgba[0], bgcol2.rgba[1],
149                                                   bgcol2.rgba[2], bgcol2.rgba[3]);
150                 cairo_set_source(cr, patt);
151                 break;
152
153                 case GRAD_HORIZ:
154                 patt = cairo_pattern_create_linear(0.0, 0.0, w, 0.0);
155                 cairo_pattern_add_color_stop_rgba(patt, 0.0,
156                                                   bgcol.rgba[0], bgcol.rgba[1],
157                                                   bgcol.rgba[2], bgcol.rgba[3]);
158                 cairo_pattern_add_color_stop_rgba(patt, 1.0,
159                                                   bgcol2.rgba[0], bgcol2.rgba[1],
160                                                   bgcol2.rgba[2], bgcol2.rgba[3]);
161                 cairo_set_source(cr, patt);
162                 break;
163
164                 default:
165                 fprintf(stderr, "Unrecognised slide background type %i\n", bg);
166                 break;
167
168         }
169         cairo_fill(cr);
170         cairo_restore(cr);
171 }
172
173
174 static void render_text(SlideItem *item, cairo_t *cr, PangoContext *pc,
175                         Stylesheet *ss, const char *stn,
176                         double parent_w, double parent_h,
177                         struct slide_pos sel_start, struct slide_pos sel_end)
178 {
179         int i;
180         double x, y, w, h;
181         double pad_l, pad_r, pad_t;
182         struct length pad[4];
183         struct length paraspacel[4];
184         double paraspace[4];
185         const char *font;
186         enum alignment align;
187         struct colour fgcol;
188         PangoRectangle rect;
189         PangoFontDescription *fontdesc;
190         PangoAlignment palignment;
191         size_t sel_s, sel_e;
192         struct frame_geom geom;
193
194         if ( (item->type == SLIDE_ITEM_TEXT) || (item->type == SLIDE_ITEM_IMAGE) ) {
195                 geom = item->geom;
196         } else {
197                 stylesheet_get_geometry(ss, stn, &geom);
198         }
199         x = lcalc(geom.x, parent_w);
200         y = lcalc(geom.y, parent_h);
201         w = lcalc(geom.w, parent_w);
202         h = lcalc(geom.h, parent_h);
203
204         if ( stylesheet_get_padding(ss, stn, pad) ) return;
205         pad_l = lcalc(pad[0], parent_w);
206         pad_r = lcalc(pad[1], parent_w);
207         pad_t = lcalc(pad[2], parent_h);
208
209         font = stylesheet_get_font(ss, stn, &fgcol, &align);
210         if ( font == NULL ) return;
211
212         fontdesc = pango_font_description_from_string(font);
213
214         if ( item->align == ALIGN_INHERIT ) {
215                 /* Use value from stylesheet */
216                 palignment = to_pangoalignment(align);
217         } else {
218                 /* Use item-specific value */
219                 palignment = to_pangoalignment(item->align);
220         }
221
222         if ( !slide_positions_equal(sel_start, sel_end) ) {
223                 sel_s = slide_pos_trail_to_offset(item, sel_start.para,
224                                                   sel_start.pos, sel_start.trail);
225                 sel_e = slide_pos_trail_to_offset(item, sel_end.para,
226                                                   sel_end.pos, sel_end.trail);
227         } else {
228                 sel_s = 0;
229                 sel_e = 0;
230         }
231
232         do_background(ss, stn, cr, x, y, w, h);
233
234         if ( stylesheet_get_paraspace(ss, stn, paraspacel) == 0 ) {
235                 paraspace[0] = lcalc(paraspacel[0], w);
236                 paraspace[1] = lcalc(paraspacel[1], w);
237                 paraspace[2] = lcalc(paraspacel[2], h);
238                 paraspace[3] = lcalc(paraspacel[3], h);
239         } else {
240                 paraspace[0] = 0.0;
241                 paraspace[1] = 0.0;
242                 paraspace[2] = 0.0;
243                 paraspace[3] = 0.0;
244         }
245
246         cairo_save(cr);
247         cairo_translate(cr, x, y);
248         cairo_translate(cr, pad_l+paraspace[0], pad_t);
249
250         for ( i=0; i<item->n_paras; i++ ) {
251
252                 PangoAttrList *attrs;
253
254                 size_t srt, end;
255                 if ( i >= sel_start.para && i <= sel_end.para ) {
256                         if ( i == sel_start.para ) {
257                                 srt = sel_s;
258                         } else {
259                                 srt = 0;
260                         }
261                         if ( i == sel_end.para ) {
262                                 end = sel_e;
263                         } else {
264                                 end = G_MAXUINT;
265                         }
266                         if ( i > sel_start.para && i < sel_end.para ) {
267                                 end = G_MAXUINT;
268                         }
269                 } else {
270                         srt = 0;
271                         end = 0;
272                 }
273
274                 if ( item->paras[i].layout == NULL ) {
275                         item->paras[i].layout = pango_layout_new(pc);
276                 }
277                 pango_layout_set_width(item->paras[i].layout,
278                                        pango_units_from_double(w-pad_l-pad_r));
279                 pango_layout_set_alignment(item->paras[i].layout, palignment);
280                 pango_layout_set_font_description(item->paras[i].layout, fontdesc);
281
282                 attrs = pango_attr_list_new();
283
284                 if ( srt > 0 || end > 0 ) {
285                         PangoAttribute *attr;
286                         attr = pango_attr_background_new(42919, 58853, 65535);
287                         attr->start_index = srt;
288                         attr->end_index = end;
289                         pango_attr_list_insert(attrs, attr);
290                 }
291
292                 pango_layout_set_attributes(item->paras[i].layout, attrs);
293
294                 runs_to_pangolayout(item->paras[i].layout, item->paras[i].runs,
295                                     item->paras[i].n_runs);
296
297                 pango_attr_list_unref(attrs);
298
299                 /* FIXME: Clip to w,h */
300
301                 cairo_set_source_rgba(cr, fgcol.rgba[0], fgcol.rgba[1], fgcol.rgba[2],
302                                           fgcol.rgba[3]);
303                 cairo_translate(cr, 0.0, paraspace[2]);
304                 cairo_move_to(cr, 0.0, 0.0);
305                 pango_cairo_update_layout(cr, item->paras[i].layout);
306                 pango_cairo_show_layout(cr, item->paras[i].layout);
307                 pango_layout_get_extents(item->paras[i].layout, NULL, &rect);
308                 cairo_translate(cr, 0.0, pango_units_to_double(rect.height)+paraspace[3]);
309                 cairo_fill(cr);
310
311         }
312         cairo_restore(cr);
313 }
314
315
316 static void render_image(SlideItem *item, cairo_t *cr,
317                          Stylesheet *ss, ImageStore *is,
318                          double parent_w, double parent_h)
319 {
320         double x, y, w, h;
321         double wd, hd;
322         cairo_surface_t *surf;
323
324         x = lcalc(item->geom.x, parent_w);
325         y = lcalc(item->geom.y, parent_h);
326         w = lcalc(item->geom.w, parent_w);
327         h = lcalc(item->geom.h, parent_h);
328
329         wd = w;  hd = h;
330         cairo_user_to_device_distance(cr, &wd, &hd);
331         surf = lookup_image(is, item->filename, wd);
332         if ( surf == NULL ) return;
333
334         cairo_user_to_device(cr, &x, &y);
335         x = rint(x);  y = rint(y);
336         cairo_device_to_user(cr, &x, &y);
337
338         cairo_save(cr);
339         cairo_new_path(cr);
340         cairo_rectangle(cr, x, y, w, h);
341         cairo_translate(cr, x, y);
342         cairo_scale(cr, w/wd, h/hd);
343         cairo_set_source_surface(cr, surf, 0.0, 0.0);
344         cairo_pattern_t *patt = cairo_get_source(cr);
345         cairo_pattern_set_extend(patt, CAIRO_EXTEND_NONE);
346         cairo_pattern_set_filter(patt, CAIRO_FILTER_BEST);
347         cairo_fill(cr);
348         cairo_restore(cr);
349 }
350
351
352 static void sort_slide_positions(struct slide_pos *a, struct slide_pos *b)
353 {
354         if ( a->para > b->para ) {
355                 size_t tpos;
356                 int tpara, ttrail;
357                 tpara = b->para;   tpos = b->pos;  ttrail = b->trail;
358                 b->para = a->para;  b->pos = a->pos;  b->trail = a->trail;
359                 a->para = tpara;    a->pos = tpos;    a->trail = ttrail;
360         }
361
362         if ( (a->para == b->para) && (a->pos > b->pos) )
363         {
364                 size_t tpos = b->pos;
365                 int ttrail = b->trail;
366                 b->pos = a->pos;  b->trail = a->trail;
367                 a->pos = tpos;    a->trail = ttrail;
368         }
369 }
370
371
372 int slide_render_cairo(Slide *s, cairo_t *cr, ImageStore *is, Stylesheet *stylesheet,
373                        int slide_number, PangoLanguage *lang, PangoContext *pc,
374                        SlideItem *sel_item, struct slide_pos sel_start, struct slide_pos sel_end)
375 {
376         int i;
377         double w, h;
378
379         slide_get_logical_size(s, stylesheet, &w, &h);
380         sort_slide_positions(&sel_start, &sel_end);
381
382         /* Overall background */
383         do_background(stylesheet, "SLIDE", cr, 0.0, 0.0, w, h);
384
385         for ( i=0; i<s->n_items; i++ ) {
386
387                 struct slide_pos srt, end;
388
389                 if ( &s->items[i] != sel_item ) {
390                         srt.para = 0;  srt.pos = 0;  srt.trail = 0;
391                         end.para = 0;  end.pos = 0;  end.trail = 0;
392                 } else {
393                         srt = sel_start;  end = sel_end;
394                 }
395
396                 switch ( s->items[i].type ) {
397
398                         case SLIDE_ITEM_TEXT :
399                         render_text(&s->items[i], cr, pc, stylesheet, "SLIDE.TEXT",
400                                     w, h, srt, end);
401                         break;
402
403                         case SLIDE_ITEM_IMAGE :
404                         render_image(&s->items[i], cr, stylesheet, is,
405                                      w, h);
406                         break;
407
408                         case SLIDE_ITEM_SLIDETITLE :
409                         render_text(&s->items[i], cr, pc, stylesheet, "SLIDE.SLIDETITLE",
410                                     w, h, srt, end);
411                         break;
412
413                         case SLIDE_ITEM_PRESTITLE :
414                         render_text(&s->items[i], cr, pc, stylesheet, "SLIDE.PRESTITLE",
415                                     w, h, srt, end);
416                         break;
417
418                         default :
419                         break;
420
421                 }
422         }
423
424         return 0;
425 }
426
427
428 int render_slides_to_pdf(Narrative *n, ImageStore *is, const char *filename)
429 {
430         double w = 2048.0;
431         cairo_surface_t *surf;
432         cairo_t *cr;
433         int i;
434         PangoContext *pc;
435         struct slide_pos sel;
436
437         surf = cairo_pdf_surface_create(filename, w, w);
438         if ( cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS ) {
439                 fprintf(stderr, _("Couldn't create Cairo surface\n"));
440                 return 1;
441         }
442
443         cr = cairo_create(surf);
444         pc = pango_cairo_create_context(cr);
445
446         sel.para = 0;  sel.pos = 0;  sel.trail = 0;
447
448         for ( i=0; i<narrative_get_num_slides(n); i++ )
449         {
450                 Slide *s;
451                 double log_w, log_h;
452
453                 s = narrative_get_slide_by_number(n, i);
454                 slide_get_logical_size(s, narrative_get_stylesheet(n),
455                                        &log_w, &log_h);
456
457                 cairo_pdf_surface_set_size(surf, w, w*(log_h/log_w));
458
459                 cairo_save(cr);
460                 cairo_scale(cr, w/log_w, w/log_w);
461                 slide_render_cairo(s, cr, is, narrative_get_stylesheet(n),
462                                    i, pango_language_get_default(), pc,
463                                    NULL, sel, sel);
464                 cairo_show_page(cr);
465                 cairo_restore(cr);
466         }
467
468         g_object_unref(pc);
469         cairo_surface_finish(surf);
470         cairo_destroy(cr);
471
472         return 0;
473 }