1
/*
2
 * Copyright © 2016 Adrian Johnson
3
 *
4
 * Permission is hereby granted, free of charge, to any person
5
 * obtaining a copy of this software and associated documentation
6
 * files (the "Software"), to deal in the Software without
7
 * restriction, including without limitation the rights to use, copy,
8
 * modify, merge, publish, distribute, sublicense, and/or sell copies
9
 * of the Software, and to permit persons to whom the Software is
10
 * furnished to do so, subject to the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
 * SOFTWARE.
23
 *
24
 * Author: Adrian Johnson <ajohnson@redneon.com>
25
 */
26

            
27
#include "cairo-test.h"
28

            
29
#include <stdio.h>
30
#include <string.h>
31
#include <stdlib.h>
32
#include <errno.h>
33
#include <fcntl.h>
34
#include <sys/stat.h>
35

            
36
#if HAVE_UNISTD_H
37
#include <unistd.h>
38
#endif
39

            
40
#ifdef HAVE_MMAP
41
#include <sys/mman.h>
42
#endif
43

            
44
#include <cairo.h>
45

            
46
#if CAIRO_HAS_PDF_SURFACE
47

            
48
#include <cairo-pdf.h>
49

            
50
/* This test checks PDF with
51
 * - tagged text
52
 * - hyperlinks
53
 * - document outline
54
 * - metadata
55
 * - thumbnails
56
 * - page labels
57
 */
58

            
59
#define BASENAME "pdf-tagged-text.out"
60

            
61
#define PAGE_WIDTH 595
62
#define PAGE_HEIGHT 842
63

            
64
#define HEADING1_SIZE 16
65
#define HEADING2_SIZE 14
66
#define HEADING3_SIZE 12
67
#define TEXT_SIZE 12
68
#define HEADING_HEIGHT 50
69
#define MARGIN 50
70

            
71
struct section {
72
    int level;
73
    const char *heading;
74
    int num_paragraphs;
75
};
76

            
77
static const struct section contents[] = {
78
    { 0, "Chapter 1",     1 },
79
    { 1, "Section 1.1",   4 },
80
    { 2, "Section 1.1.1", 3 },
81
    { 1, "Section 1.2",   2 },
82
    { 2, "Section 1.2.1", 4 },
83
    { 2, "Section 1.2.2", 4 },
84
    { 1, "Section 1.3",   2 },
85
    { 0, "Chapter 2",     1 },
86
    { 1, "Section 2.1",   4 },
87
    { 2, "Section 2.1.1", 3 },
88
    { 1, "Section 2.2",   2 },
89
    { 2, "Section 2.2.1", 4 },
90
    { 2, "Section 2.2.2", 4 },
91
    { 1, "Section 2.3",   2 },
92
    { 0, "Chapter 3",     1 },
93
    { 1, "Section 3.1",   4 },
94
    { 2, "Section 3.1.1", 3 },
95
    { 1, "Section 3.2",   2 },
96
    { 2, "Section 3.2.1", 4 },
97
    { 2, "Section 3.2.2", 4 },
98
    { 1, "Section 3.3",   2 },
99
    { 0, NULL }
100
};
101

            
102
static const char *ipsum_lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing"
103
    " elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
104
    " Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi"
105
    " ut aliquip ex ea commodo consequat. Duis aute irure dolor in"
106
    " reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla"
107
    " pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa"
108
    " qui officia deserunt mollit anim id est laborum.";
