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

            
37
#include <cairo.h>
38
#include <cairo-pdf.h>
39

            
40
/* Test PDF logical structure
41
 */
42

            
43
#define BASENAME "pdf-structure"
44

            
45
#define PAGE_WIDTH 595
46
#define PAGE_HEIGHT 842
47

            
48
#define PDF_VERSION CAIRO_PDF_VERSION_1_4
49

            
50
struct pdf_structure_test {
51
    const char *name;
52
    void (*func)(cairo_t *cr);
53
};
54

            
55
static void
56
text(cairo_t *cr, const char *text)
57
{
58
    double x, y;
59

            
60
    cairo_show_text (cr, text);
61
    cairo_get_current_point (cr, &x, &y);
62
    cairo_move_to (cr, 20, y + 15);
63
}
64

            
65
static void
66
test_simple (cairo_t *cr)
67
{
68
    cairo_tag_begin (cr, "Document", NULL);
69

            
70
    cairo_tag_begin (cr, "H", "");
71
    text (cr, "Heading");
72
    cairo_tag_end (cr, "H");
73

            
74
    cairo_tag_begin (cr, "Sect", NULL);
75

            
76
    cairo_tag_begin (cr, "P", "");
77
    text (cr, "Para1");
78
    text (cr, "Para2");
79
    cairo_tag_end (cr, "P");
80

            
81
    cairo_tag_begin (cr, "P", "");
82
    text (cr, "Para3");
83

            
84
    cairo_tag_begin (cr, "Note", "");
85
    text (cr, "Note");
86
    cairo_tag_end (cr, "Note");
87

            
88
    text (cr, "Para4");
89
    cairo_tag_end (cr, "P");
90

            
91
    cairo_tag_end (cr, "Sect");
92

            
93
    cairo_tag_end (cr, "Document");
94
}
95

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

            
103
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para1'");
104
    text (cr, "Para1");
105
    text (cr, "Para2");
106
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
107

            
108
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para2'");
109
    text (cr, "Para3");
110
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
111

            
112
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='Note' id='note'");
113
    text (cr, "Note");
114
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
115

            
116
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para3'");
117
    text (cr, "Para4");
118
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
119

            
120
    cairo_tag_begin (cr, "Document", NULL);
121

            
122
    cairo_tag_begin (cr, "H", "");
123
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading'");
124
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
125
    cairo_tag_end (cr, "H");
126

            
127
    cairo_tag_begin (cr, "Sect", NULL);
128

            
129
    cairo_tag_begin (cr, "P", "");
130
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para1'");
131
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
132
    cairo_tag_end (cr, "P");
133

            
134
    cairo_tag_begin (cr, "P", "");
135

            
136
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para2'");
137
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
138

            
139
    cairo_tag_begin (cr, "Note", "");
140
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='note'");
141
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
142
    cairo_tag_end (cr, "Note");
143

            
144
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para3'");
145
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
146

            
147
    cairo_tag_end (cr, "P");
148

            
149
    cairo_tag_end (cr, "Sect");
150

            
151
    cairo_tag_end (cr, "Document");
152
}
153

            
154
static void
155
test_group (cairo_t *cr)
156
{
157
    cairo_tag_begin (cr, "Document", NULL);
158

            
159
    cairo_tag_begin (cr, "H", "");
160
    text (cr, "Heading");
161
    cairo_tag_end (cr, "H");
162

            
163
    cairo_tag_begin (cr, "Sect", NULL);
164

            
165
    cairo_push_group (cr);
166

            
167
    cairo_tag_begin (cr, "P", "");
168
    text (cr, "Para1");
169
    text (cr, "Para2");
170
    cairo_tag_end (cr, "P");
171

            
172
    cairo_pop_group_to_source (cr);
173
    cairo_paint (cr);
174

            
175
    cairo_tag_end (cr, "Sect");
176

            
177
    cairo_tag_end (cr, "Document");
178
}
179

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

            
190
    cairo_push_group (cr);
191

            
192
    cairo_tag_begin (cr, "P", "");
193
    text (cr, "Para1");
194
    cairo_tag_end (cr, "P");
195

            
196
    cairo_pop_group_to_source (cr);
197
    cairo_paint (cr);
198

            
199
    cairo_set_source_rgb (cr, 0, 0, 0);
200
    text (cr, "text");
201
}
202

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

            
209
    cairo_tag_begin (cr, "H", "");
210
    text (cr, "Heading");
211
    cairo_tag_end (cr, "H");
212

            
213
    cairo_push_group (cr);
214

            
215
    cairo_tag_begin (cr, "P", "");
216
    text (cr, "Para1");
217
    cairo_tag_end (cr, "P");
218

            
219
    cairo_pop_group_to_source (cr);
220
    cairo_paint (cr);
