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

#include "cairo-test.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <cairo.h>
#include <cairo-pdf.h>

/* This test checks PDF with
 * - tagged text
 * - hyperlinks
 * - document outline
 * - metadata
 * - thumbnails
 * - page labels
 */

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

#define PAGE_WIDTH 595
#define PAGE_HEIGHT 842

#define HEADING1_SIZE 16
#define HEADING2_SIZE 14
#define HEADING3_SIZE 12
#define TEXT_SIZE 12
#define HEADING_HEIGHT 50
#define MARGIN 50

struct section {
    int level;
    const char *heading;
    int num_paragraphs;
};

static const struct section contents[] = {
    { 0, "Chapter 1",     1 },
    { 1, "Section 1.1",   4 },
    { 2, "Section 1.1.1", 3 },
    { 1, "Section 1.2",   2 },
    { 2, "Section 1.2.1", 4 },
    { 2, "Section 1.2.2", 4 },
    { 1, "Section 1.3",   2 },
    { 0, "Chapter 2",     1 },
    { 1, "Section 2.1",   4 },
    { 2, "Section 2.1.1", 3 },
    { 1, "Section 2.2",   2 },
    { 2, "Section 2.2.1", 4 },
    { 2, "Section 2.2.2", 4 },
    { 1, "Section 2.3",   2 },
    { 0, "Chapter 3",     1 },
    { 1, "Section 3.1",   4 },
    { 2, "Section 3.1.1", 3 },
    { 1, "Section 3.2",   2 },
    { 2, "Section 3.2.1", 4 },
    { 2, "Section 3.2.2", 4 },
    { 1, "Section 3.3",   2 },
    { 0, NULL }
};

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

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

#define MAX_PARAGRAPH_LINES 20

static int paragraph_num_lines;
static char *paragraph_text[MAX_PARAGRAPH_LINES];
static double paragraph_height;
static double line_height;
static double y_pos;
static int outline_parents[10];
static int page_num;

static void
layout_paragraph (cairo_t *cr)
{
    char *text, *begin, *end, *prev_end;
    cairo_text_extents_t text_extents;
    cairo_font_extents_t font_extents;

    cairo_select_font_face (cr, "Serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, TEXT_SIZE);
    cairo_font_extents (cr, &font_extents);
    line_height = font_extents.height;
    paragraph_height = 0;
    paragraph_num_lines = 0;
    text = strdup (ipsum_lorem);
    begin = text;
    end = text;
    prev_end = end;
    while (*begin) {
	end = strchr(end, ' ');
	if (!end) {
	    paragraph_text[paragraph_num_lines++] = strdup (begin);
	    break;
	}
	*end = 0;
	cairo_text_extents (cr, begin, &text_extents);
	*end = ' ';
	if (text_extents.width + 2*MARGIN > PAGE_WIDTH) {
	    int len = prev_end - begin;
	    char *s = malloc (len);
	    memcpy (s, begin, len);
	    s[len-1] = 0;
	    paragraph_text[paragraph_num_lines++] = s;
	    begin = prev_end + 1;
	}
	prev_end = end;
	end++;
    }
    paragraph_height = line_height * (paragraph_num_lines + 1);
    free (text);
}

static void
draw_paragraph (cairo_t *cr)
{
    int i;

    cairo_select_font_face (cr, "Serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, TEXT_SIZE);
    cairo_tag_begin (cr, "P", NULL);
    for (i = 0; i < paragraph_num_lines; i++) {
	cairo_move_to (cr, MARGIN, y_pos);
	cairo_show_text (cr, paragraph_text[i]);
	y_pos += line_height;
    }
    cairo_tag_end (cr, "P");
    y_pos += line_height;
}

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

    buf[0] = 0;
    if (prefix)
	strcat (buf, prefix);

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

    cairo_save (cr);
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, 12);
    cairo_move_to (cr, PAGE_WIDTH/2, PAGE_HEIGHT - MARGIN);
    cairo_show_text (cr, buf);
    cairo_restore (cr);
    cairo_pdf_surface_set_page_label (surface, buf);
}

static void
draw_contents (cairo_surface_t *surface, cairo_t *cr, const struct section *section)
{
    char buf[100];

    sprintf(buf, "dest='%s'", section->heading);
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    switch (section->level) {
	case 0:
	    cairo_set_font_size(cr, HEADING1_SIZE);
	    break;
	case 1:
	    cairo_set_font_size(cr, HEADING2_SIZE);
	    break;
	case 2:
	    cairo_set_font_size(cr, HEADING3_SIZE);
	    break;
    }

    if (y_pos + HEADING_HEIGHT + MARGIN > PAGE_HEIGHT) {
	cairo_show_page (cr);
	draw_page_num (surface, cr, roman_numerals[page_num++], 0);
	y_pos = MARGIN;
    }
    cairo_move_to (cr, MARGIN, y_pos);
    cairo_save (cr);
    cairo_set_source_rgb (cr, 0, 0, 1);
    cairo_tag_begin (cr, "TOCI", NULL);
    cairo_tag_begin (cr, "Reference", NULL);
    cairo_tag_begin (cr, CAIRO_TAG_LINK, buf);
    cairo_show_text (cr, section->heading);
    cairo_tag_end (cr, CAIRO_TAG_LINK);
    cairo_tag_end (cr, "Reference");
    cairo_tag_end (cr, "TOCI");
    cairo_restore (cr);
    y_pos += HEADING_HEIGHT;
}