109

            
110
static const char *roman_numerals[] = {
111
    "i", "ii", "iii", "iv", "v"
112
};
113

            
114
#define MAX_PARAGRAPH_LINES 20
115

            
116
static const char *utf8_destination = "l\xc3\xa4nk";
117

            
118
static int paragraph_num_lines;
119
static char *paragraph_text[MAX_PARAGRAPH_LINES];
120
static double paragraph_height;
121
static double line_height;
122
static double y_pos;
123
static int outline_parents[10];
124
static int page_num;
125

            
126
static void
127
layout_paragraph (cairo_t *cr)
128
{
129
    char *text, *begin, *end, *prev_end;
130
    cairo_text_extents_t text_extents;
131
    cairo_font_extents_t font_extents;
132

            
133
    cairo_select_font_face (cr, "Serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
134
    cairo_set_font_size(cr, TEXT_SIZE);
135
    cairo_font_extents (cr, &font_extents);
136
    line_height = font_extents.height;
137
    paragraph_height = 0;
138
    paragraph_num_lines = 0;
139
    text = strdup (ipsum_lorem);
140
    begin = text;
141
    end = text;
142
    prev_end = end;
143
    while (*begin) {
144
	end = strchr(end, ' ');
145
	if (!end) {
146
	    paragraph_text[paragraph_num_lines++] = strdup (begin);
147
	    break;
148
	}
149
	*end = 0;
150
	cairo_text_extents (cr, begin, &text_extents);
151
	*end = ' ';
152
	if (text_extents.width + 2*MARGIN > PAGE_WIDTH) {
153
	    int len = prev_end - begin;
154
	    char *s = xmalloc (len);
155
	    memcpy (s, begin, len);
156
	    s[len-1] = 0;
157
	    paragraph_text[paragraph_num_lines++] = s;
158
	    begin = prev_end + 1;
159
	}
160
	prev_end = end;
161
	end++;
162
    }
163
    paragraph_height = line_height * (paragraph_num_lines + 1);
164
    free (text);
165
}
166

            
167
static void
168
draw_paragraph (cairo_t *cr)
169
{
170
    int i;
171

            
172
    cairo_select_font_face (cr, "Serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
173
    cairo_set_font_size(cr, TEXT_SIZE);
174
    cairo_tag_begin (cr, "P", NULL);
175
    for (i = 0; i < paragraph_num_lines; i++) {
176
	cairo_move_to (cr, MARGIN, y_pos);
177
	cairo_show_text (cr, paragraph_text[i]);
178
	y_pos += line_height;
179
    }
180
    cairo_tag_end (cr, "P");
181
    y_pos += line_height;
182
}
183

            
184
static void
185
draw_page_num (cairo_surface_t *surface, cairo_t *cr, const char *prefix, int num)
186
{
187
    char buf[100];
188

            
189
    buf[0] = 0;
190
    if (prefix)
191
	strcat (buf, prefix);
192

            
193
    if (num)
194
	sprintf (buf + strlen(buf), "%d", num);
195

            
196
    cairo_save (cr);
197
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
198
    cairo_set_font_size(cr, 12);
199
    cairo_set_source_rgb (cr, 0, 0, 0);
200
    cairo_move_to (cr, PAGE_WIDTH/2, PAGE_HEIGHT - MARGIN);
201
    cairo_show_text (cr, buf);
202
    cairo_restore (cr);
203
    cairo_pdf_surface_set_page_label (surface, buf);
204
}
205

            
206
static void
207
draw_contents (cairo_surface_t *surface, cairo_t *cr, const struct section *section)
208
{
209
    char *attrib;
210

            
211
    xasprintf (&attrib, "dest='%s'", section->heading);
212
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
213
    switch (section->level) {
214
	case 0:
215
	    cairo_set_font_size(cr, HEADING1_SIZE);
216
	    break;
217
	case 1:
218
	    cairo_set_font_size(cr, HEADING2_SIZE);
219
	    break;
220
	case 2:
221
	    cairo_set_font_size(cr, HEADING3_SIZE);
222
	    break;
223
    }
224

            
225
    if (y_pos + HEADING_HEIGHT + MARGIN > PAGE_HEIGHT) {
226
	cairo_show_page (cr);
227
	draw_page_num (surface, cr, roman_numerals[page_num++], 0);
228
	y_pos = MARGIN;
229
    }
230
    cairo_move_to (cr, MARGIN, y_pos);
231
    cairo_save (cr);
232
    cairo_set_source_rgb (cr, 0, 0, 1);
233
    cairo_tag_begin (cr, "TOCI", NULL);
234
    cairo_tag_begin (cr, "Reference", NULL);
235
    cairo_tag_begin (cr, CAIRO_TAG_LINK, attrib);
236
    cairo_show_text (cr, section->heading);
237
    cairo_tag_end (cr, CAIRO_TAG_LINK);
238
    cairo_tag_end (cr, "Reference");
239
    cairo_tag_end (cr, "TOCI");
240
    cairo_restore (cr);
241
    y_pos += HEADING_HEIGHT;
242
    free (attrib);
243
}
244

            
245
static void
246
draw_section (cairo_surface_t *surface, cairo_t *cr, const struct section *section)
247
{
248
    int flags, i;
249
    char *name_attrib;
250
    char *dest_attrib;
251

            
252
    cairo_tag_begin (cr, "Sect", NULL);
253
    xasprintf(&name_attrib, "name='%s'", section->heading);
254
    xasprintf(&dest_attrib, "dest='%s'", section->heading);
255
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
256
    if (section->level == 0) {
257
	cairo_show_page (cr);
258
	draw_page_num (surface, cr, NULL, page_num++);
259
	cairo_set_font_size(cr, HEADING1_SIZE);
260
	cairo_move_to (cr, MARGIN, MARGIN);
261
	cairo_tag_begin (cr, "H1", NULL);
262
	cairo_tag_begin (cr, CAIRO_TAG_DEST, name_attrib);
263
	cairo_show_text (cr, section->heading);
264
	cairo_tag_end (cr, CAIRO_TAG_DEST);
265
	cairo_tag_end (cr, "H1");
266
	y_pos = MARGIN + HEADING_HEIGHT;
267
	flags = CAIRO_PDF_OUTLINE_FLAG_BOLD | CAIRO_PDF_OUTLINE_FLAG_OPEN;
268
	outline_parents[0] = cairo_pdf_surface_add_outline (surface,
269
							    CAIRO_PDF_OUTLINE_ROOT,
270
							    section->heading,
271
							    dest_attrib,
272
							    flags);
273
    } else {
274
	if (section->level == 1) {
275
	    cairo_set_font_size(cr, HEADING2_SIZE);
276
	    flags = 0;
277
	} else {
278
	    cairo_set_font_size(cr, HEADING3_SIZE);
279
	    flags = CAIRO_PDF_OUTLINE_FLAG_ITALIC;
280
	}
281

            
282
	if (y_pos + HEADING_HEIGHT + paragraph_height + MARGIN > PAGE_HEIGHT) {
283
	    cairo_show_page (cr);
284
	    draw_page_num (surface, cr, NULL, page_num++);
285
	    y_pos = MARGIN;
286
	}
287
	cairo_move_to (cr, MARGIN, y_pos);
288
	if (section->level == 1)
289
	    cairo_tag_begin (cr, "H2", NULL);
290
	else
291
	    cairo_tag_begin (cr, "H3", NULL);
292
	cairo_tag_begin (cr, CAIRO_TAG_DEST, name_attrib);
293
	cairo_show_text (cr, section->heading);
294
	cairo_tag_end (cr, CAIRO_TAG_DEST);
295
	if (section->level == 1)
296
	    cairo_tag_end (cr, "H2");
297
	else
298
	    cairo_tag_end (cr, "H3");
299
	y_pos += HEADING_HEIGHT;
300
	outline_parents[section->level] = cairo_pdf_surface_add_outline (surface,
301
									 outline_parents[section->level - 1],
302
									 section->heading,
303
									 dest_attrib,
304
									 flags);
305
    }
306

            
307
    for (i = 0; i < section->num_paragraphs; i++) {
308
	if (y_pos + paragraph_height + MARGIN > PAGE_HEIGHT) {
309
	    cairo_show_page (cr);
310
	    draw_page_num (surface, cr, NULL, page_num++);
311
		y_pos = MARGIN;
312
	}
313
	draw_paragraph (cr);
314
    }
315
    cairo_tag_end (cr, "Sect");
316
    free (name_attrib);
317
    free (dest_attrib);
318
}
319

            
320
static void
321
draw_cover (cairo_surface_t *surface, cairo_t *cr)
322
{
323
    cairo_text_extents_t text_extents;
324
    char *attrib;
325
    cairo_rectangle_t url_box;
326
    const char *cairo_url = "https://www.cairographics.org/";
327
    const double url_box_margin = 20.0;
328

            
329
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='cover'  internal");
330
    cairo_tag_end (cr, CAIRO_TAG_DEST);
331

            
332
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
333
    cairo_set_font_size(cr, 16);
334
    cairo_move_to (cr, PAGE_WIDTH/3, 0.15*PAGE_HEIGHT);
335
    cairo_tag_begin (cr, "Span", NULL);
336
    cairo_show_text (cr, "PDF Features Test");
337
    cairo_tag_end (cr, "Span");
338

            
339
    /* Test URL link using "rect" attribute. The entire rectangle surrounding the URL should be a clickable link.  */
340
    cairo_move_to (cr, PAGE_WIDTH/3, 0.2*PAGE_HEIGHT);
341
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
342
    cairo_set_font_size(cr, TEXT_SIZE);
343
    cairo_set_source_rgb (cr, 0, 0, 1);
344
    cairo_show_text (cr, cairo_url);
345
    cairo_text_extents (cr, cairo_url, &text_extents);
346
    url_box.x = PAGE_WIDTH/3 - url_box_margin;
347
    url_box.y = 0.2*PAGE_HEIGHT - url_box_margin;
348
    url_box.width = text_extents.width + 2*url_box_margin;
349
    url_box.height = -text_extents.height + 2*url_box_margin;
350
    cairo_rectangle(cr, url_box.x, url_box.y, url_box.width, url_box.height);
351
    cairo_stroke(cr);
352
    xasprintf(&attrib, "rect=[%f %f %f %f] uri=\'%s\'",
353
             url_box.x, url_box.y, url_box.width, url_box.height, cairo_url);
354
    cairo_tag_begin (cr, CAIRO_TAG_LINK, attrib);
355
    cairo_tag_end (cr, CAIRO_TAG_LINK);
356
    free (attrib);
357

            
358
    /* Create link to not yet emmited page number */
359
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "page=5");
360
    cairo_move_to (cr, PAGE_WIDTH/3, 0.25*PAGE_HEIGHT);
361
    cairo_show_text (cr, "link to page 5");
362
    cairo_tag_end (cr, CAIRO_TAG_LINK);
363

            
364
    /* Create link to not yet emmited destination */
365
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='Section 3.3'");
366
    cairo_move_to (cr, PAGE_WIDTH/3, 0.3*PAGE_HEIGHT);
367
    cairo_show_text (cr, "link to page section 3.3");
368
    cairo_tag_end (cr, CAIRO_TAG_LINK);
369

            
370
    /* Create link to external file */
371
    cairo_move_to (cr, PAGE_WIDTH/3, 0.35*PAGE_HEIGHT);
372
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "file='foo.pdf' page=1");
373
    cairo_show_text (cr, "link file 'foo.pdf'");
374
    cairo_tag_end (cr, CAIRO_TAG_LINK);
375

            
376
    /* Create link to missing dest */
377
    cairo_move_to (cr, PAGE_WIDTH/3, 0.4*PAGE_HEIGHT);
378
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='I don\\'t exist'");
379
    cairo_show_text (cr, "link to missing dest");
380
    cairo_tag_end (cr, CAIRO_TAG_LINK);
381

            
382
    /* Create link to missing dest with URI fallback*/
383
    cairo_move_to (cr, PAGE_WIDTH/3, 0.45*PAGE_HEIGHT);
384
    xasprintf(&attrib, "dest='I also don\\'t exist' uri='%s'", cairo_url);
385
    cairo_tag_begin (cr, CAIRO_TAG_LINK, attrib);
386
    cairo_show_text (cr, "link to missing dest with uri fallback");
387
    cairo_tag_end (cr, CAIRO_TAG_LINK);
388
    free (attrib);
389

            
390
    /* Create link to utf8 dest */
391
    cairo_move_to (cr, PAGE_WIDTH/3, 0.5*PAGE_HEIGHT);
392
    xasprintf(&attrib, "dest='%s'", utf8_destination);
393
    cairo_tag_begin (cr, CAIRO_TAG_LINK, attrib);
394
    cairo_show_text (cr, "link to utf8 dest");
395
    cairo_tag_end (cr, CAIRO_TAG_LINK);
396
    free (attrib);
397

            
398
    draw_page_num (surface, cr, "cover", 0);
399
}
400

            
401
static void
402
create_document (cairo_surface_t *surface, cairo_t *cr)
403
{
404
    char *attrib;
405

            
406
    layout_paragraph (cr);
407

            
408
    cairo_pdf_surface_set_thumbnail_size (surface, PAGE_WIDTH/10, PAGE_HEIGHT/10);
409

            
410
    cairo_pdf_surface_set_metadata (surface, CAIRO_PDF_METADATA_TITLE, "PDF Features Test");
411
    cairo_pdf_surface_set_metadata (surface, CAIRO_PDF_METADATA_AUTHOR, "cairo test suite");
412
    cairo_pdf_surface_set_metadata (surface, CAIRO_PDF_METADATA_SUBJECT, "cairo test");
413
    cairo_pdf_surface_set_metadata (surface, CAIRO_PDF_METADATA_KEYWORDS,
414
				    "tags, links, outline, page labels, metadata, thumbnails");
415
    cairo_pdf_surface_set_metadata (surface, CAIRO_PDF_METADATA_CREATOR, "pdf-features");
416
    cairo_pdf_surface_set_metadata (surface, CAIRO_PDF_METADATA_CREATE_DATE, "2016-01-01T12:34:56+10:30");
417
    cairo_pdf_surface_set_metadata (surface, CAIRO_PDF_METADATA_MOD_DATE, "2016-06-21T05:43:21Z");
418

            
419
    cairo_pdf_surface_set_custom_metadata (surface, "DocumentNumber", "12345");
420
    /* Include some non ASCII characters */
421
    cairo_pdf_surface_set_custom_metadata (surface, "Document Name", "\xc2\xab""cairo test\xc2\xbb");
422
    /* Test unsetting custom metadata. "DocumentNumber" should not be emitted. */
423
    cairo_pdf_surface_set_custom_metadata (surface, "DocumentNumber", "");
424

            
425
    cairo_tag_begin (cr, "Document", NULL);
426

            
427
    draw_cover (surface, cr);
428
    cairo_pdf_surface_add_outline (surface,
429
				   CAIRO_PDF_OUTLINE_ROOT,
430
				   "Cover", "page=1",
431
                                   CAIRO_PDF_OUTLINE_FLAG_BOLD);
432

            
433
    /* Create a simple link annotation. */
434
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "uri='http://example.org' rect=[10 10 20 20]");
435
    cairo_tag_end (cr, CAIRO_TAG_LINK);