221

            
222
    cairo_set_source_rgb (cr, 0, 0, 0);
223
    text (cr, "text");
224

            
225
    cairo_tag_end (cr, "Document");
226
}
227

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

            
235
    cairo_push_group (cr);
236

            
237
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='P' id='para'");
238
    text (cr, "Para1");
239
    text (cr, "Para2");
240
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
241

            
242
    cairo_pop_group_to_source (cr);
243
    cairo_paint (cr);
244

            
245
    cairo_tag_begin (cr, "Document", NULL);
246

            
247
    cairo_tag_begin (cr, "H", "");
248
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading'");
249
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
250
    cairo_tag_end (cr, "H");
251

            
252
    cairo_tag_begin (cr, "Sect", NULL);
253

            
254
    cairo_tag_begin (cr, "P", "");
255
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para'");
256
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
257
    cairo_tag_end (cr, "P");
258

            
259
    cairo_tag_end (cr, "Sect");
260

            
261
    cairo_tag_end (cr, "Document");
262

            
263
}
264

            
265
static void
266
test_repeated_group (cairo_t *cr)
267
{
268
    cairo_pattern_t *pat;
269

            
270
    cairo_tag_begin (cr, "Document", NULL);
271

            
272
    cairo_tag_begin (cr, "H", "");
273
    text (cr, "Heading");
274
    cairo_tag_end (cr, "H");
275

            
276
    cairo_tag_begin (cr, "Sect", NULL);
277

            
278
    cairo_push_group (cr);
279

            
280
    cairo_tag_begin (cr, "P", "");
281
    text (cr, "Para1");
282
    text (cr, "Para2");
283
    cairo_tag_end (cr, "P");
284

            
285
    pat = cairo_pop_group (cr);
286

            
287
    cairo_set_source (cr, pat);
288
    cairo_paint (cr);
289

            
290
    cairo_translate (cr, 0, 100);
291
    cairo_set_source (cr, pat);
292
    cairo_rectangle (cr, 0, 0, 100, 100);
293
    cairo_fill (cr);
294

            
295
    cairo_translate (cr, 0, 100);
296
    cairo_set_source_rgb (cr, 1, 0, 0);
297
    cairo_mask (cr, pat);
298

            
299
    cairo_translate (cr, 0, 100);
300
    cairo_set_source_rgb (cr, 0, 1, 0);
301
    cairo_move_to (cr, 20, 0);
302
    cairo_line_to (cr, 100, 0);
303
    cairo_stroke (cr);
304

            
305
    cairo_translate (cr, 0, 100);
306
    cairo_set_source_rgb (cr, 0, 0, 1);
307
    cairo_move_to (cr, 20, 0);
308
    cairo_show_text (cr, "Text");
309

            
310
    cairo_tag_end (cr, "Sect");
311

            
312
    cairo_tag_end (cr, "Document");
313
}
314

            
315
static void
316
test_multipage_simple (cairo_t *cr)
317
{
318
    cairo_tag_begin (cr, "Document", NULL);
319

            
320
    cairo_tag_begin (cr, "H", "");
321

            
322
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para1-dest'");
323
    text (cr, "Heading1");
324
    cairo_tag_end (cr, CAIRO_TAG_LINK);
325

            
326
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para2-dest'");
327
    text (cr, "Heading2");
328
    cairo_tag_end (cr, CAIRO_TAG_LINK);
329

            
330
    cairo_tag_end (cr, "H");
331

            
332
    cairo_tag_begin (cr, "Sect", NULL);
333

            
334
    cairo_show_page (cr);
335

            
336
    cairo_tag_begin (cr, "P", "");
337

            
338
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='para1-dest' internal");
339
    text (cr, "Para1");
340
    cairo_tag_end (cr, CAIRO_TAG_DEST);
341

            
342
    cairo_show_page (cr);
343

            
344
    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='para2-dest' internal");
345
    text (cr, "Para2");
346
    cairo_tag_end (cr, CAIRO_TAG_DEST);
347

            
348
    cairo_tag_end (cr, "P");
349

            
350
    cairo_tag_end (cr, "Sect");
351

            
352
    cairo_tag_end (cr, "Document");
353
}
354

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

            
362
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT, "tag_name='H' id='heading2'");
363
    text (cr, "Heading2");
364
    cairo_tag_end (cr, CAIRO_TAG_CONTENT);
365

            
366
    cairo_show_page (cr);
367

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

            
374
    cairo_show_page (cr);
375

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

            
382
    cairo_tag_begin (cr, "Document", NULL);
383

            
384
    cairo_tag_begin (cr, "H", "");
385

            
386
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para1-dest' link_page=1");
387
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading1'");
388
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
389
    cairo_tag_end (cr, CAIRO_TAG_LINK);
390

            
391
    cairo_tag_begin (cr, CAIRO_TAG_LINK, "dest='para2-dest' link_page=1");
