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