1 /* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
2 /* Balsa E-Mail Client
3  * Copyright (C) 1997-2013 Stuart Parmenter and others
4  * Written by (C) Albrecht Dre� <albrecht.dress@arcor.de> 2007
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2, or (at your option)
9  * any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19  * 02111-1307, USA.
20  */
21 
22 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
23 # include "config.h"
24 #endif                          /* HAVE_CONFIG_H */
25 #include "balsa-print-object-text.h"
26 
27 #include <gtk/gtk.h>
28 #include <string.h>
29 #include <glib/gi18n.h>
30 #include "libbalsa.h"
31 #include "rfc2445.h"
32 #include "balsa-icons.h"
33 #include "balsa-print-object.h"
34 #include "balsa-print-object-decor.h"
35 #include "balsa-print-object-default.h"
36 
37 
38 typedef enum {
39     PHRASE_BF = 0,
40     PHRASE_EM,
41     PHRASE_UL,
42     PHRASE_TYPE_COUNT
43 } PhraseType;
44 
45 typedef struct {
46     PhraseType phrase_type;
47     guint start_index;
48     guint end_index;
49 } PhraseRegion;
50 
51 
52 /* object related functions */
53 static void balsa_print_object_text_class_init(BalsaPrintObjectTextClass * klass);
54 static void balsa_print_object_text_init(GTypeInstance * instance,
55 					 gpointer g_class);
56 static void balsa_print_object_text_destroy(GObject * self);
57 
58 static void balsa_print_object_text_draw(BalsaPrintObject * self,
59 					 GtkPrintContext * context,
60 					 cairo_t * cairo_ctx);
61 
62 static GList * collect_attrs(GList * all_attr, guint offset, guint len);
63 static PangoAttrList * phrase_list_to_pango(GList * phrase_list);
64 static GList * phrase_highlight(const gchar * buffer, gunichar tag_char,
65 				PhraseType tag_type, GList * phrase_list);
66 
67 
68 static BalsaPrintObjectClass *parent_class = NULL;
69 
70 
71 GType
balsa_print_object_text_get_type()72 balsa_print_object_text_get_type()
73 {
74     static GType balsa_print_object_text_type = 0;
75 
76     if (!balsa_print_object_text_type) {
77 	static const GTypeInfo balsa_print_object_text_info = {
78 	    sizeof(BalsaPrintObjectTextClass),
79 	    NULL,		/* base_init */
80 	    NULL,		/* base_finalize */
81 	    (GClassInitFunc) balsa_print_object_text_class_init,
82 	    NULL,		/* class_finalize */
83 	    NULL,		/* class_data */
84 	    sizeof(BalsaPrintObjectText),
85 	    0,			/* n_preallocs */
86 	    (GInstanceInitFunc) balsa_print_object_text_init
87 	};
88 
89 	balsa_print_object_text_type =
90 	    g_type_register_static(BALSA_TYPE_PRINT_OBJECT,
91 				   "BalsaPrintObjectText",
92 				   &balsa_print_object_text_info, 0);
93     }
94 
95     return balsa_print_object_text_type;
96 }
97 
98 
99 static void
balsa_print_object_text_class_init(BalsaPrintObjectTextClass * klass)100 balsa_print_object_text_class_init(BalsaPrintObjectTextClass * klass)
101 {
102     parent_class = g_type_class_ref(BALSA_TYPE_PRINT_OBJECT);
103     BALSA_PRINT_OBJECT_CLASS(klass)->draw =
104 	balsa_print_object_text_draw;
105     G_OBJECT_CLASS(klass)->finalize = balsa_print_object_text_destroy;
106 }
107 
108 
109 static void
balsa_print_object_text_init(GTypeInstance * instance,gpointer g_class)110 balsa_print_object_text_init(GTypeInstance * instance, gpointer g_class)
111 {
112     BalsaPrintObjectText *po = BALSA_PRINT_OBJECT_TEXT(instance);
113 
114     po->text = NULL;
115     po->attributes = NULL;
116 }
117 
118 
119 static void
balsa_print_object_text_destroy(GObject * self)120 balsa_print_object_text_destroy(GObject * self)
121 {
122     BalsaPrintObjectText *po = BALSA_PRINT_OBJECT_TEXT(self);
123 
124     g_list_foreach(po->attributes, (GFunc) g_free, NULL);
125     g_list_free(po->attributes);
126     g_free(po->text);
127 
128     G_OBJECT_CLASS(parent_class)->finalize(self);
129 }
130 
131 /* prepare a text/plain part, which gets
132  * - citation bars and colourisation of cited text (prefs dependant)
133  * - syntax highlighting (prefs dependant)
134  * - RFC 3676 "flowed" processing */
135 GList *
balsa_print_object_text_plain(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)136 balsa_print_object_text_plain(GList *list, GtkPrintContext * context,
137 			      LibBalsaMessageBody * body,
138 			      BalsaPrintSetup * psetup)
139 {
140 #if USE_GREGEX
141     GRegex *rex;
142 #else                           /* USE_GREGEX */
143     regex_t rex;
144 #endif                          /* USE_GREGEX */
145     gchar *textbuf;
146     PangoFontDescription *font;
147     gdouble c_at_x;
148     gdouble c_use_width;
149     guint first_page;
150     gchar * par_start;
151     gchar * eol;
152     gint par_len;
153 
154     /* set up the regular expression for qouted text */
155 #if USE_GREGEX
156     if (!(rex = balsa_quote_regex_new()))
157 #else                           /* USE_GREGEX */
158     if (regcomp(&rex, balsa_app.quote_regex, REG_EXTENDED) != 0)
159 #endif                          /* USE_GREGEX */
160 	return balsa_print_object_default(list, context, body, psetup);
161 
162     /* start on new page if less than 2 lines can be printed */
163     if (psetup->c_y_pos + 2 * P_TO_C(psetup->p_body_font_height) >
164 	psetup->c_height) {
165 	psetup->c_y_pos = 0;
166 	psetup->page_count++;
167     }
168     c_at_x = psetup->c_x0 + psetup->curr_depth * C_LABEL_SEP;
169     c_use_width = psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
170 
171     /* copy the text body to a buffer */
172     if (body->buffer)
173 	textbuf = g_strdup(body->buffer);
174     else
175 	libbalsa_message_body_get_content(body, &textbuf, NULL);
176 
177     /* fake an empty buffer if textbuf is NULL */
178     if (!textbuf)
179 	textbuf = g_strdup("");
180 
181     /* be sure the we have correct utf-8 stuff here... */
182     libbalsa_utf8_sanitize(&textbuf, balsa_app.convert_unknown_8bit, NULL);
183 
184     /* apply flowed if requested */
185     if (libbalsa_message_body_is_flowed(body)) {
186 	GString *flowed;
187 
188 	flowed =
189 	    libbalsa_process_text_rfc2646(textbuf, G_MAXINT, FALSE, FALSE,
190 					  FALSE,
191 					  libbalsa_message_body_is_delsp
192 					  (body));
193 	g_free(textbuf);
194 	textbuf = flowed->str;
195 	g_string_free(flowed, FALSE);
196     }
197 
198     /* get the font */
199     font = pango_font_description_from_string(balsa_app.print_body_font);
200 
201     /* loop over paragraphs */
202     par_start = textbuf;
203     eol = strchr(par_start, '\n');
204     par_len = eol ? eol - par_start : (gint) strlen(par_start);
205     while (*par_start) {
206 	GString *level_buf;
207 	guint curr_level;
208 	guint cite_level;
209 	GList *par_parts;
210 	GList *this_par_part;
211 	GList *attr_list;
212 	PangoLayout *layout;
213 	PangoAttrList *pango_attr_list;
214 	GArray *attr_offs;
215 	gdouble c_at_y;
216 
217 	level_buf = NULL;
218 	curr_level = 0;		/* just to make the compiler happy */
219 	do {
220 	    gchar *thispar;
221 	    guint cite_idx;
222 
223 	    thispar = g_strndup(par_start, par_len);
224 
225 	    /* get the cite level and strip off the prefix */
226 #if USE_GREGEX
227 	    if (libbalsa_match_regex
228 		(thispar, rex, &cite_level, &cite_idx))
229 #else                           /* USE_GREGEX */
230 	    if (libbalsa_match_regex
231 		(thispar, &rex, &cite_level, &cite_idx))
232 #endif                          /* USE_GREGEX */
233             {
234 		gchar *new;
235 
236 		new = thispar + cite_idx;
237 		if (g_unichar_isspace(g_utf8_get_char(new)))
238 		    new = g_utf8_next_char(new);
239 		new = g_strdup(new);
240 		g_free(thispar);
241 		thispar = new;
242 	    }
243 
244 	    /* glue paragraphs with the same cite level together */
245 	    if (!level_buf || (curr_level == cite_level)) {
246 		if (!level_buf) {
247 		    level_buf = g_string_new(thispar);
248 		    curr_level = cite_level;
249 		} else {
250 		    level_buf = g_string_append_c(level_buf, '\n');
251 		    level_buf = g_string_append(level_buf, thispar);
252 		}
253 
254 		par_start = eol ? eol + 1 : par_start + par_len;
255 		eol = strchr(par_start, '\n');
256 		par_len = eol ? eol - par_start : (gint) strlen(par_start);
257 	    }
258 
259 	    g_free(thispar);
260 	} while (*par_start && (curr_level == cite_level));
261 
262 	/* configure the layout so we can use Pango to split the text into pages */
263 	layout = gtk_print_context_create_pango_layout(context);
264 	pango_layout_set_font_description(layout, font);
265 	pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
266 	pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
267 
268 	/* leave place for the citation bars */
269 	pango_layout_set_width(layout,
270 			       C_TO_P(c_use_width - curr_level * C_LABEL_SEP));
271 
272 	/* highlight structured phrases if requested */
273 	if (balsa_app.print_highlight_phrases) {
274 	    attr_list =
275 		phrase_highlight(level_buf->str, '*', PHRASE_BF, NULL);
276 	    attr_list =
277 		phrase_highlight(level_buf->str, '/', PHRASE_EM, attr_list);
278 	    attr_list =
279 		phrase_highlight(level_buf->str, '_', PHRASE_UL, attr_list);
280 	} else
281 	    attr_list = NULL;
282 
283 	/* start on new page if less than one line can be printed */
284 	if (psetup->c_y_pos + P_TO_C(psetup->p_body_font_height) >
285 	    psetup->c_height) {
286 	    psetup->c_y_pos = 0;
287 	    psetup->page_count++;
288 	}
289 
290 	/* split paragraph if necessary */
291 	pango_attr_list = phrase_list_to_pango(attr_list);
292 	first_page = psetup->page_count - 1;
293 	c_at_y = psetup->c_y_pos;
294 	par_parts =
295 	    split_for_layout(layout, level_buf->str, pango_attr_list,
296 			     psetup, FALSE, &attr_offs);
297 	if (pango_attr_list)
298 	    pango_attr_list_unref(pango_attr_list);
299 	g_object_unref(G_OBJECT(layout));
300 	g_string_free(level_buf, TRUE);
301 
302 	/* each part is a new text object */
303 	this_par_part = par_parts;
304 	while (this_par_part) {
305 	    BalsaPrintObjectText *pot;
306 
307 	    pot = g_object_new(BALSA_TYPE_PRINT_OBJECT_TEXT, NULL);
308 	    g_assert(pot != NULL);
309 	    BALSA_PRINT_OBJECT(pot)->on_page = first_page++;
310 	    BALSA_PRINT_OBJECT(pot)->c_at_x = c_at_x;
311 	    BALSA_PRINT_OBJECT(pot)->c_at_y = psetup->c_y0 + c_at_y;
312 	    BALSA_PRINT_OBJECT(pot)->depth = psetup->curr_depth;
313 	    c_at_y = 0.0;
314 	    BALSA_PRINT_OBJECT(pot)->c_width = c_use_width;
315 	    /* note: height is calculated when the object is drawn */
316 	    pot->text = (gchar *) this_par_part->data;
317 	    pot->cite_level = curr_level;
318 	    pot->attributes =
319 		collect_attrs(attr_list,
320 			      g_array_index(attr_offs, guint, 0),
321 			      strlen(pot->text));
322 
323 	    list = g_list_append(list, pot);
324 	    g_array_remove_index(attr_offs, 0);
325 	    this_par_part = g_list_next(this_par_part);
326 	}
327 	if (attr_list) {
328 	    g_list_foreach(attr_list, (GFunc) g_free, NULL);
329 	    g_list_free(attr_list);
330 	}
331 	g_list_free(par_parts);
332 	g_array_free(attr_offs, TRUE);
333     }
334 
335     /* clean up */
336     pango_font_description_free(font);
337     g_free(textbuf);
338 #if USE_GREGEX
339     g_regex_unref(rex);
340 #endif                          /* USE_GREGEX */
341     return list;
342 }
343 
344 
345 /* prepare a text part which is simply printed "as is" without all the bells
346  * and whistles of text/plain (see above) */
347 GList *
balsa_print_object_text(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)348 balsa_print_object_text(GList *list, GtkPrintContext * context,
349 			LibBalsaMessageBody * body,
350 			BalsaPrintSetup * psetup)
351 {
352     gchar *textbuf;
353     PangoFontDescription *font;
354     gdouble c_at_x;
355     gdouble c_use_width;
356     guint first_page;
357     GList *par_parts;
358     GList *this_par_part;
359     PangoLayout *layout;
360     gdouble c_at_y;
361 
362     /* start on new page if less than 2 lines can be printed */
363     if (psetup->c_y_pos + 2 * P_TO_C(psetup->p_body_font_height) >
364 	psetup->c_height) {
365 	psetup->c_y_pos = 0;
366 	psetup->page_count++;
367     }
368     c_at_x = psetup->c_x0 + psetup->curr_depth * C_LABEL_SEP;
369     c_use_width = psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
370 
371     /* copy the text body to a buffer */
372     if (body->buffer)
373 	textbuf = g_strdup(body->buffer);
374     else
375 	libbalsa_message_body_get_content(body, &textbuf, NULL);
376 
377     /* fake an empty buffer if textbuf is NULL */
378     if (!textbuf)
379 	textbuf = g_strdup("");
380 
381     /* be sure the we have correct utf-8 stuff here... */
382     libbalsa_utf8_sanitize(&textbuf, balsa_app.convert_unknown_8bit, NULL);
383 
384     /* get the font */
385     font = pango_font_description_from_string(balsa_app.print_body_font);
386 
387     /* configure the layout so we can use Pango to split the text into pages */
388     layout = gtk_print_context_create_pango_layout(context);
389     pango_layout_set_font_description(layout, font);
390     pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
391     pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
392     pango_layout_set_width(layout, C_TO_P(c_use_width));
393 
394     /* split paragraph if necessary */
395     first_page = psetup->page_count - 1;
396     c_at_y = psetup->c_y_pos;
397     par_parts = split_for_layout(layout, textbuf, NULL, psetup, FALSE, NULL);
398     g_object_unref(G_OBJECT(layout));
399     pango_font_description_free(font);
400     g_free(textbuf);
401 
402     /* each part is a new text object */
403     this_par_part = par_parts;
404     while (this_par_part) {
405 	BalsaPrintObjectText *pot;
406 
407 	pot = g_object_new(BALSA_TYPE_PRINT_OBJECT_TEXT, NULL);
408 	g_assert(pot != NULL);
409 	BALSA_PRINT_OBJECT(pot)->on_page = first_page++;
410 	BALSA_PRINT_OBJECT(pot)->c_at_x = c_at_x;
411 	BALSA_PRINT_OBJECT(pot)->c_at_y = psetup->c_y0 + c_at_y;
412 	BALSA_PRINT_OBJECT(pot)->depth = psetup->curr_depth;
413 	c_at_y = 0.0;
414 	BALSA_PRINT_OBJECT(pot)->c_width = c_use_width;
415 	/* note: height is calculated when the object is drawn */
416 	pot->text = (gchar *) this_par_part->data;
417 	pot->cite_level = 0;
418 	pot->attributes = NULL;
419 
420 	list = g_list_append(list, pot);
421 	this_par_part = g_list_next(this_par_part);
422     }
423     g_list_free(par_parts);
424 
425     return list;
426 }
427 
428 
429 /* note: a vcard is an icon plus a series of labels/text, so this function actually
430  * returns a BalsaPrintObjectDefault... */
431 
432 #define ADD_VCARD_FIELD(buf, labwidth, layout, field, descr)		\
433     do {								\
434 	if (field) {							\
435 	    gint label_width = p_string_width_from_layout(layout, descr); \
436 	    if (label_width > labwidth)					\
437 		labwidth = label_width;					\
438 	    if ((buf)->len > 0)						\
439 		g_string_append_c(buf, '\n');				\
440 	    g_string_append_printf(buf, "%s\t%s", descr, field);	\
441 	}								\
442     } while(0)
443 
444 GList *
balsa_print_object_text_vcard(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)445 balsa_print_object_text_vcard(GList * list,
446 			      GtkPrintContext * context,
447 			      LibBalsaMessageBody * body,
448 			      BalsaPrintSetup * psetup)
449 {
450     BalsaPrintObjectDefault *pod;
451     BalsaPrintObject *po;
452     PangoFontDescription *header_font;
453     PangoLayout *test_layout;
454     PangoTabArray *tabs;
455     GString *desc_buf;
456     gdouble c_max_height;
457     LibBalsaAddress * addr = NULL;
458     gchar *textbuf;
459 
460     /* check if we can create an address from the body and fall back to default if
461     * this fails */
462     if (body->buffer)
463 	textbuf = g_strdup(body->buffer);
464     else
465 	libbalsa_message_body_get_content(body, &textbuf, NULL);
466     if (textbuf)
467         addr = libbalsa_address_new_from_vcard(textbuf, body->charset);
468     if (!addr) {
469 	g_free(textbuf);
470 	return balsa_print_object_text(list, context, body, psetup);
471     }
472 
473     /* proceed with the address information */
474     pod = g_object_new(BALSA_TYPE_PRINT_OBJECT_DEFAULT, NULL);
475     g_assert(pod != NULL);
476     po = BALSA_PRINT_OBJECT(pod);
477 
478     /* create the part */
479     po->depth = psetup->curr_depth;
480     po->c_width =
481 	psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
482 
483     /* get the stock contacts icon or the mime type icon on fail */
484     pod->pixbuf =
485 	gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
486 				 BALSA_PIXMAP_IDENTITY, 48,
487 				 GTK_ICON_LOOKUP_USE_BUILTIN, NULL);
488     if (!pod->pixbuf) {
489 	gchar *conttype = libbalsa_message_body_get_mime_type(body);
490 
491 	pod->pixbuf = libbalsa_icon_finder(NULL, conttype, NULL, NULL,
492                                            GTK_ICON_SIZE_DND);
493     }
494     pod->c_image_width = gdk_pixbuf_get_width(pod->pixbuf);
495     pod->c_image_height = gdk_pixbuf_get_height(pod->pixbuf);
496 
497 
498     /* create a layout for calculating the maximum label width */
499     header_font =
500 	pango_font_description_from_string(balsa_app.print_header_font);
501     test_layout = gtk_print_context_create_pango_layout(context);
502     pango_layout_set_font_description(test_layout, header_font);
503     pango_font_description_free(header_font);
504 
505     /* add fields from the address */
506     desc_buf = g_string_new("");
507     pod->p_label_width = 0;
508     ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
509 		    addr->full_name,    _("Full Name"));
510     ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
511 		    addr->nick_name,    _("Nick Name"));
512     ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
513 		    addr->first_name,   _("First Name"));
514     ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
515 		    addr->last_name,    _("Last Name"));
516     ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
517 		    addr->organization, _("Organization"));
518     if (addr->address_list)
519         ADD_VCARD_FIELD(desc_buf, pod->p_label_width, test_layout,
520 			(const gchar *) addr->address_list->data, _("Email Address"));
521     g_object_unref(addr);
522 
523     /* add a small space between label and value */
524     pod->p_label_width += C_TO_P(C_LABEL_SEP);
525 
526     /* configure the layout so we can calculate the text height */
527     pango_layout_set_indent(test_layout, -pod->p_label_width);
528     tabs =
529 	pango_tab_array_new_with_positions(1, FALSE, PANGO_TAB_LEFT,
530 					   pod->p_label_width);
531     pango_layout_set_tabs(test_layout, tabs);
532     pango_tab_array_free(tabs);
533     pango_layout_set_width(test_layout,
534 			   C_TO_P(po->c_width -
535 				  4 * C_LABEL_SEP - pod->c_image_width));
536     pango_layout_set_alignment(test_layout, PANGO_ALIGN_LEFT);
537     pod->c_text_height =
538 	P_TO_C(p_string_height_from_layout(test_layout, desc_buf->str));
539     pod->description = g_string_free(desc_buf, FALSE);
540 
541     /* check if we should move to the next page */
542     c_max_height = MAX(pod->c_text_height, pod->c_image_height);
543     if (psetup->c_y_pos + c_max_height > psetup->c_height) {
544 	psetup->c_y_pos = 0;
545 	psetup->page_count++;
546     }
547 
548     /* remember the extent */
549     po->on_page = psetup->page_count - 1;
550     po->c_at_x = psetup->c_x0 + po->depth * C_LABEL_SEP;
551     po->c_at_y = psetup->c_y0 + psetup->c_y_pos;
552     po->c_width = psetup->c_width - 2 * po->depth * C_LABEL_SEP;
553     po->c_height = c_max_height;
554 
555     /* adjust the y position */
556     psetup->c_y_pos += c_max_height;
557 
558     return g_list_append(list, po);
559 }
560 
561 
562 /* add a text/calendar object */
563 
564 #define ADD_VCAL_FIELD(buf, labwidth, layout, field, descr)		\
565     do {								\
566 	if (field) {							\
567 	    gint label_width = p_string_width_from_layout(layout, descr); \
568 	    if (label_width > labwidth)					\
569 		labwidth = label_width;					\
570 	    if ((buf)->len > 0)						\
571 		g_string_append_c(buf, '\n');				\
572 	    g_string_append_printf(buf, "%s\t%s", descr, field);	\
573 	}								\
574     } while(0)
575 
576 #define ADD_VCAL_DATE(buf, labwidth, layout, date, descr)               \
577     do {                                                                \
578         if (date != (time_t) -1) {                                      \
579             gchar * _dstr =                                             \
580                 libbalsa_date_to_utf8(&date, balsa_app.date_string);    \
581             ADD_VCAL_FIELD(buf, labwidth, layout, _dstr, descr);        \
582             g_free(_dstr);                                              \
583         }                                                               \
584     } while (0)
585 
586 #define ADD_VCAL_ADDRESS(buf, labwidth, layout, addr, descr)            \
587     do {                                                                \
588         if (addr) {                                                     \
589             gchar * _astr = libbalsa_vcal_attendee_to_str(addr);        \
590             ADD_VCAL_FIELD(buf, labwidth, layout, _astr, descr);        \
591             g_free(_astr);                                              \
592         }                                                               \
593     } while (0)
594 
595 
596 GList *
balsa_print_object_text_calendar(GList * list,GtkPrintContext * context,LibBalsaMessageBody * body,BalsaPrintSetup * psetup)597 balsa_print_object_text_calendar(GList * list,
598                                  GtkPrintContext * context,
599                                  LibBalsaMessageBody * body,
600                                  BalsaPrintSetup * psetup)
601 {
602     BalsaPrintObjectDefault *pod;
603     BalsaPrintObject *po;
604     PangoFontDescription *header_font;
605     PangoLayout *test_layout;
606     PangoTabArray *tabs;
607     GString *desc_buf;
608     LibBalsaVCal * vcal_obj;
609     GList * this_ev;
610     guint first_page;
611     GList *par_parts;
612     GList *this_par_part;
613     gdouble c_at_y;
614 
615     /* check if we can evaluate the body as calendar object and fall back
616      * to text if not */
617     if (!(vcal_obj = libbalsa_vcal_new_from_body(body)))
618 	return balsa_print_object_text(list, context, body, psetup);
619 
620     /* proceed with the address information */
621     pod = g_object_new(BALSA_TYPE_PRINT_OBJECT_DEFAULT, NULL);
622     g_assert(pod != NULL);
623     po = BALSA_PRINT_OBJECT(pod);
624 
625     /* create the part */
626     po->depth = psetup->curr_depth;
627     po->c_width =
628 	psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
629 
630     /* get the stock calendar icon or the mime type icon on fail */
631     pod->pixbuf =
632 	gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
633 				 "x-office-calendar", 48,
634 				 GTK_ICON_LOOKUP_USE_BUILTIN, NULL);
635     if (!pod->pixbuf) {
636 	gchar *conttype = libbalsa_message_body_get_mime_type(body);
637 
638 	pod->pixbuf = libbalsa_icon_finder(NULL, conttype, NULL, NULL,
639                                            GTK_ICON_SIZE_DND);
640     }
641     pod->c_image_width = gdk_pixbuf_get_width(pod->pixbuf);
642     pod->c_image_height = gdk_pixbuf_get_height(pod->pixbuf);
643 
644     /* move to the next page if the icon doesn't fit */
645     if (psetup->c_y_pos + pod->c_image_height > psetup->c_height) {
646 	psetup->c_y_pos = 0;
647 	psetup->page_count++;
648     }
649 
650     /* create a layout for calculating the maximum label width and for splitting
651      * the body (if necessary) */
652     header_font =
653 	pango_font_description_from_string(balsa_app.print_header_font);
654     test_layout = gtk_print_context_create_pango_layout(context);
655     pango_layout_set_font_description(test_layout, header_font);
656     pango_font_description_free(header_font);
657 
658     /* add fields from the events*/
659     desc_buf = g_string_new("");
660     pod->p_label_width = 0;
661     for (this_ev = vcal_obj->vevent; this_ev; this_ev = g_list_next(this_ev)) {
662         LibBalsaVEvent * event = (LibBalsaVEvent *) this_ev->data;
663 
664         if (desc_buf->len > 0)
665             g_string_append_c(desc_buf, '\n');
666         ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
667                        event->summary, _("Summary"));
668         ADD_VCAL_ADDRESS(desc_buf, pod->p_label_width, test_layout,
669                          event->organizer, _("Organizer"));
670         ADD_VCAL_DATE(desc_buf, pod->p_label_width, test_layout,
671                       event->start, _("Start"));
672         ADD_VCAL_DATE(desc_buf, pod->p_label_width, test_layout,
673                       event->end, _("End"));
674         ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
675                        event->location, _("Location"));
676         if (event->attendee) {
677             GList * att = event->attendee;
678             gchar * this_att;
679 
680             this_att =
681                 libbalsa_vcal_attendee_to_str(LIBBALSA_ADDRESS(att->data));
682             att = g_list_next(att);
683             ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
684                            this_att, att ? _("Attendees") : _("Attendee"));
685             g_free(this_att);
686             for (; att; att = g_list_next(att)) {
687                 this_att =
688                     libbalsa_vcal_attendee_to_str(LIBBALSA_ADDRESS(att->data));
689                 g_string_append_printf(desc_buf, "\n\t%s", this_att);
690                 g_free(this_att);
691             }
692         }
693         if (event->description) {
694             gchar ** desc_lines = g_strsplit(event->description, "\n", -1);
695             gint i;
696 
697             ADD_VCAL_FIELD(desc_buf, pod->p_label_width, test_layout,
698                            desc_lines[0], _("Description"));
699             for (i = 1; desc_lines[i]; i++)
700                 g_string_append_printf(desc_buf, "\n\t%s", desc_lines[i]);
701             g_strfreev(desc_lines);
702         }
703     }
704     g_object_unref(vcal_obj);
705 
706     /* add a small space between label and value */
707     pod->p_label_width += C_TO_P(C_LABEL_SEP);
708 
709     /* configure the layout so we can split the text */
710     pango_layout_set_indent(test_layout, -pod->p_label_width);
711     tabs =
712 	pango_tab_array_new_with_positions(1, FALSE, PANGO_TAB_LEFT,
713 					   pod->p_label_width);
714     pango_layout_set_tabs(test_layout, tabs);
715     pango_tab_array_free(tabs);
716     pango_layout_set_width(test_layout,
717 			   C_TO_P(po->c_width -
718 				  4 * C_LABEL_SEP - pod->c_image_width));
719     pango_layout_set_alignment(test_layout, PANGO_ALIGN_LEFT);
720 
721     /* split paragraph if necessary */
722     first_page = psetup->page_count - 1;
723     c_at_y = psetup->c_y_pos;
724     par_parts =
725         split_for_layout(test_layout, desc_buf->str, NULL, psetup, TRUE, NULL);
726     g_string_free(desc_buf, TRUE);
727 
728     /* set the parameters of the first part */
729     pod->description = (gchar *) par_parts->data;
730     pod->c_text_height =
731 	P_TO_C(p_string_height_from_layout(test_layout, pod->description));
732     po->on_page = first_page++;
733     po->c_at_x = psetup->c_x0 + po->depth * C_LABEL_SEP;
734     po->c_at_y = psetup->c_y0 + c_at_y;
735     po->c_height = MAX(pod->c_image_height, pod->c_text_height);
736     list = g_list_append(list, pod);
737 
738     /* add more parts */
739     for (this_par_part = g_list_next(par_parts); this_par_part;
740          this_par_part = g_list_next(this_par_part)) {
741         BalsaPrintObjectDefault * new_pod;
742         BalsaPrintObject *new_po;
743 
744         /* create a new object */
745         new_pod = g_object_new(BALSA_TYPE_PRINT_OBJECT_DEFAULT, NULL);
746         g_assert(new_pod != NULL);
747         new_po = BALSA_PRINT_OBJECT(new_pod);
748 
749         /* fill data */
750         new_pod->p_label_width = pod->p_label_width;
751         new_pod->c_image_width = pod->c_image_width;
752         new_pod->description = (gchar *) this_par_part->data;
753         new_pod->c_text_height =
754             P_TO_C(p_string_height_from_layout(test_layout, new_pod->description));
755         new_po->on_page = first_page++;
756         new_po->c_at_x = psetup->c_x0 + po->depth * C_LABEL_SEP;
757         new_po->c_at_y = psetup->c_y0;
758         new_po->c_height = new_pod->c_text_height;
759         new_po->depth = psetup->curr_depth;
760         new_po->c_width =
761             psetup->c_width - 2 * psetup->curr_depth * C_LABEL_SEP;
762 
763         /* append */
764         list = g_list_append(list, new_pod);
765     }
766     g_list_free(par_parts);
767     g_object_unref(G_OBJECT(test_layout));
768 
769     return list;
770 }
771 
772 
773 static void
balsa_print_object_text_draw(BalsaPrintObject * self,GtkPrintContext * context,cairo_t * cairo_ctx)774 balsa_print_object_text_draw(BalsaPrintObject * self,
775 			     GtkPrintContext * context,
776 			     cairo_t * cairo_ctx)
777 {
778     BalsaPrintObjectText *po;
779     PangoFontDescription *font;
780     gint p_height;
781     PangoLayout *layout;
782     PangoAttrList *attr_list;
783 
784     po = BALSA_PRINT_OBJECT_TEXT(self);
785     g_assert(po != NULL);
786 
787     /* prepare */
788     font = pango_font_description_from_string(balsa_app.print_body_font);
789     layout = gtk_print_context_create_pango_layout(context);
790     pango_layout_set_font_description(layout, font);
791     pango_font_description_free(font);
792     pango_layout_set_width(layout,
793 			   C_TO_P(self->c_width - po->cite_level * C_LABEL_SEP));
794     pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
795     pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
796     pango_layout_set_text(layout, po->text, -1);
797     if ((attr_list = phrase_list_to_pango(po->attributes))) {
798 	pango_layout_set_attributes(layout, attr_list);
799 	pango_attr_list_unref(attr_list);
800     }
801     pango_layout_get_size(layout, NULL, &p_height);
802     if (po->cite_level > 0) {
803 	cairo_save(cairo_ctx);
804 	if (balsa_app.print_highlight_cited) {
805 	    gint k = (po->cite_level - 1) % MAX_QUOTED_COLOR;
806 
807 	    cairo_set_source_rgb(cairo_ctx,
808 				 balsa_app.quoted_color[k].red,
809 				 balsa_app.quoted_color[k].green,
810 				 balsa_app.quoted_color[k].blue);
811 	}
812     }
813     cairo_move_to(cairo_ctx, self->c_at_x + po->cite_level * C_LABEL_SEP,
814 		  self->c_at_y);
815     pango_cairo_show_layout(cairo_ctx, layout);
816     g_object_unref(G_OBJECT(layout));
817     if (po->cite_level > 0) {
818 	guint n;
819 
820 	cairo_new_path(cairo_ctx);
821 	cairo_set_line_width(cairo_ctx, 1.0);
822 	for (n = 0; n < po->cite_level; n++) {
823 	    gdouble c_xpos = self->c_at_x + 0.5 + n * C_LABEL_SEP;
824 
825 	    cairo_move_to(cairo_ctx, c_xpos, self->c_at_y);
826 	    cairo_line_to(cairo_ctx, c_xpos, self->c_at_y + P_TO_C(p_height));
827 	}
828 	cairo_stroke(cairo_ctx);
829 	cairo_restore(cairo_ctx);
830     }
831 
832     self->c_height = P_TO_C(p_height);	/* needed to properly print borders */
833 }
834 
835 
836 #define UNICHAR_PREV(p)  g_utf8_get_char(g_utf8_prev_char(p))
837 
838 static GList *
phrase_highlight(const gchar * buffer,gunichar tag_char,PhraseType tag_type,GList * phrase_list)839 phrase_highlight(const gchar * buffer, gunichar tag_char,
840 		 PhraseType tag_type, GList * phrase_list)
841 {
842     gchar *utf_start;
843 
844     /* find the tag char in the text and scan the buffer for
845        <buffer start or whitespace><tag char><alnum><any text><alnum><tagchar>
846        <whitespace, punctuation or buffer end> */
847     utf_start = g_utf8_strchr(buffer, -1, tag_char);
848     while (utf_start) {
849 	gchar *s_next = g_utf8_next_char(utf_start);
850 
851 	if ((utf_start == buffer
852 	     || g_unichar_isspace(UNICHAR_PREV(utf_start)))
853 	    && *s_next != '\0'
854 	    && g_unichar_isalnum(g_utf8_get_char(s_next))) {
855 	    gchar *utf_end;
856 	    gchar *line_end;
857 	    gchar *e_next;
858 
859 	    /* found a proper start sequence - find the end or eject */
860 	    if (!(utf_end = g_utf8_strchr(s_next, -1, tag_char)))
861 		return phrase_list;
862 	    line_end = g_utf8_strchr(s_next, -1, '\n');
863 	    e_next = g_utf8_next_char(utf_end);
864 	    while (!g_unichar_isalnum(UNICHAR_PREV(utf_end)) ||
865 		   !(*e_next == '\0' ||
866 		     g_unichar_isspace(g_utf8_get_char(e_next)) ||
867 		     g_unichar_ispunct(g_utf8_get_char(e_next)))) {
868 		if (!(utf_end = g_utf8_strchr(e_next, -1, tag_char)))
869 		    return phrase_list;
870 		e_next = g_utf8_next_char(utf_end);
871 	    }
872 
873 	    /* append the attribute if there is no line break */
874 	    if (!line_end || line_end >= e_next) {
875 		PhraseRegion *new_region = g_new0(PhraseRegion, 1);
876 
877 		new_region->phrase_type = tag_type;
878 		new_region->start_index = utf_start - buffer;
879 		new_region->end_index = e_next - buffer;
880 		phrase_list = g_list_prepend(phrase_list, new_region);
881 
882 		/* set the next start properly */
883 		utf_start =
884 		    *e_next ? g_utf8_strchr(e_next, -1, tag_char) : NULL;
885 	    } else
886 		utf_start =
887 		    *s_next ? g_utf8_strchr(s_next, -1, tag_char) : NULL;
888 	} else
889 	    /* no start sequence, find the next start tag char */
890 	    utf_start =
891 		*s_next ? g_utf8_strchr(s_next, -1, tag_char) : NULL;
892     }
893 
894     return phrase_list;
895 }
896 
897 
898 static PangoAttrList *
phrase_list_to_pango(GList * phrase_list)899 phrase_list_to_pango(GList * phrase_list)
900 {
901     PangoAttrList *attr_list;
902     PangoAttribute *ph_attr[PHRASE_TYPE_COUNT];
903     gint n;
904 
905     if (!phrase_list)
906 	return NULL;
907 
908     attr_list = pango_attr_list_new();
909     ph_attr[PHRASE_BF] = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
910     ph_attr[PHRASE_EM] = pango_attr_style_new(PANGO_STYLE_ITALIC);
911     ph_attr[PHRASE_UL] = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
912 
913     while (phrase_list) {
914 	PhraseRegion *region = (PhraseRegion *) phrase_list->data;
915 	PangoAttribute *new_attr;
916 
917 	new_attr = pango_attribute_copy(ph_attr[region->phrase_type]);
918 	new_attr->start_index = region->start_index;
919 	new_attr->end_index = region->end_index;
920 	pango_attr_list_insert(attr_list, new_attr);
921 
922 	phrase_list = g_list_next(phrase_list);
923     }
924 
925     for (n = 0; n < PHRASE_TYPE_COUNT; n++)
926 	pango_attribute_destroy(ph_attr[n]);
927 
928     return attr_list;
929 }
930 
931 
932 static GList *
collect_attrs(GList * all_attr,guint offset,guint len)933 collect_attrs(GList * all_attr, guint offset, guint len)
934 {
935     GList *attr = NULL;
936 
937     while (all_attr) {
938 	PhraseRegion *region = (PhraseRegion *) all_attr->data;
939 
940 	if ((region->start_index >= offset
941 	     && region->start_index <= offset + len)
942 	    || (region->end_index >= offset
943 		&& region->end_index <= offset + len)) {
944 	    PhraseRegion *this_reg =
945 		g_memdup(region, sizeof(PhraseRegion));
946 
947 	    if (this_reg->start_index < offset)
948 		this_reg->start_index = 0;
949 	    else
950 		this_reg->start_index -= offset;
951 	    if (this_reg->end_index > offset + len)
952 		this_reg->end_index = len;
953 	    else
954 		this_reg->end_index -= offset;
955 	    attr = g_list_prepend(attr, this_reg);
956 	}
957 	all_attr = g_list_next(all_attr);
958     }
959 
960     return attr;
961 }
962