From c9187c529eb8574bfdc67c556cb0944a05e697cd Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Sun, 1 Jan 2023 13:50:38 +1030 Subject: [PATCH] Add a test that demonstrates a recording surface bug when re-used on different surfaces There is a bug in the recording surface where if one recording surface is used as the same source on different paginated targets that support fine grained fallbacks, the output may be incorrect because the analysis results of a previous target are re-used on another target. --- test/create-regions.c | 435 ++++++++++++++++++++++ test/meson.build | 1 + test/reference/create-regions.pdf.ref.png | Bin 0 -> 3227 bytes test/reference/create-regions.ps.ref.png | Bin 0 -> 2524 bytes test/reference/create-regions.svg.ref.png | Bin 0 -> 4514 bytes 5 files changed, 436 insertions(+) create mode 100644 test/create-regions.c create mode 100644 test/reference/create-regions.pdf.ref.png create mode 100644 test/reference/create-regions.ps.ref.png create mode 100644 test/reference/create-regions.svg.ref.png diff --git a/test/create-regions.c b/test/create-regions.c new file mode 100644 index 000000000..749595a44 --- /dev/null +++ b/test/create-regions.c @@ -0,0 +1,435 @@ +/* + * Copyright © 2023 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 + */ + +#include "config.h" + +#include +#include +#include +#include + +#if CAIRO_HAS_PDF_SURFACE && CAIRO_HAS_PS_SURFACE && CAIRO_HAS_SVG_SURFACE + +#include +#include +#include + +#ifdef HAVE_UNISTD_H +#include +#include +#endif + +#include "cairo-test.h" +#include "buffer-diff.h" + +/* This test is to ensure that _cairo_recording_surface_replay_and_create_regions() + * works with a recording surface source re-used for multiple paginated surfaces. + * The prior use of the source with a paginated surface should not affect the region + * analysis on subsequent surfaces. + * + * If test output should only contain fallback images for unsupported + * operations. If the recording surface is incorrectly re-using the + * analysis from a different target, some operations may be missing + * from the ouput (recording surface marked the operation as supported + * when it is not) or some operations may be have fallbacks for + * natively suported opetions (recording surface marked a supported + * operation as unsupprted). + * + * To create ref images, run the test for one target at a time to + * prevent re-use of the recording surface for different targets. + */ + + +#define SIZE 40 +#define PAD 25 +#define PAGE_SIZE (SIZE*3 + PAD*4) + +/* Apply a slight rotation and use a very low fallback resolution to + * ensure fallback images are apparent in the output. */ +#define ROTATE 5 +#define FALLBACK_PPI 18 + +static void +create_recordings (cairo_operator_t op, + cairo_surface_t **recording, + cairo_surface_t **recording_group) +{ + *recording = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, NULL); + cairo_t *cr = cairo_create (*recording); + + cairo_rotate (cr, ROTATE*M_PI/180.0); + + cairo_rectangle (cr, 0, 0, SIZE*3.0/4.0, SIZE*3.0/4.0); + cairo_set_source_rgb (cr, 1, 0, 0); + cairo_fill (cr); + + cairo_set_operator (cr, op); + + cairo_rectangle (cr, SIZE/4.0, SIZE/4.0, SIZE*3.0/4.0, SIZE*3.0/4.0); + cairo_set_source_rgb (cr, 0, 1, 0); + cairo_fill (cr); + + cairo_destroy (cr); + + *recording_group = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, NULL); + cr = cairo_create (*recording_group); + + cairo_set_source_surface (cr, *recording, 0, 0); + cairo_paint (cr); + + cairo_destroy (cr); +} + +static void +_xunlink (const cairo_test_context_t *ctx, const char *pathname) +{ + if (unlink (pathname) < 0 && errno != ENOENT) { + cairo_test_log (ctx, "Error: Cannot remove %s: %s\n", + pathname, strerror (errno)); + exit (1); + } +} + +static cairo_bool_t +check_result (cairo_test_context_t *ctx, + const cairo_boilerplate_target_t *target, + const char *test_name, + const char *base_name, + cairo_surface_t *surface) +{ + const char *format; + char *ref_name; + char *png_name; + char *diff_name; + cairo_surface_t *test_image, *ref_image, *diff_image; + buffer_diff_result_t result; + cairo_status_t status; + cairo_bool_t ret; + + if (target->finish_surface != NULL) { + status = target->finish_surface (surface); + if (status) { + cairo_test_log (ctx, "Error: Failed to finish surface: %s\n", + cairo_status_to_string (status)); + cairo_surface_destroy (surface); + return FALSE; + } + } + + xasprintf (&png_name, "%s.out.png", base_name); + xasprintf (&diff_name, "%s.diff.png", base_name); + + test_image = target->get_image_surface (surface, 0, PAGE_SIZE, PAGE_SIZE); + if (cairo_surface_status (test_image)) { + cairo_test_log (ctx, "Error: Failed to extract page: %s\n", + cairo_status_to_string (cairo_surface_status (test_image))); + cairo_surface_destroy (test_image); + free (png_name); + free (diff_name); + return FALSE; + } + + _xunlink (ctx, png_name); + status = cairo_surface_write_to_png (test_image, png_name); + if (status) { + cairo_test_log (ctx, "Error: Failed to write output image: %s\n", + cairo_status_to_string (status)); + cairo_surface_destroy (test_image); + free (png_name); + free (diff_name); + return FALSE; + } + + format = cairo_boilerplate_content_name (target->content); + ref_name = cairo_test_reference_filename (ctx, + base_name, + test_name, + target->name, + target->basename, + format, + CAIRO_TEST_REF_SUFFIX, + CAIRO_TEST_PNG_EXTENSION); + if (ref_name == NULL) { + cairo_test_log (ctx, "Error: Cannot find reference image for %s\n", + base_name); + cairo_surface_destroy (test_image); + free (png_name); + free (diff_name); + return FALSE; + } + + ref_image = cairo_test_get_reference_image (ctx, ref_name, + target->content == CAIRO_TEST_CONTENT_COLOR_ALPHA_FLATTENED); + if (cairo_surface_status (ref_image)) { + cairo_test_log (ctx, "Error: Cannot open reference image for %s: %s\n", + ref_name, + cairo_status_to_string (cairo_surface_status (ref_image))); + cairo_surface_destroy (ref_image); + cairo_surface_destroy (test_image); + free (png_name); + free (diff_name); + free (ref_name); + return FALSE; + } + + diff_image = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + PAGE_SIZE, PAGE_SIZE); + + ret = TRUE; + status = image_diff (ctx, + test_image, ref_image, diff_image, + &result); + _xunlink (ctx, diff_name); + if (status) { + cairo_test_log (ctx, "Error: Failed to compare images: %s\n", + cairo_status_to_string (status)); + ret = FALSE; + } else if (image_diff_is_failure (&result, target->error_tolerance)) + { + ret = FALSE; + + status = cairo_surface_write_to_png (diff_image, diff_name); + if (status) { + cairo_test_log (ctx, "Error: Failed to write differences image: %s\n", + cairo_status_to_string (status)); + } + } + + cairo_surface_destroy (test_image); + cairo_surface_destroy (diff_image); + free (png_name); + free (diff_name); + free (ref_name); + + return ret; +} + +#define TEST_ROWS 2 +#define TEST_COLS 3 + +static void +draw (cairo_t *cr, cairo_surface_t *recordings[TEST_ROWS][TEST_COLS]) +{ + cairo_translate (cr, PAD, PAD); + for (int row = 0; row < TEST_ROWS; row++) { + cairo_save (cr); + for (int col = 0; col < TEST_COLS; col++) { + cairo_save (cr); + cairo_set_source_surface (cr, recordings[row][col], 0, 0); + cairo_paint (cr); + cairo_restore (cr); + cairo_translate (cr, SIZE + PAD, 0); + } + cairo_restore (cr); + cairo_translate (cr, 0, SIZE + PAD); + } +} + +#define NUM_TEST_SURFACES 3 + +typedef struct _test_surface { + const cairo_boilerplate_target_t *target; + cairo_surface_t *surface; + char *base_name; + void *closure; +} test_surface_t; + +static test_surface_t test_surfaces[NUM_TEST_SURFACES]; + +static void +init_test_surfaces() +{ + memset (test_surfaces, 0, sizeof (*test_surfaces)); + test_surfaces[0].surface = cairo_pdf_surface_create_for_stream (NULL, NULL, PAGE_SIZE, PAGE_SIZE); + test_surfaces[1].surface = cairo_ps_surface_create_for_stream (NULL, NULL, PAGE_SIZE, PAGE_SIZE); + test_surfaces[2].surface = cairo_svg_surface_create_for_stream (NULL, NULL, PAGE_SIZE, PAGE_SIZE); +} + +static void +add_test_surface (const cairo_boilerplate_target_t *target, + cairo_surface_t *surface, + char *base_name, + void *closure) +{ + for (int i = 0; i < NUM_TEST_SURFACES; i++) { + if (cairo_surface_get_type (test_surfaces[i].surface) == cairo_surface_get_type (surface)) { + cairo_surface_destroy (test_surfaces[i].surface); + test_surfaces[i].target = target; + test_surfaces[i].surface = surface; + test_surfaces[i].base_name = base_name; + test_surfaces[i].closure = closure; + break; + } + } +} + +static void +destroy_test_surfaces() +{ + for (int i = 0; i < NUM_TEST_SURFACES; i++) { + cairo_surface_destroy (test_surfaces[i].surface); + if (test_surfaces[i].target && test_surfaces[i].target->cleanup) + test_surfaces[i].target->cleanup (test_surfaces[i].closure); + if (test_surfaces[i].base_name) + free (test_surfaces[i].base_name); + } +} + + +static cairo_test_status_t +preamble (cairo_test_context_t *ctx) +{ + cairo_t *cr; + cairo_test_status_t ret = CAIRO_TEST_UNTESTED; + unsigned int i; + const char *path = cairo_test_mkdir (CAIRO_TEST_OUTPUT_DIR) ? CAIRO_TEST_OUTPUT_DIR : "."; + cairo_surface_t *recordings[TEST_ROWS][TEST_COLS]; + const char *test_name = "create-regions"; + char *base_name; + + /* Each row displays three recordings. One with operations + * supported by all paginated surfaces (OVER), one with operations + * supported by PDF but not PS or SVG surfaces (DIFFERENCE), and + * one with operations supported by SVG but not PDF or PS + * (DEST_XOR). + * + * The recordings for the first row is a single recording. The + * recordings for the second row contains the first row recording + * inside another recording to test the use of cloned recording + * surfaces. + * + * We are looking to see that fallback images are only used for + * unsupported operations. + */ + create_recordings (CAIRO_OPERATOR_OVER, &recordings[0][0], &recordings[1][0]); + create_recordings (CAIRO_OPERATOR_DIFFERENCE, &recordings[0][1], &recordings[1][1]); + create_recordings (CAIRO_OPERATOR_XOR, &recordings[0][2], &recordings[1][2]); + + init_test_surfaces(); + for (i = 0; i < ctx->num_targets; i++) { + const cairo_boilerplate_target_t *target = ctx->targets_to_test[i]; + cairo_surface_t *surface = NULL; + void *closure; + const char *format; + + /* This test only works on surfaces that support fine grained fallbacks */ + if (! (target->expected_type == CAIRO_SURFACE_TYPE_PDF || + target->expected_type == CAIRO_SURFACE_TYPE_PS || + target->expected_type ==CAIRO_SURFACE_TYPE_SVG)) + continue; + + if (! cairo_test_is_target_enabled (ctx, target->name)) + continue; + + if (ret == CAIRO_TEST_UNTESTED) + ret = CAIRO_TEST_SUCCESS; + + format = cairo_boilerplate_content_name (target->content); + base_name = NULL; + xasprintf (&base_name, "%s/%s.%s.%s", + path, test_name, + target->name, + format); + + surface = (target->create_surface) (base_name, + target->content, + PAGE_SIZE, PAGE_SIZE, + PAGE_SIZE, PAGE_SIZE, + CAIRO_BOILERPLATE_MODE_TEST, + &closure); + if (surface == NULL || cairo_surface_status (surface)) { + cairo_test_log (ctx, "Failed to generate surface: %s.%s\n", + target->name, + format); + ret = CAIRO_TEST_FAILURE; + break; + } + + cairo_surface_set_fallback_resolution (surface, FALLBACK_PPI, FALLBACK_PPI); + add_test_surface (target, surface, base_name, closure); + } + + for (int i = 0; i < NUM_TEST_SURFACES; i++) { + cairo_status_t status; + + if (test_surfaces[i].target != NULL) { + cairo_test_log (ctx, + "Testing create-regions with %s target\n", + test_surfaces[i].target->name); + printf ("%s:\t", test_surfaces[i].base_name); + fflush (stdout); + } + + cr = cairo_create (test_surfaces[i].surface); + + draw (cr, recordings); + + status = cairo_status (cr); + cairo_destroy (cr); + cairo_surface_finish (test_surfaces[i].surface); + + if (test_surfaces[i].target) { + cairo_bool_t pass = FALSE; + if (status) { + cairo_test_log (ctx, "Error: Failed to create target surface: %s\n", + cairo_status_to_string (status)); + ret = CAIRO_TEST_FAILURE; + } else { + /* extract the image and compare it to our reference */ + if (! check_result (ctx, test_surfaces[i].target, test_name, test_surfaces[i].base_name, test_surfaces[i].surface)) + ret = CAIRO_TEST_FAILURE; + else + pass = TRUE; + } + + if (pass) { + printf ("PASS\n"); + } else { + printf ("FAIL\n"); + } + fflush (stdout); + } + } + + destroy_test_surfaces(); + + for (int row = 0; row < TEST_ROWS; row++) { + for (int col = 0; col < TEST_COLS; col++) { + cairo_surface_destroy (recordings[row][col]); + } + } + + return ret; +} + +CAIRO_TEST (create_regions, + "Check region analysis when re-used with different surfaces", + "fallback", /* keywords */ + NULL, /* requirements */ + 0, 0, + preamble, NULL) + +#endif /* CAIRO_HAS_PDF_SURFACE && CAIRO_HAS_PS_SURFACE && CAIRO_HAS_SVG_SURFACE */ diff --git a/test/meson.build b/test/meson.build index 28047cb04..e852914da 100644 --- a/test/meson.build +++ b/test/meson.build @@ -492,6 +492,7 @@ test_multi_page_sources = [ ] test_fallback_resolution_sources = [ + 'create-regions.c', 'fallback-resolution.c', ] diff --git a/test/reference/create-regions.pdf.ref.png b/test/reference/create-regions.pdf.ref.png new file mode 100644 index 0000000000000000000000000000000000000000..5762fbbc73b3f80622d03fcdcd134a76fe16b45b GIT binary patch literal 3227 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 Wy=#8wlkIm+?wqr`6WQ@%`hNhL`S%e3 literal 0 HcmV?d00001 diff --git a/test/reference/create-regions.svg.ref.png b/test/reference/create-regions.svg.ref.png new file mode 100644 index 0000000000000000000000000000000000000000..a89dfb044623f144922c66be39e6d998042cdc0c GIT binary patch literal 4514 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

Sb|SJnn|v48K(AczIxgz)C) z&A1Q7O@?LtY1v5!cFLb-)1Hc^+-4O_%IoaT>$#J LMmnXpoI?KxI+wOC literal 0 HcmV?d00001