436

            
437
    /* Try to create a link annotation while the clip is empty;
438
     * it will still be emitted.
439
     */
440
    cairo_save (cr);
441
    cairo_new_path (cr);
442
    cairo_rectangle (cr, 100, 100, 50, 0);
443
    cairo_clip (cr);
444
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "uri='http://example.com' rect=[100 100 20 20]");
445
    cairo_tag_end (cr, CAIRO_TAG_LINK);
446
    cairo_restore (cr);
447

            
448
    /* An annotation whose rect has a negative coordinate. */
449
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "uri='http://127.0.0.1/' rect=[10.0 -10.0 100.0 100.0]");
450
    cairo_tag_end (cr, CAIRO_TAG_LINK);
451

            
452

            
453
    /* Distilled from Mozilla bug https://bugzilla.mozilla.org/show_bug.cgi?id=1725743:
454
     * attempting to emit a Destination tag within a pushed group will lead to an
455
     * assertion in _cairo_pdf_interchange_end_structure_tag when processing a
456
     * following LINK tag that is outside the pushed group.
457
     */
458

            
459
    /* PushLayer */
460
    cairo_push_group_with_content (cr, CAIRO_CONTENT_COLOR_ALPHA);
461

            
462
    /* Destination */
463
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='a' x=42 y=42");
464
    cairo_tag_end (cr, CAIRO_TAG_DEST);