static void
draw_section (cairo_surface_t *surface, cairo_t *cr, const struct section *section)
{
    int flags, i;
    char buf[100];
    char buf2[100];

    cairo_tag_begin (cr, "Sect", NULL);
    sprintf(buf, "name='%s'", section->heading);
    sprintf(buf2, "dest='%s'", section->heading);
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
    if (section->level == 0) {
	cairo_show_page (cr);
	draw_page_num (surface, cr, NULL, page_num++);
	cairo_set_font_size(cr, HEADING1_SIZE);
	cairo_move_to (cr, MARGIN, MARGIN);
	cairo_tag_begin (cr, "H1", NULL);
	cairo_tag_begin (cr, CAIRO_TAG_DEST, buf);
	cairo_show_text (cr, section->heading);
	cairo_tag_end (cr, CAIRO_TAG_DEST);
	cairo_tag_end (cr, "H1");
	y_pos = MARGIN + HEADING_HEIGHT;
	flags = CAIRO_PDF_OUTLINE_FLAG_BOLD | CAIRO_PDF_OUTLINE_FLAG_OPEN;
	outline_parents[0] = cairo_pdf_surface_add_outline (surface,
							    CAIRO_PDF_OUTLINE_ROOT,
							    section->heading,
							    buf2,
							    flags);
    } else {
	if (section->level == 1) {
	    cairo_set_font_size(cr, HEADING2_SIZE);
	    flags = 0;
	} else {
	    cairo_set_font_size(cr, HEADING3_SIZE);
	    flags = CAIRO_PDF_OUTLINE_FLAG_ITALIC;
	}

	if (y_pos + HEADING_HEIGHT + paragraph_height + MARGIN > PAGE_HEIGHT) {
	    cairo_show_page (cr);
	    draw_page_num (surface, cr, NULL, page_num++);
	    y_pos = MARGIN;
	}
	cairo_move_to (cr, MARGIN, y_pos);
	if (section->level == 1)
	    cairo_tag_begin (cr, "H2", NULL);
	else
	    cairo_tag_begin (cr, "H3", NULL);
	cairo_tag_begin (cr, CAIRO_TAG_DEST, buf);
	cairo_show_text (cr, section->heading);
	cairo_tag_end (cr, CAIRO_TAG_DEST);
	if (section->level == 1)
	    cairo_tag_end (cr, "H2");
	else
	    cairo_tag_end (cr, "H3");
	y_pos += HEADING_HEIGHT;
	outline_parents[section->level] = cairo_pdf_surface_add_outline (surface,
									 outline_parents[section->level - 1],
									 section->heading,
									 buf2,
									 flags);
    }

    for (i = 0; i < section->num_paragraphs; i++) {
	if (y_pos + paragraph_height + MARGIN > PAGE_HEIGHT) {
	    cairo_show_page (cr);
	    draw_page_num (surface, cr, NULL, page_num++);
		y_pos = MARGIN;
	}
	draw_paragraph (cr);
    }
    cairo_tag_end (cr, "Sect");
}

static void
draw_cover (cairo_surface_t *surface, cairo_t *cr)
{
    cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
    cairo_set_font_size(cr, 16);
    cairo_move_to (cr, PAGE_WIDTH/3, PAGE_HEIGHT/2);
    cairo_tag_begin (cr, "Span", NULL);
    cairo_show_text (cr, "PDF Features Test");
    cairo_tag_end (cr, "Span");

    draw_page_num (surface, cr, "cover", 0);
}

static void
create_document (cairo_surface_t *surface, cairo_t *cr)
{
    layout_paragraph (cr);

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

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

    cairo_tag_begin (cr, "Document", NULL);

    draw_cover (surface, cr);
    cairo_pdf_surface_add_outline (surface,
				   CAIRO_PDF_OUTLINE_ROOT,
				   "Cover", "page=1",
                                   CAIRO_PDF_OUTLINE_FLAG_BOLD);
    cairo_show_page (cr);

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

    cairo_pdf_surface_add_outline (surface,
				   CAIRO_PDF_OUTLINE_ROOT,
				   "Contents", "dest='TOC'",
                                   CAIRO_PDF_OUTLINE_FLAG_BOLD);

    cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='TOC' internal");
    cairo_tag_begin (cr, "TOC", NULL);
    const struct section *sect = contents;
    while (sect->heading) {
	draw_contents (surface, cr, sect);
	sect++;
    }
    cairo_tag_end (cr, "TOC");
    cairo_tag_end (cr, CAIRO_TAG_DEST);

    page_num = 1;
    sect = contents;
    while (sect->heading) {
	draw_section (surface, cr, sect);
	sect++;
    }

    cairo_tag_end (cr, "Document");
}

static cairo_test_status_t
preamble (cairo_test_context_t *ctx)
{
    cairo_surface_t *surface;
    cairo_t *cr;
    cairo_status_t status, status2;
    char *filename;
    const char *path = cairo_test_mkdir (CAIRO_TEST_OUTPUT_DIR) ? CAIRO_TEST_OUTPUT_DIR : ".";

    if (! cairo_test_is_target_enabled (ctx, "pdf"))
	return CAIRO_TEST_UNTESTED;

    xasprintf (&filename, "%s/%s.pdf", path, BASENAME);
    surface = cairo_pdf_surface_create (filename, PAGE_WIDTH, PAGE_HEIGHT);

    cr = cairo_create (surface);
    create_document (surface, cr);

    status = cairo_status (cr);
    cairo_destroy (cr);
    cairo_surface_finish (surface);
    status2 = cairo_surface_status (surface);
    if (status != CAIRO_STATUS_SUCCESS)
	status = status2;

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

    free (filename);

    return CAIRO_TEST_SUCCESS;
}

CAIRO_TEST (pdf_tagged_text,
	    "Check tagged text, hyperlinks and PDF document features",
	    "pdf", /* keywords */
	    NULL, /* requirements */
	    0, 0,
	    preamble, NULL)