1
/*
2
 * Copyright © 2023 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

            
33
#ifdef HAVE_UNISTD_H
34
#include <unistd.h> /* __unix__ */
35
#endif
36
#if HAVE_SYS_WAIT_H
37
#include <sys/wait.h>
38
#endif
39

            
40
#include <cairo.h>
41
#include <cairo-pdf.h>
42

            
43
/* Test PDF logical structure
44
 */
45

            
46
#define BASENAME "pdf-structure"
47

            
48
#define PAGE_WIDTH 595
49
#define PAGE_HEIGHT 842
50

            
51
#define PDF_VERSION CAIRO_PDF_VERSION_1_4
52

            
53
struct pdf_structure_test {
54
    const char *name;
55
    void (*func)(cairo_t *cr);
56
};
57

            
58
static void
59
text(cairo_t *cr, const char *text)
60
{
61
    double x, y;
62

            
63
    cairo_show_text (cr, text);
64
    cairo_get_current_point (cr, &x, &y);
65
    cairo_move_to (cr, 20, y + 15);
66
}
67

            
68
static void
69
test_simple (cairo_t *cr)
70
{
71
    cairo_tag_begin (cr, "Document", NULL);
72

            
73
    cairo_tag_begin (cr, "H", "");
74
    text (cr, "Heading");
75
    cairo_tag_end (cr, "H");
76

            
77
    cairo_tag_begin (cr, "Sect", NULL);
78

            
79
    cairo_tag_begin (cr, "P", "");
80
    text (cr, "Para1");
81
    text (cr, "Para2");
82
    cairo_tag_end (cr, "P");
83

            
84
    cairo_tag_begin (cr, "P", "");
85
    text (cr, "Para3");
86

            
87
    cairo_tag_begin (cr, "Note", "");
88
    text (cr, "Note");
89
    cairo_tag_end (cr, "Note");
90

            
91
    text (cr, "Para4");
92
    cairo_tag_end (cr, "P");
93

            
94
    cairo_tag_end (cr, "Sect");
95

            
96
    cairo_tag_end (cr, "Document");
97
}
98

            
99
static void
100
test_simple_ref (cairo_t *cr)
101
{
102
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='H' id='heading'");
103
    text (cr, "Heading");
104
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
105

            
106
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para1'");
107
    text (cr, "Para1");
108
    text (cr, "Para2");
109
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
110

            
111
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para2'");
112
    text (cr, "Para3");
113
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
114

            
115
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='Note' id='note'");
116
    text (cr, "Note");
117
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
118

            
119
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para3'");
120
    text (cr, "Para4");
121
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
122

            
123
    cairo_tag_begin (cr, "Document", NULL);
124

            
125
    cairo_tag_begin (cr, "H", "");
126
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading'");
127
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
128
    cairo_tag_end (cr, "H");
129

            
130
    cairo_tag_begin (cr, "Sect", NULL);
131

            
132
    cairo_tag_begin (cr, "P", "");
133
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para1'");
134
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
135
    cairo_tag_end (cr, "P");
136

            
137
    cairo_tag_begin (cr, "P", "");
138

            
139
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para2'");
140
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
141

            
142
    cairo_tag_begin (cr, "Note", "");
143
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='note'");
144
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
145
    cairo_tag_end (cr, "Note");
146

            
147
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para3'");
148
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
149

            
150
    cairo_tag_end (cr, "P");
151

            
152
    cairo_tag_end (cr, "Sect");
153

            
154
    cairo_tag_end (cr, "Document");
155
}
156

            
157
static void
158
test_group (cairo_t *cr)
159
{
160
    cairo_tag_begin (cr, "Document", NULL);
161

            
162
    cairo_tag_begin (cr, "H", "");
163
    text (cr, "Heading");
164
    cairo_tag_end (cr, "H");
165

            
166
    cairo_tag_begin (cr, "Sect", NULL);
167

            
168
    cairo_push_group (cr);
169

            
170
    cairo_tag_begin (cr, "P", "");
171
    text (cr, "Para1");
172
    text (cr, "Para2");
173
    cairo_tag_end (cr, "P");
174

            
175
    cairo_pop_group_to_source (cr);
176
    cairo_paint (cr);
177

            
178
    cairo_tag_end (cr, "Sect");
179

            
180
    cairo_tag_end (cr, "Document");
181
}
182

            
183
/* https://bugzilla.mozilla.org/show_bug.cgi?id=1896173
184
 * This particular combination of tags and groups resulted in a crash.
185
 */