465

            
466
    /* PopLayer */
467
    cairo_pop_group_to_source (cr);
468
    cairo_paint_with_alpha (cr, 1);
469
    cairo_set_source_rgb (cr, 0, 0, 0);
470

            
471
    /* Link */
472
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "rect=[100 200 300 400] uri='http://127.0.0.1/'");
473
    cairo_tag_end (cr, CAIRO_TAG_LINK);
474

            
475
    /* End of extra Mozilla testcase. */
476

            
477

            
478
    cairo_show_page (cr);
479

            
480
    page_num = 0;
481
    draw_page_num (surface, cr, roman_numerals[page_num++], 0);
482
    y_pos = MARGIN;
483

            
484
    cairo_pdf_surface_add_outline (surface,
485
				   CAIRO_PDF_OUTLINE_ROOT,
486
				   "Contents", "dest='TOC'",
487
                                   CAIRO_PDF_OUTLINE_FLAG_BOLD);
488

            
489
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='TOC' internal");
490
    cairo_tag_begin (cr, "TOC", NULL);
491
    const struct section *sect = contents;
492
    while (sect->heading) {
493
	draw_contents (surface, cr, sect);
494
	sect++;
495
    }
496
    cairo_tag_end (cr, "TOC");
497
    cairo_tag_end (cr, CAIRO_TAG_DEST);
