/*
* Copyright © 2017 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>
*/
/* Check that source surfaces with same CAIRO_MIME_TYPE_UNIQUE_ID are
* embedded only once in PDF/PS.
*
* To exercise all the surface embedding code in PS/PDF, four types of
* source surfaces are painted on each page, each with its own UNIQUE_ID:
* - an image surface
* - a recording surface with a jpeg mime attached
* - a bounded recording surface
* - an unbounded recording surface.
*
* Four pages are generated. Each source is clipped starting with the
* smallest area on the first page increasing to the unclipped size on
* the last page. This is to ensure the output does not embed the
* source clipped to a smaller size than used on subsequent pages.
*
* The test verifies the use of UNIQUE_ID by comparing the file size
* with the expected size.
*/
#include "cairo-test.h"
#include <math.h>
#include <stdio.h>
#include <cairo.h>
#if CAIRO_HAS_PS_SURFACE
#include <cairo-ps.h>
#endif
#if CAIRO_HAS_PDF_SURFACE
#include <cairo-pdf.h>
#endif
#define NUM_PAGES 4
#define WIDTH 275
#define HEIGHT 275
#define BASENAME "mime-unique-id"
/* Expected file size to check that surfaces are embedded only once.
* SIZE_TOLERANCE should be large enough to allow some variation in
* file size due to changes to the PS/PDF surfaces while being small
* enough to catch any attempt to embed the surface more than
* once. The compressed size of each surface embedded in PDF is:
* - image: 108,774
* - jpeg: 11,400
* - recording: 17,518
*
* If the size check fails, manually check the output and if the
* surfaces are still embedded only once, update the expected sizes.
*/
#define PS2_EXPECTED_SIZE 417510
#define PS3_EXPECTED_SIZE 381554
#define PDF_EXPECTED_SIZE 347182
#define SIZE_TOLERANCE 5000
static const char *png_filename = "romedalen.png";
static const char *jpeg_filename = "romedalen.jpg";
static cairo_test_status_t
create_image_surface (cairo_test_context_t *ctx, cairo_surface_t **surface)
{
cairo_status_t status;
const char *unique_id = "image";
*surface = cairo_test_create_surface_from_png (ctx, png_filename);
status = cairo_surface_set_mime_data (*surface, CAIRO_MIME_TYPE_UNIQUE_ID,
(unsigned char *)unique_id,
strlen (unique_id),
NULL, NULL);
if (status) {
cairo_surface_destroy (*surface);
return cairo_test_status_from_status (ctx, status);
}
return CAIRO_TEST_SUCCESS;
}
static cairo_test_status_t
create_recording_surface_with_mime_jpg (cairo_test_context_t *ctx, cairo_surface_t **surface)
{
cairo_status_t status;
FILE *f;
unsigned char *data;
long len;
const char *unique_id = "jpeg";
cairo_rectangle_t extents = { 0, 0, 1, 1 };
*surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, &extents);
f = fopen (jpeg_filename, "rb");
if (f == NULL) {
cairo_test_log (ctx, "Unable to open file %s\n", jpeg_filename);
return CAIRO_TEST_FAILURE;
}
fseek (f, 0, SEEK_END);
len = ftell(f);
fseek (f, 0, SEEK_SET);
data = malloc (len);
if (fread(data, len, 1, f) != 1) {
cairo_test_log (ctx, "Unable to read file %s\n", jpeg_filename);
return CAIRO_TEST_FAILURE;
}
fclose(f);
status = cairo_surface_set_mime_data (*surface,
CAIRO_MIME_TYPE_JPEG,
data, len,
free, data);
if (status) {
cairo_surface_destroy (*surface);
return cairo_test_status_from_status (ctx, status);
}
status = cairo_surface_set_mime_data (*surface, CAIRO_MIME_TYPE_UNIQUE_ID,
(unsigned char *)unique_id,
strlen (unique_id),
NULL, NULL);
if (status) {
cairo_surface_destroy (*surface);
return cairo_test_status_from_status (ctx, status);
}
return CAIRO_TEST_SUCCESS;
}
static void
draw_tile (cairo_t *cr)
{
cairo_move_to (cr, 10 + 5, 10);
cairo_arc (cr, 10, 10, 5, 0, 2*M_PI);
cairo_close_path (cr);
cairo_set_source_rgb (cr, 1, 0, 0);
cairo_fill (cr);
cairo_move_to (cr, 30, 10-10*0.43);
cairo_line_to (cr, 25, 10+10*0.43);
cairo_line_to (cr, 35, 10+10*0.43);
cairo_close_path (cr);
cairo_set_source_rgb (cr, 0, 1, 0);
cairo_fill (cr);
cairo_rectangle (cr, 5, 25, 10, 10);
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_fill (cr);
cairo_save (cr);
cairo_translate (cr, 30, 30);
cairo_rotate (cr, M_PI/4.0);
cairo_rectangle (cr, -5, -5, 10, 10);
cairo_set_source_rgb (cr, 1, 0, 1);
cairo_fill (cr);
cairo_restore (cr);
}
#define RECORDING_SIZE 800
#define TILE_SIZE 40
static cairo_test_status_t
create_recording_surface (cairo_test_context_t *ctx, cairo_surface_t **surface, cairo_bool_t bounded)
{
cairo_status_t status;
int x, y;
cairo_t *cr;
cairo_matrix_t ctm;
int start, size;
const char *bounded_id = "recording bounded";
const char *unbounded_id = "recording unbounded";
cairo_rectangle_t extents = { 0, 0, RECORDING_SIZE, RECORDING_SIZE };
if (bounded) {
*surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, &extents);
start = 0;
size = RECORDING_SIZE;
} else {
*surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, NULL);
start = RECORDING_SIZE / 2;
size = RECORDING_SIZE * 2;
}
/* Draw each tile instead of creating a cairo pattern to make size
* of the emitted recording as large as possible.
*/
cr = cairo_create (*surface);
cairo_set_source_rgb (cr, 1, 1, 0);
cairo_paint (cr);
cairo_get_matrix (cr, &ctm);
for (y = start; y < size; y += TILE_SIZE) {
for (x = start; x < size; x += TILE_SIZE) {
draw_tile (cr);
cairo_translate (cr, TILE_SIZE, 0);
}
cairo_matrix_translate (&ctm, 0, TILE_SIZE);
cairo_set_matrix (cr, &ctm);
}
cairo_destroy (cr);
status = cairo_surface_set_mime_data (*surface, CAIRO_MIME_TYPE_UNIQUE_ID,
(unsigned char *)(bounded ? bounded_id : unbounded_id),
strlen (bounded ? bounded_id : unbounded_id),
NULL, NULL);
if (status) {
cairo_surface_destroy (*surface);
return cairo_test_status_from_status (ctx, status);
}
return CAIRO_TEST_SUCCESS;
}
/* Draw @source scaled to fit @rect and clipped to a rectangle
* @clip_margin units smaller on each side. @rect will be stroked
* with a solid line and the clip rect stroked with a dashed line.
*/
static void
draw_surface (cairo_t *cr, cairo_surface_t *source, cairo_rectangle_int_t *rect, int clip_margin)
{
cairo_surface_type_t type;
int width, height;
cairo_rectangle_t extents;
const double dashes[2] = { 2, 2 };
type = cairo_surface_get_type (source);
if (type == CAIRO_SURFACE_TYPE_IMAGE) {
width = cairo_image_surface_get_width (source);
height = cairo_image_surface_get_height (source);
} else {
if (cairo_recording_surface_get_extents (source, &extents)) {
width = extents.width;
height = extents.height;
} else {
width = RECORDING_SIZE;
height = RECORDING_SIZE;
}
}
cairo_save (cr);
cairo_rectangle (cr, rect->x, rect->y, rect->width, rect->height);
cairo_stroke (cr);
cairo_rectangle (cr,
rect->x + clip_margin,
rect->y + clip_margin,
rect->width - clip_margin*2,
rect->height - clip_margin*2);
cairo_set_dash (cr, dashes, 2, 0);
cairo_stroke_preserve (cr);
cairo_clip (cr);
cairo_translate (cr, rect->x, rect->y);
cairo_scale (cr, (double)rect->width/width, (double)rect->height/height);
cairo_set_source_surface (cr, source, 0, 0);
cairo_paint (cr);
cairo_restore (cr);
}
static cairo_test_status_t
draw_pages (cairo_test_context_t *ctx, cairo_surface_t *surface)
{
cairo_t *cr;
int i;
cairo_rectangle_int_t img_rect;
cairo_rectangle_int_t jpg_rect;
cairo_rectangle_int_t bounded_rect;
cairo_rectangle_int_t unbounded_rect;
int clip_margin;
cairo_surface_t *source;
cairo_test_status_t status;
cr = cairo_create (surface);
/* target area to fill with the image source */
img_rect.x = 25;
img_rect.y = 25;
img_rect.width = 100;
img_rect.height = 100;
/* target area to fill with the recording with jpeg mime source */
jpg_rect.x = 150;
jpg_rect.y = 25;
jpg_rect.width = 100;
jpg_rect.height = 100;
/* target area to fill with the bounded recording source */
bounded_rect.x = 25;
bounded_rect.y = 150;
bounded_rect.width = 100;
bounded_rect.height = 100;
/* target area to fill with the unbounded recording source */
unbounded_rect.x = 150;
unbounded_rect.y = 150;
unbounded_rect.width = 100;
unbounded_rect.height = 100;
/* Draw the image and recording surface on each page. The sources
* are clipped starting with a small clip area on the first page
* and increasing to the source size on last page to ensure the
* embedded source is not clipped to the area used on the first
* page.
*
* The sources are created each time they are used to ensure
* CAIRO_MIME_TYPE_UNIQUE_ID is tested.
*/
for (i = 0; i < NUM_PAGES; i++) {
clip_margin = (NUM_PAGES - i - 1) * 5;
status = create_image_surface (ctx, &source);
if (status)
return status;
draw_surface (cr, source, &img_rect, clip_margin);
cairo_surface_destroy (source);
status = create_recording_surface_with_mime_jpg (ctx, &source);
if (status)
return status;
draw_surface (cr, source, &jpg_rect, clip_margin);
cairo_surface_destroy (source);
status = create_recording_surface (ctx, &source, TRUE);
if (status)
return status;
draw_surface (cr, source, &bounded_rect, clip_margin);
cairo_surface_destroy (source);
status = create_recording_surface (ctx, &source, FALSE);
if (status)
return status;
draw_surface (cr, source, &unbounded_rect, clip_margin);
cairo_surface_destroy (source);
cairo_show_page (cr);
}
cairo_destroy (cr);
return CAIRO_TEST_SUCCESS;
}
static cairo_test_status_t
check_file_size (cairo_test_context_t *ctx, const char *filename, long expected_size)
{
FILE *f;
long size;
f = fopen (filename, "rb");
if (f == NULL) {
cairo_test_log (ctx, "Unable to open file %s\n", filename);
return CAIRO_TEST_FAILURE;
}
fseek (f, 0, SEEK_END);
size = ftell (f);
fclose(f);
if (labs(size - expected_size) > SIZE_TOLERANCE) {
cairo_test_log (ctx,
"mime-unique-id: File %s has size %ld. Expected size %ld +/- %ld."
" Check if surfaces are embedded once.\n",
filename, size, expected_size, (long)SIZE_TOLERANCE);
return CAIRO_TEST_FAILURE;
}
return CAIRO_TEST_SUCCESS;
}
static cairo_test_status_t
preamble (cairo_test_context_t *ctx)
{
cairo_surface_t *surface;
cairo_status_t status;
char *filename;
cairo_test_status_t result = CAIRO_TEST_UNTESTED;
cairo_test_status_t test_status;
const char *path = cairo_test_mkdir (CAIRO_TEST_OUTPUT_DIR) ? CAIRO_TEST_OUTPUT_DIR : ".";
#if CAIRO_HAS_PS_SURFACE
if (cairo_test_is_target_enabled (ctx, "ps2"))
{
xasprintf (&filename, "%s/%s.ps2.out.ps", path, BASENAME);
surface = cairo_ps_surface_create (filename, WIDTH, HEIGHT);
status = cairo_surface_status (surface);
if (status) {
cairo_test_log (ctx, "Failed to create ps surface for file %s: %s\n",
filename, cairo_status_to_string (status));
test_status = CAIRO_TEST_FAILURE;
goto ps2_finish;
}
cairo_ps_surface_restrict_to_level (surface, CAIRO_PS_LEVEL_2);
test_status = draw_pages (ctx, surface);
cairo_surface_destroy (surface);
if (test_status == CAIRO_TEST_SUCCESS)
test_status = check_file_size (ctx, filename, PS2_EXPECTED_SIZE);
ps2_finish:
cairo_test_log (ctx, "TEST: %s TARGET: %s RESULT: %s\n",
ctx->test->name,
"ps2",
test_status ? "FAIL" : "PASS");
if (result == CAIRO_TEST_UNTESTED || test_status == CAIRO_TEST_FAILURE)
result = test_status;
free (filename);
}
if (cairo_test_is_target_enabled (ctx, "ps3"))
{
xasprintf (&filename, "%s/%s.ps3.out.ps", path, BASENAME);
surface = cairo_ps_surface_create (filename, WIDTH, HEIGHT);
status = cairo_surface_status (surface);
if (status) {
cairo_test_log (ctx, "Failed to create ps surface for file %s: %s\n",
filename, cairo_status_to_string (status));
test_status = CAIRO_TEST_FAILURE;
goto ps3_finish;
}
test_status = draw_pages (ctx, surface);
cairo_surface_destroy (surface);
if (test_status == CAIRO_TEST_SUCCESS)
test_status = check_file_size (ctx, filename, PS3_EXPECTED_SIZE);
ps3_finish:
cairo_test_log (ctx, "TEST: %s TARGET: %s RESULT: %s\n",
ctx->test->name,
"ps3",
test_status ? "FAIL" : "PASS");
if (result == CAIRO_TEST_UNTESTED || test_status == CAIRO_TEST_FAILURE)
result = test_status;
free (filename);
}
#endif
#if CAIRO_HAS_PDF_SURFACE
if (cairo_test_is_target_enabled (ctx, "pdf"))
{
xasprintf (&filename, "%s/%s.pdf.out.pdf", path, BASENAME);
surface = cairo_pdf_surface_create (filename, WIDTH, HEIGHT);
status = cairo_surface_status (surface);
if (status) {
cairo_test_log (ctx, "Failed to create pdf surface for file %s: %s\n",
filename, cairo_status_to_string (status));
test_status = CAIRO_TEST_FAILURE;
goto pdf_finish;
}
test_status = draw_pages (ctx, surface);
cairo_surface_destroy (surface);
if (test_status == CAIRO_TEST_SUCCESS)
test_status = check_file_size (ctx, filename, PDF_EXPECTED_SIZE);
pdf_finish:
cairo_test_log (ctx, "TEST: %s TARGET: %s RESULT: %s\n",
ctx->test->name,
"pdf",
test_status ? "FAIL" : "PASS");
if (result == CAIRO_TEST_UNTESTED || test_status == CAIRO_TEST_FAILURE)
result = test_status;
free (filename);
}
#endif
return result;
}
CAIRO_TEST (mime_unique_id,
"Check that paginated surfaces embed only one copy of surfaces with the same CAIRO_MIME_TYPE_UNIQUE_ID.",
"paginated", /* keywords */
"target=vector", /* requirements */
0, 0,
preamble, NULL)