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