498

            
499
    page_num = 1;
500
    sect = contents;
501
    while (sect->heading) {
502
	draw_section (surface, cr, sect);
503
	sect++;
504
    }
505

            
506
    cairo_show_page (cr);
507

            
508
    cairo_set_source_rgb (cr, 0, 0, 1);
509

            
510
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='cover'");
511
    cairo_move_to (cr, PAGE_WIDTH/3, 2*PAGE_HEIGHT/5);
512
    cairo_show_text (cr, "link to cover");
513
    cairo_tag_end (cr, CAIRO_TAG_LINK);
514

            
515
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "page=3");
516
    cairo_move_to (cr, PAGE_WIDTH/3, 3*PAGE_HEIGHT/5);
517
    cairo_show_text (cr, "link to page 3");
518
    cairo_tag_end (cr, CAIRO_TAG_LINK);
519

            
520
    /* Create utf8 dest */
521
    cairo_move_to (cr, PAGE_WIDTH/3, 4*PAGE_HEIGHT/5);
522
    xasprintf(&attrib, "name='%s'", utf8_destination);
523
    cairo_tag_begin (cr, CAIRO_TAG_DEST, attrib);
524
    cairo_show_text (cr, utf8_destination);
525
    cairo_tag_end (cr, CAIRO_TAG_DEST);