186
static void
187
test_group2 (cairo_t *cr)
188
{
189
    cairo_tag_begin (cr, "H", "");
190
    text (cr, "Heading");
191
    cairo_tag_end (cr, "H");
192

            
193
    cairo_push_group (cr);
194

            
195
    cairo_tag_begin (cr, "P", "");
196
    text (cr, "Para1");
197
    cairo_tag_end (cr, "P");
198

            
199
    cairo_pop_group_to_source (cr);
200
    cairo_paint (cr);
201

            
202
    cairo_set_source_rgb (cr, 0, 0, 0);
203
    text (cr, "text");
204
}
205

            
206
/* Check that the fix for test_group2() works when there is a top level tag. */
207
static void
208
test_group3 (cairo_t *cr)
209
{
210
    cairo_tag_begin (cr, "Document", NULL);
211

            
212
    cairo_tag_begin (cr, "H", "");
213
    text (cr, "Heading");
214
    cairo_tag_end (cr, "H");
215

            
216
    cairo_push_group (cr);
217

            
218
    cairo_tag_begin (cr, "P", "");
219
    text (cr, "Para1");
220
    cairo_tag_end (cr, "P");
221

            
222
    cairo_pop_group_to_source (cr);
223
    cairo_paint (cr);
224

            
225
    cairo_set_source_rgb (cr, 0, 0, 0);
226
    text (cr, "text");
227

            
228
    cairo_tag_end (cr, "Document");
229
}
230

            
231
static void
232
test_group_ref (cairo_t *cr)
233
{
234
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='H' id='heading'");
235
    text (cr, "Heading");
236
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
237

            
238
    cairo_push_group (cr);
239

            
240
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para'");
241
    text (cr, "Para1");
242
    text (cr, "Para2");
243
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
244

            
245
    cairo_pop_group_to_source (cr);
246
    cairo_paint (cr);
247

            
248
    cairo_tag_begin (cr, "Document", NULL);
249

            
250
    cairo_tag_begin (cr, "H", "");
251
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading'");
252
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
253
    cairo_tag_end (cr, "H");
254

            
255
    cairo_tag_begin (cr, "Sect", NULL);
256

            
257
    cairo_tag_begin (cr, "P", "");
258
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para'");
259
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
260
    cairo_tag_end (cr, "P");
261

            
262
    cairo_tag_end (cr, "Sect");
263

            
264
    cairo_tag_end (cr, "Document");
265

            
266
}
267

            
268
static void
269
test_repeated_group (cairo_t *cr)
270
{
271
    cairo_pattern_t *pat;
272

            
273
    cairo_tag_begin (cr, "Document", NULL);
274

            
275
    cairo_tag_begin (cr, "H", "");
276
    text (cr, "Heading");
277
    cairo_tag_end (cr, "H");
278

            
279
    cairo_tag_begin (cr, "Sect", NULL);
280

            
281
    cairo_push_group (cr);
282

            
283
    cairo_tag_begin (cr, "P", "");
284
    text (cr, "Para1");
285
    text (cr, "Para2");
286
    cairo_tag_end (cr, "P");
287

            
288
    pat = cairo_pop_group (cr);
289

            
290
    cairo_set_source (cr, pat);
291
    cairo_paint (cr);
292

            
293
    cairo_translate (cr, 0, 100);
294
    cairo_set_source (cr, pat);
295
    cairo_rectangle (cr, 0, 0, 100, 100);
296
    cairo_fill (cr);
297

            
298
    cairo_translate (cr, 0, 100);
299
    cairo_set_source_rgb (cr, 1, 0, 0);
300
    cairo_mask (cr, pat);
301

            
302
    cairo_translate (cr, 0, 100);
303
    cairo_set_source_rgb (cr, 0, 1, 0);
304
    cairo_move_to (cr, 20, 0);
305
    cairo_line_to (cr, 100, 0);
306
    cairo_stroke (cr);
307

            
308
    cairo_translate (cr, 0, 100);
309
    cairo_set_source_rgb (cr, 0, 0, 1);
310
    cairo_move_to (cr, 20, 0);
311
    cairo_show_text (cr, "Text");
312

            
313
    cairo_tag_end (cr, "Sect");
314

            
315
    cairo_tag_end (cr, "Document");
316
}
317

            
318
static void
319
test_multipage_simple (cairo_t *cr)
320
{
321
    cairo_tag_begin (cr, "Document", NULL);
322

            
323
    cairo_tag_begin (cr, "H", "");
324

            
325
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para1-dest'");
326
    text (cr, "Heading1");
327
    cairo_tag_end (cr, CAIRO_TAG_LINK);
328

            
329
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para2-dest'");
330
    text (cr, "Heading2");
331
    cairo_tag_end (cr, CAIRO_TAG_LINK);
332

            
333
    cairo_tag_end (cr, "H");
334

            
335
    cairo_tag_begin (cr, "Sect", NULL);
336

            
337
    cairo_show_page (cr);
338

            
339
    cairo_tag_begin (cr, "P", "");
340

            
341
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='para1-dest' internal");
342
    text (cr, "Para1");
343
    cairo_tag_end (cr, CAIRO_TAG_DEST);
344

            
345
    cairo_show_page (cr);
346

            
347
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='para2-dest' internal");
348
    text (cr, "Para2");
349
    cairo_tag_end (cr, CAIRO_TAG_DEST);
350

            
351
    cairo_tag_end (cr, "P");
352

            
353
    cairo_tag_end (cr, "Sect");
354

            
355
    cairo_tag_end (cr, "Document");
356
}
357

            
358
static void
359
test_multipage_simple_ref (cairo_t *cr)
360
{
361
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='H' id='heading1'");
362
    text (cr, "Heading1");
363
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
364

            
365
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='H' id='heading2'");
366
    text (cr, "Heading2");
367
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
368

            
369
    cairo_show_page (cr);
370

            
371
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='para1-dest' internal");
372
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para1'");
373
    text (cr, "Para1");
374
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
375
    cairo_tag_end (cr, CAIRO_TAG_DEST);
376

            
377
    cairo_show_page (cr);
378

            
379
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='para2-dest' internal");
380
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para2'");
381
    text (cr, "Para2");
382
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
383
    cairo_tag_end (cr, CAIRO_TAG_DEST);
384

            
385
    cairo_tag_begin (cr, "Document", NULL);
386

            
387
    cairo_tag_begin (cr, "H", "");
388

            
389
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para1-dest' link_page=1");
390
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading1'");
391
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
392
    cairo_tag_end (cr, CAIRO_TAG_LINK);
393

            
394
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para2-dest' link_page=1");
395
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading2'");
396
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
397
    cairo_tag_end (cr, CAIRO_TAG_LINK);
398

            
399
    cairo_tag_end (cr, "H");
400

            
401
    cairo_tag_begin (cr, "Sect", NULL);
402

            
403
    cairo_tag_begin (cr, "P", "");
404
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para1'");
405
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
406
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para2'");
407
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
408
    cairo_tag_end (cr, "P");
409

            
410
    cairo_tag_end (cr, "Sect");
411

            
412
    cairo_tag_end (cr, "Document");
413
}
414

            
415
static void
416
test_multipage_group (cairo_t *cr)
417
{
418
    cairo_tag_begin (cr, "Document", NULL);
419

            
420
    cairo_tag_begin (cr, "H", "");
421
    text (cr, "Heading");
422
    cairo_tag_end (cr, "H");
423

            
424
    cairo_tag_begin (cr, "Sect", NULL);
425

            
426
    cairo_push_group (cr);
427

            
428
    cairo_tag_begin (cr, "P", "");
429
    text (cr, "Para1");
430
    text (cr, "Para2");
431
    cairo_tag_end (cr, "P");
432

            
433
    cairo_pop_group_to_source (cr);
434
    cairo_paint (cr);
435
    cairo_set_source_rgb (cr, 0, 0, 0);
436

            
437
    cairo_show_page (cr);
438

            
439
    cairo_tag_begin (cr, "P", "");
440
    text (cr, "Para3");
441
    cairo_tag_end (cr, "P");
442

            
443
    cairo_tag_end (cr, "Sect");
444

            
445
    cairo_tag_end (cr, "Document");
446
}
447

            
448
/* Same as test_multipage_group but but repeat the group on the second page. */
449
static void
450
test_multipage_group2 (cairo_t *cr)
451
{
452
    cairo_tag_begin (cr, "Document", NULL);
453

            
454
    cairo_tag_begin (cr, "H", "");
455
    text (cr, "Heading");
456
    cairo_tag_end (cr, "H");
457

            
458
    cairo_tag_begin (cr, "Sect", NULL);
459

            
460
    cairo_push_group (cr);
461

            
462
    cairo_tag_begin (cr, "P", "");
463
    text (cr, "Para1");
464
    text (cr, "Para2");
465
    cairo_tag_end (cr, "P");
466

            
467
    cairo_pop_group_to_source (cr);
468
    cairo_paint (cr);
469

            
470
    cairo_show_page (cr);
471

            
472
    cairo_paint (cr);
473
    cairo_set_source_rgb (cr, 0, 0, 0);
474

            
475
    cairo_tag_begin (cr, "P", "");
476
    text (cr, "Para3");
477
    cairo_tag_end (cr, "P");
478

            
479
    cairo_tag_end (cr, "Sect");
480

            
481
    cairo_tag_end (cr, "Document");
482
}
483

            
484
static const struct pdf_structure_test pdf_structure_tests[] = {
485
    { "simple", test_simple },
486
    { "simple-ref", test_simple_ref },
487
    { "group", test_group },
488
    { "group2", test_group2 },
489
    { "group3", test_group3 },
490
    { "group-ref", test_group_ref },
491
    { "repeated-group", test_repeated_group },
492
    { "multipage-simple", test_multipage_simple },
493
    { "multipage-simple-ref", test_multipage_simple_ref },
494
    { "multipage-group", test_multipage_group },
495
    { "multipage-group2", test_multipage_group2 },
496
};
497

            
498
static cairo_test_status_t
499
create_pdf (cairo_test_context_t *ctx, const struct pdf_structure_test *test, const char *output)
500
{
501
    cairo_surface_t *surface;
502
    cairo_t *cr;
503
    cairo_status_t status, status2;
504

            
505
    surface = cairo_pdf_surface_create (output, PAGE_WIDTH, PAGE_HEIGHT);
506

            
507
    cairo_pdf_surface_restrict_to_version (surface, PDF_VERSION);
508

            
509
    cr = cairo_create (surface);
510

            
511
    cairo_select_font_face (cr, CAIRO_TEST_FONT_FAMILY " Serif",
512
                            CAIRO_FONT_SLANT_NORMAL,
513
                            CAIRO_FONT_WEIGHT_NORMAL);
514
    cairo_set_font_size (cr, 10);
515
    cairo_move_to (cr, 20, 20);
516

            
517
    test->func(cr);
518

            
519
    status = cairo_status (cr);
520
    cairo_destroy (cr);
521
    cairo_surface_finish (surface);
522
    status2 = cairo_surface_status (surface);
523
    if (status == CAIRO_STATUS_SUCCESS)
524
	status = status2;
525

            
526
    cairo_surface_destroy (surface);
527
    if (status) {
528
	cairo_test_log (ctx, "Failed to create pdf surface for file %s: %s\n",
529
			output, cairo_status_to_string (status));
530
	return CAIRO_TEST_FAILURE;
531
    }
532

            
533
    return CAIRO_TEST_SUCCESS;
534
}
535

            
536
static cairo_test_status_t
537
check_pdf (cairo_test_context_t *ctx, const struct pdf_structure_test *test, const char *output)
538
{
539
    char *command;
540
    int ret;
541
    cairo_test_status_t result = CAIRO_TEST_FAILURE;
542

            
543
    /* check-pdf-structure.sh <pdf-file> <pdfinfo-output> <pdfinfo-ref> <diff-output> */
544
    xasprintf (&command,
545
               "%s/check-pdf-structure.sh  %s  %s/%s-%s.out.txt  %s/%s-%s.ref.txt %s/%s-%s.diff.txt ",
546
               ctx->srcdir,
547
               output,
548
               ctx->output, BASENAME, test->name,
549
               ctx->refdir, BASENAME, test->name,
550
               ctx->output, BASENAME, test->name);
551

            
552
    ret = system (command);
553
    cairo_test_log (ctx, "%s  exit code %d\n", command,
554
                    WIFEXITED (ret) ? WEXITSTATUS (ret) : -1);
555

            
556
    if (WIFEXITED (ret)) {
557
        if (WEXITSTATUS (ret) == 0)
558
            result = CAIRO_TEST_SUCCESS;
559
        else if (WEXITSTATUS (ret) == 4)
560
            result = CAIRO_TEST_UNTESTED; /* pdfinfo not found or wrong version */
561
    }
562

            
563
    free (command);
564
    return result;
565
}
566

            
567
static void
568
merge_test_status (cairo_test_status_t *current, cairo_test_status_t new)
569
{
570
    if (new == CAIRO_TEST_FAILURE || *current == CAIRO_TEST_FAILURE)
571
        *current = CAIRO_TEST_FAILURE;
572
    else if (new == CAIRO_TEST_UNTESTED)
573
        *current = CAIRO_TEST_UNTESTED;
574
    else
575
        *current = new;
576
}
577

            
578
static cairo_test_status_t
579
1
preamble (cairo_test_context_t *ctx)
580
{
581
    int i;
582
    char *filename;
583
    cairo_test_status_t result, all_results;
584
1
    cairo_bool_t can_check = FALSE;
585

            
586
/* Need a POSIX shell to run the check. */
587
#ifdef __unix__
588
1
    can_check = TRUE;
589
#endif
590

            
591
1
    all_results = CAIRO_TEST_SUCCESS;
592
1
    if (! cairo_test_is_target_enabled (ctx, "pdf"))
593
1
	return CAIRO_TEST_UNTESTED;
594

            
595
    for (i = 0; i < ARRAY_LENGTH(pdf_structure_tests); i++) {
596
        xasprintf (&filename, "%s/%s-%s.out.pdf",
597
                   ctx->output,
598
                   BASENAME,
599
                   pdf_structure_tests[i].name);
600

            
601
        result = create_pdf (ctx, &pdf_structure_tests[i], filename);
602
        merge_test_status (&all_results, result);
603

            
604
        if (can_check && result == CAIRO_TEST_SUCCESS) {
605
            result = check_pdf (ctx, &pdf_structure_tests[i], filename);
606
            merge_test_status (&all_results, result);
607
        } else {
608
            merge_test_status (&all_results, CAIRO_TEST_UNTESTED);
609
        }
610
    }
611

            
612
    free (filename);
613
    return all_results;
614
}
615

            
616
1
CAIRO_TEST (pdf_structure,
617
	    "Check PDF Structure",
618
	    "pdf", /* keywords */
619
	    NULL, /* requirements */
620
	    0, 0,
621
	    preamble, NULL)