392
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='heading2'");
393
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
394
    cairo_tag_end (cr, CAIRO_TAG_LINK);
395

            
396
    cairo_tag_end (cr, "H");
397

            
398
    cairo_tag_begin (cr, "Sect", NULL);
399

            
400
    cairo_tag_begin (cr, "P", "");
401
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para1'");
402
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
403
    cairo_tag_begin (cr, CAIRO_TAG_CONTENT_REF, "ref='para2'");
404
    cairo_tag_end (cr, CAIRO_TAG_CONTENT_REF);
405
    cairo_tag_end (cr, "P");
406

            
407
    cairo_tag_end (cr, "Sect");
408

            
409
    cairo_tag_end (cr, "Document");
410
}
411

            
412
static void
413
test_multipage_group (cairo_t *cr)
414
{
415
    cairo_tag_begin (cr, "Document", NULL);
416

            
417
    cairo_tag_begin (cr, "H", "");
418
    text (cr, "Heading");
419
    cairo_tag_end (cr, "H");
420

            
421
    cairo_tag_begin (cr, "Sect", NULL);
422

            
423
    cairo_push_group (cr);
424

            
425
    cairo_tag_begin (cr, "P", "");
426
    text (cr, "Para1");
427
    text (cr, "Para2");
428
    cairo_tag_end (cr, "P");
429

            
430
    cairo_pop_group_to_source (cr);
431
    cairo_paint (cr);
432
    cairo_set_source_rgb (cr, 0, 0, 0);
433

            
434
    cairo_show_page (cr);
435

            
436
    cairo_tag_begin (cr, "P", "");
437
    text (cr, "Para3");
438
    cairo_tag_end (cr, "P");
439

            
440
    cairo_tag_end (cr, "Sect");
441

            
442
    cairo_tag_end (cr, "Document");
443
}
444

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

            
451
    cairo_tag_begin (cr, "H", "");
452
    text (cr, "Heading");
453
    cairo_tag_end (cr, "H");
454

            
455
    cairo_tag_begin (cr, "Sect", NULL);
456

            
457
    cairo_push_group (cr);
458

            
459
    cairo_tag_begin (cr, "P", "");
460
    text (cr, "Para1");
461
    text (cr, "Para2");
462
    cairo_tag_end (cr, "P");
463

            
464
    cairo_pop_group_to_source (cr);
465
    cairo_paint (cr);
466

            
467
    cairo_show_page (cr);
468

            
469
    cairo_paint (cr);
470
    cairo_set_source_rgb (cr, 0, 0, 0);
471

            
472
    cairo_tag_begin (cr, "P", "");
473
    text (cr, "Para3");
474
    cairo_tag_end (cr, "P");
475

            
476
    cairo_tag_end (cr, "Sect");
477

            
478
    cairo_tag_end (cr, "Document");
479
}
480

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

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

            
502
    surface = cairo_pdf_surface_create (output, PAGE_WIDTH, PAGE_HEIGHT);
503

            
504
    cairo_pdf_surface_restrict_to_version (surface, PDF_VERSION);
505

            
506
    cr = cairo_create (surface);
507

            
508
    cairo_select_font_face (cr, CAIRO_TEST_FONT_FAMILY " Serif",
509
                            CAIRO_FONT_SLANT_NORMAL,
510
                            CAIRO_FONT_WEIGHT_NORMAL);
511
    cairo_set_font_size (cr, 10);
512
    cairo_move_to (cr, 20, 20);
513

            
514
    test->func(cr);
515

            
516
    status = cairo_status (cr);
517
    cairo_destroy (cr);
518
    cairo_surface_finish (surface);
519
    status2 = cairo_surface_status (surface);
520
    if (status == CAIRO_STATUS_SUCCESS)
521
	status = status2;
522

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

            
530
    return CAIRO_TEST_SUCCESS;
531
}
532

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

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

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

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

            
560
    free (command);
561
    return result;
562
}
563

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

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

            
583
/* Need a POSIX shell to run the check. */
584
#ifdef __unix__
585
1
    can_check = TRUE;
586
#endif
587

            
588
1
    all_results = CAIRO_TEST_SUCCESS;
589
1
    if (! cairo_test_is_target_enabled (ctx, "pdf"))
590
1
	return CAIRO_TEST_UNTESTED;
591

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

            
598
        result = create_pdf (ctx, &pdf_structure_tests[i], filename);
599
        merge_test_status (&all_results, result);
600

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

            
609
    free (filename);
610
    return all_results;
611
}
612

            
613
1
CAIRO_TEST (pdf_structure,
614
	    "Check PDF Structure",
615
	    "pdf", /* keywords */
616
	    NULL, /* requirements */
617
	    0, 0,
618
	    preamble, NULL)