526
    free (attrib);
527

            
528
    cairo_tag_end (cr, "Document");
529
}
530

            
531
#ifdef HAVE_MMAP
532
static cairo_test_status_t
533
check_contains_string(cairo_test_context_t *ctx, const void *hay, size_t size, const char *needle)
534
{
535
    if (memmem(hay, size, needle, strlen(needle)))
536
        return CAIRO_TEST_SUCCESS;
537

            
538
    cairo_test_log (ctx, "Failed to find expected string in generated PDF: %s\n", needle);
539
    return CAIRO_TEST_FAILURE;
540
}
541
#endif
542

            
543
static cairo_test_status_t
544
check_created_pdf(cairo_test_context_t *ctx, const char* filename)
545
{
546
    cairo_test_status_t result = CAIRO_TEST_SUCCESS;
547
    int fd;
548
    struct stat st;
549
#ifdef HAVE_MMAP
550
    void *contents;
551
#endif
552

            
553
    fd = open(filename, O_RDONLY, 0);
554
    if (fd < 0) {
555
        cairo_test_log (ctx, "Failed to open generated PDF file %s: %s\n", filename, strerror(errno));
556
        return CAIRO_TEST_FAILURE;
557
    }
558

            
559
    if (fstat(fd, &st) == -1)
560
    {
561
        cairo_test_log (ctx, "Failed to stat generated PDF file %s: %s\n", filename, strerror(errno));
562
        close(fd);
563
        return CAIRO_TEST_FAILURE;
564
    }
565

            
566
    if (st.st_size == 0)
567
    {
568
        cairo_test_log (ctx, "Generated PDF file %s is empty\n", filename);
569
        close(fd);
570
        return CAIRO_TEST_FAILURE;
571
    }
572

            
573
#ifdef HAVE_MMAP
574
    contents = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
575
    if (contents == NULL)
576
    {
577
        cairo_test_log (ctx, "Failed to mmap generated PDF file %s: %s\n", filename, strerror(errno));
578
        close(fd);
579
        return CAIRO_TEST_FAILURE;
580
    }
581

            
582
    /* check metadata */
583
    result |= check_contains_string(ctx, contents, st.st_size, "/Title (PDF Features Test)");
584
    result |= check_contains_string(ctx, contents, st.st_size, "/Author (cairo test suite)");
585
    result |= check_contains_string(ctx, contents, st.st_size, "/Creator (pdf-features)");
586
    result |= check_contains_string(ctx, contents, st.st_size, "/CreationDate (20160101123456+10'30')");
587
    result |= check_contains_string(ctx, contents, st.st_size, "/ModDate (20160621054321Z)");
588

            
589
    /* check that both the example.org and example.com links were generated */
590
    result |= check_contains_string(ctx, contents, st.st_size, "http://example.org");
591
    result |= check_contains_string(ctx, contents, st.st_size, "http://example.com");
592

            
593
    // TODO: add more checks
594

            
595
    munmap(contents, st.st_size);
596
#endif
597

            
598
    close(fd);
599

            
600
    return result;
601
}
602

            
603
static cairo_test_status_t
604
create_pdf (cairo_test_context_t *ctx, cairo_bool_t check_output)
605
{
606
    cairo_surface_t *surface;
607
    cairo_t *cr;
608
    cairo_status_t status, status2;
609
    cairo_test_status_t result;
610
    cairo_pdf_version_t version;
611
    char *filename;
612
    const char *path = cairo_test_mkdir (CAIRO_TEST_OUTPUT_DIR) ? CAIRO_TEST_OUTPUT_DIR : ".";
613

            
614
    /* check_created_pdf() only works with version 1.4. In version 1.5
615
     * the text that is searched for is compressed. */
616
    version = check_output ? CAIRO_PDF_VERSION_1_4 : CAIRO_PDF_VERSION_1_5;
617

            
618
    xasprintf (&filename, "%s/%s-%s.pdf",
619
               path,
620
               BASENAME,
621
               check_output ? "1.4" : "1.5");
622
    surface = cairo_pdf_surface_create (filename, PAGE_WIDTH, PAGE_HEIGHT);
623

            
624
    cairo_pdf_surface_restrict_to_version (surface, version);
625

            
626
    cr = cairo_create (surface);
627
    create_document (surface, cr);
628

            
629
    status = cairo_status (cr);
630
    cairo_destroy (cr);
631
    cairo_surface_finish (surface);
632
    status2 = cairo_surface_status (surface);
633
    if (status == CAIRO_STATUS_SUCCESS)
634
	status = status2;
635

            
636
    cairo_surface_destroy (surface);
637
    if (status) {
638
	cairo_test_log (ctx, "Failed to create pdf surface for file %s: %s\n",
639
			filename, cairo_status_to_string (status));
640
	return CAIRO_TEST_FAILURE;
641
    }
642

            
643
    result = CAIRO_TEST_SUCCESS;
644
    if (check_output)
645
        result = check_created_pdf(ctx, filename);
646

            
647
    free (filename);
648

            
649
    return result;
650
}
651

            
652
static cairo_test_status_t
653
1
preamble (cairo_test_context_t *ctx)
654
{
655
    cairo_test_status_t result;
656

            
657
1
    if (! cairo_test_is_target_enabled (ctx, "pdf"))
658
1
	return CAIRO_TEST_UNTESTED;
659

            
660
    /* Create version 1.5 PDF. This can only be manually checked */
661
    create_pdf (ctx, FALSE);
662

            
663
    /* Create version 1.4 PDF and checkout output */
664
    result = create_pdf (ctx, TRUE);
665

            
666

            
667
    return result;
668
}
669

            
670
1
CAIRO_TEST (pdf_tagged_text,
671
	    "Check tagged text, hyperlinks and PDF document features",
672
	    "pdf", /* keywords */
673
	    NULL, /* requirements */
674
	    0, 0,
675
	    preamble, NULL)
676

            
677
#endif /* CAIRO_HAS_PDF_SURFACE */