/* Lasem - SVG and Mathml library * * lsm-test - Regression test utility for Lasem * * Copyright © 2004 Richard D. Worth * Copyright © 2006 Red Hat, Inc. * Copyright © 2007-2012 Emmanuel Pacaud * * Permission to use, copy, modify, distribute, and sell this software * and its documentation for any purpose is hereby granted without * fee, provided that the above copyright notice appear in all copies * and that both that copyright notice and this permission notice * appear in supporting documentation, and that the name of the authors * not be used in advertising or publicity pertaining to distribution * of the software without specific, written prior permission. * The authors make no representations about the suitability of this * software for any purpose. It is provided "as is" without express * or implied warranty. * * THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN * NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * Authors: Emmanuel Pacaud <emmanuel@gnome.org> * Richard D. Worth <richard@theworths.org> * Carl Worth <cworth@cworth.org> */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <lsmmathml.h> #include <glib.h> #include <glib/gprintf.h> #include <gio/gio.h> #include <libxml/parser.h> #include <../itex2mml/itex2MML.h> #define XML_FILENAME "lasemtest.xml" static char *option_debug_domains = NULL; static char **option_input_filenames = NULL; double option_ppi = 72.0; static gboolean option_fatal_warning = FALSE; static gboolean option_debug_filter = FALSE; static gboolean option_debug_pattern = FALSE; static gboolean option_debug_mask = FALSE; static gboolean option_debug_group = FALSE; static gboolean option_debug_text = FALSE; static gboolean option_dry_run = FALSE; static double option_compare_fuzz = 10.0; static const GOptionEntry entries[] = { { G_OPTION_REMAINING, ' ', 0, G_OPTION_ARG_FILENAME_ARRAY, &option_input_filenames, NULL, NULL}, { "ppi", 'p', 0, G_OPTION_ARG_DOUBLE, &option_ppi, "Pixel per inch", NULL }, { "debug", 'd', 0, G_OPTION_ARG_STRING, &option_debug_domains, "Debug domains", NULL }, { "fatal-warning", 'f', 0, G_OPTION_ARG_NONE, &option_fatal_warning, "Make warning fatal", NULL }, { "debug-filter", ' ' , 0, G_OPTION_ARG_NONE, &option_debug_filter, "Debug filter surfaces", NULL }, { "debug-pattern", ' ' , 0, G_OPTION_ARG_NONE, &option_debug_pattern, "Debug pattern surfaces", NULL }, { "debug-mask", ' ' , 0, G_OPTION_ARG_NONE, &option_debug_mask, "Debug mask surfaces", NULL }, { "debug-group", ' ' , 0, G_OPTION_ARG_NONE, &option_debug_group, "Debug group surfaces", NULL }, { "debug-text", ' ' , 0, G_OPTION_ARG_NONE, &option_debug_text, "Debug text layout", NULL }, { "dry-run", 'n' , 0, G_OPTION_ARG_NONE, &option_dry_run, "Don't write files", NULL }, { "compare-fuzz", 'z', 0, G_OPTION_ARG_DOUBLE, &option_compare_fuzz, "Compare fuzz", NULL}, { NULL } }; static const char *fail_face = ""; static const char *success_face = ""; static const char *normal_face = ""; FILE *lasem_test_html_file = NULL; typedef struct { double elapsed_time; unsigned int rendered_count; unsigned int comparison_count; unsigned int failed_count; unsigned int success_count; } Statistic; static void __attribute__((format(printf,1,2))) lasem_test_html (const char *fmt, ...) { va_list va; FILE *file = lasem_test_html_file ? lasem_test_html_file : stdout; va_start (va, fmt); vfprintf (file, fmt, va); va_end (va); } static GRegex *regex_mml = NULL; typedef struct _buffer_diff_result { unsigned int pixels_changed; unsigned int max_diff; } buffer_diff_result_t; typedef guint32 pixman_bits_t; static void buffer_diff_core (unsigned char *_buf_a, unsigned char *_buf_b, unsigned char *_buf_diff, int width, int height, int stride, pixman_bits_t mask, buffer_diff_result_t *result_ret) { int x, y; pixman_bits_t *row_a, *row_b, *row; buffer_diff_result_t result = {0, 0}; pixman_bits_t *buf_a = (pixman_bits_t*)_buf_a; pixman_bits_t *buf_b = (pixman_bits_t*)_buf_b; pixman_bits_t *buf_diff = (pixman_bits_t*)_buf_diff; stride /= sizeof(pixman_bits_t); for (y = 0; y < height; y++) { row_a = buf_a + y * stride; row_b = buf_b + y * stride; row = buf_diff + y * stride; for (x = 0; x < width; x++) { /* check if the pixels are the same */ if ((row_a[x] & mask) != (row_b[x] & mask)) { int channel; pixman_bits_t diff_pixel = 0; /* calculate a difference value for all 4 channels */ for (channel = 0; channel < 4; channel++) { int value_a = (row_a[x] >> (channel*8)) & 0xff; int value_b = (row_b[x] >> (channel*8)) & 0xff; unsigned int diff; diff = abs (value_a - value_b); if (diff > result.max_diff) result.max_diff = diff; diff *= 4; /* emphasize */ if (diff) diff += 128; /* make sure it's visible */ if (diff > 255) diff = 255; diff_pixel |= diff << (channel*8); } result.pixels_changed++; row[x] = diff_pixel; } else { row[x] = 0; } row[x] |= 0xff000000; /* Set ALPHA to 100% (opaque) */ } } *result_ret = result; } static gboolean compare_surfaces (const char *test_name, cairo_surface_t *surface_a, cairo_surface_t *surface_b) { int width_a, width_b, height_a, height_b, stride_a, stride_b; if (surface_b == NULL) return FALSE; if (surface_a == NULL) return FALSE; width_a = cairo_image_surface_get_width (surface_a); height_a = cairo_image_surface_get_height (surface_a); stride_a = cairo_image_surface_get_stride (surface_a); width_b = cairo_image_surface_get_width (surface_b); height_b = cairo_image_surface_get_height (surface_b); stride_b = cairo_image_surface_get_stride (surface_b); if (width_a == width_b && height_a == height_b && stride_a == stride_b) { buffer_diff_result_t result; cairo_surface_t *surface_diff; char *diff_png_filename; char *command; char *command_result = NULL; int command_status; diff_png_filename = g_strdup_printf ("%s-diff.png", test_name); surface_diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width_a, height_a); buffer_diff_core (cairo_image_surface_get_data (surface_a), cairo_image_surface_get_data (surface_b), cairo_image_surface_get_data (surface_diff), cairo_image_surface_get_width (surface_a), cairo_image_surface_get_height (surface_a), cairo_image_surface_get_stride (surface_a), 0xffffffff, &result); cairo_surface_write_to_png (surface_diff, diff_png_filename); cairo_surface_destroy (surface_diff); g_free (diff_png_filename); if (result.pixels_changed == 0) { g_printf (" %sOK%s \n", success_face, normal_face); return TRUE; } command = g_strdup_printf ("compare -metric AE -fuzz %g%% %s-out.png %s-ref.png %s-compare-diff.png", option_compare_fuzz, test_name, test_name, test_name); g_spawn_command_line_sync (command, NULL, &command_result, &command_status, NULL); g_free (command); if (command_result != NULL && atoi (command_result) == 0) { g_printf (" %sOK%s \n", success_face, normal_face); return TRUE; } } g_printf (" %sFAIL%s \n", fail_face, normal_face); return FALSE; } static void lasem_test_render (char const *filename, gboolean compare, gboolean dry_run, gboolean save_png, Statistic *statistic) { LsmDomDocument *document; LsmDomView *view; GTimer *timer; cairo_t *cairo; cairo_surface_t *surface; char *buffer = NULL; gsize size; char *png_filename; char *reference_png_filename; char *test_name; char *mime; unsigned int width, height; gboolean is_xml, success; gboolean is_svg; gboolean is_mathml; gboolean check; GRegex *regex; GError *error = NULL; char *filtered_buffer; g_return_if_fail (statistic != NULL); test_name = g_regex_replace (regex_mml, filename, -1, 0, "", 0, NULL); png_filename = g_strdup_printf ("%s-out.png", test_name); reference_png_filename = g_strdup_printf ("%s-ref.png", test_name); if (g_file_test (reference_png_filename, G_FILE_TEST_IS_REGULAR)) { check = compare; } else { g_free (reference_png_filename); reference_png_filename = g_strdup_printf ("%s.png", test_name); check = FALSE; } mime = g_content_type_guess (filename, NULL, 0, NULL); is_svg = strcmp (mime, "image/svg+xml") == 0; is_mathml = (strcmp (mime, "text/mathml") == 0) || (strcmp (mime, "application/mathml+xml") == 0); is_xml = is_svg || is_mathml; g_printf ("\trender %s (%s)", filename, mime); g_free (mime); success = g_file_get_contents (filename, &buffer, &size, NULL); if (success) { LsmBox viewport; char *xml; if (is_xml) xml = buffer; else { xml = itex2MML_parse (buffer, size); size = -1; } timer = g_timer_new (); document = lsm_dom_document_new_from_memory (xml, size, NULL); lsm_dom_document_set_path (document, filename); view = lsm_dom_document_create_view (document); viewport.x = 0.0; viewport.y = 0.0; viewport.width = 480.0; viewport.height = 360.0; lsm_dom_view_set_resolution (view, option_ppi); lsm_dom_view_set_viewport_pixels (view, &viewport); lsm_dom_view_get_size_pixels (LSM_DOM_VIEW (view), &width, &height, NULL); if (option_debug_mask) lsm_dom_view_set_debug (view, "mask", TRUE); if (option_debug_pattern) lsm_dom_view_set_debug (view, "pattern", TRUE); if (option_debug_filter) lsm_dom_view_set_debug (view, "filter", TRUE); if (option_debug_group) lsm_dom_view_set_debug (view, "group", TRUE); if (option_debug_text) lsm_dom_view_set_debug (view, "text", TRUE); surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width + 2, height + 2); cairo = cairo_create (surface); cairo_surface_destroy (surface); lsm_dom_view_render (LSM_DOM_VIEW (view), cairo, 1, 1); statistic->elapsed_time += g_timer_elapsed (timer, NULL); statistic->rendered_count++; g_timer_destroy (timer); if (!dry_run && save_png) cairo_surface_write_to_png (surface, png_filename); if (check) { cairo_surface_t *reference_surface; reference_surface = cairo_image_surface_create_from_png (reference_png_filename); if (reference_surface != NULL) { gboolean same; same = compare_surfaces (test_name, surface, reference_surface); cairo_surface_destroy (reference_surface); if (same) statistic->success_count++; else statistic->failed_count++; statistic->comparison_count++; } } else g_printf ("\n"); cairo_destroy (cairo); g_object_unref (view); g_object_unref (document); if (save_png) { lasem_test_html ("<table border=\"1\" cellpadding=\"8\">\n"); lasem_test_html ("<tr>"); lasem_test_html ("<td><a href=\"%s\"><img border=\"0\" src=\"%s\"/></a></td>", filename, png_filename); lasem_test_html ("<td><img src=\"%s\"/></td>", reference_png_filename); lasem_test_html ("<td>"); if (is_mathml) { regex = g_regex_new ("<math>", 0, 0, &error); assert (error == NULL); filtered_buffer = g_regex_replace (regex, xml, -1, 0, "<math xmlns=\"http://www.w3.org/1998/Math/MathML\">", 0, NULL); g_regex_unref (regex); lasem_test_html ("%s", filtered_buffer); g_free (filtered_buffer); } if (is_svg) { lasem_test_html ("<object type=\"image/svg+xml\" data=\""); lasem_test_html ("%s", filename); lasem_test_html ("\" width=\"%dpx\"/>", width + 2); } lasem_test_html ("</td>"); lasem_test_html ("</tr>\n"); lasem_test_html ("</table>\n"); } if (!is_xml && !g_file_test (reference_png_filename, G_FILE_TEST_IS_REGULAR) && !dry_run) { FILE *file; int result __attribute__((unused)); char *cmd; file = fopen ("lsmmathmltest.tmp", "w"); fprintf (file, "\\documentclass[10pt]{article}\n"); fprintf (file, "\\usepackage{amsmath}\n"); fprintf (file, "\\usepackage{amsfonts}\n"); fprintf (file, "\\usepackage{amssymb}\n"); fprintf (file, "\\usepackage{pst-plot}\n"); fprintf (file, "\\usepackage{color}\n"); fprintf (file, "\\pagestyle{empty}\n"); fprintf (file, "\\begin{document}\n"); fprintf (file, "%s\n", buffer); fprintf (file, "\\end{document}\n"); fclose (file); result = system ("latex --interaction=nonstopmode lsmmathmltest.tmp"); result = system ("dvips -E lsmmathmltest.dvi -o lsmmathmltest.ps"); cmd = g_strdup_printf ("convert -density 120 lsmmathmltest.ps %s", reference_png_filename); result = system (cmd); g_free (cmd); result = system ("rm lsmmathmltest.tmp"); result = system ("rm lsmmathmltest.dvi"); result = system ("rm lsmmathmltest.log"); result = system ("rm lsmmathmltest.aux"); result = system ("rm lsmmathmltest.ps"); } if (xml != buffer && !dry_run) { char *xml_filename; xml_filename = g_strdup_printf ("%s.xml", test_name); g_file_set_contents (xml_filename, xml, -1, NULL); g_free (xml_filename); g_free (buffer); itex2MML_free_string (xml); } else g_free (xml); } g_free (png_filename); g_free (reference_png_filename); g_free (test_name); } static void lasem_test_process_dir (const char *name, gboolean compare, gboolean dry_run, Statistic *statistic) { GDir *directory; GError *error = NULL; const char *entry; char *filename; unsigned int n_files = 0; directory = g_dir_open (name, 0, &error); assert (error == NULL); g_printf ("In directory %s\n", name); lasem_test_html ("<h1>%s</h1>", name); do { entry = g_dir_read_name (directory); if (entry != NULL && strstr (entry, "ignore-") != entry && strcmp (entry, "images") != 0) { gboolean save_png = strstr (entry, "dont-render-") != entry; filename = g_build_filename (name, entry, NULL); if (g_file_test (filename, G_FILE_TEST_IS_DIR)) lasem_test_process_dir (filename, compare, dry_run, statistic); else if (g_file_test (filename, G_FILE_TEST_IS_REGULAR) && g_regex_match (regex_mml, filename, 0, NULL)) { lasem_test_render (filename, compare, dry_run, save_png, statistic); n_files++; } g_free (filename); } } while (entry != NULL); g_dir_close (directory); } static gboolean check_for_compare (void) { char *result, *output; g_spawn_command_line_sync ("compare --version", &output, &result, NULL, NULL); g_free (output); if (result != NULL) { g_free (result); return TRUE; } g_printf ("Compare utility not found.\nPlease install ImageMagick.\n"); return FALSE; } int main (int argc, char **argv) { GOptionContext *context; GError *error = NULL; unsigned int i; unsigned int n_input_files = 0; Statistic statistic = {0, 0, 0, 0, 0}; if (!check_for_compare()) return EXIT_FAILURE; #if GLIB_CHECK_VERSION(2,50,0) if (g_log_writer_supports_color (STDOUT_FILENO)) { fail_face = "\033[41m\033[37m\033[1m"; success_face = "\033[42m\033[37m\033[1m"; normal_face = "\033[m"; } #endif lasem_test_html_file = fopen (XML_FILENAME, "w"); lasem_test_html ("<?xml version=\"1.0\"?>"); lasem_test_html ("<!DOCTYPE html PUBLIC " "\"-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN\" " "\"http://www.w3.org/Math/DTD/mathml2/xhtml-math11-f.dtd\">"); lasem_test_html ("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"); lasem_test_html ("<body>\n"); context = g_option_context_new (NULL); g_option_context_add_main_entries (context, entries, NULL); if (!g_option_context_parse (context, &argc, &argv, &error)) { g_option_context_free (context); g_print ("Option parsing failed: %s\n", error->message); return 1; } g_option_context_free (context); lsm_debug_enable (option_debug_domains); if (option_fatal_warning) g_log_set_fatal_mask ("Lasem", G_LOG_FATAL_MASK | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING); regex_mml = g_regex_new ("\\.(mml|tex|svg)$", 0, 0, &error); assert (error == NULL); n_input_files = option_input_filenames != NULL ? g_strv_length (option_input_filenames) : 0; if (n_input_files == 1 && g_file_test (option_input_filenames[0], G_FILE_TEST_IS_DIR)) lasem_test_process_dir (option_input_filenames[0], TRUE, option_dry_run, &statistic); else { if (n_input_files > 0) for (i = 0; i < n_input_files; i++) lasem_test_render (option_input_filenames[i], TRUE, option_dry_run, TRUE, &statistic); else lasem_test_process_dir (".", TRUE, option_dry_run, &statistic); } lasem_test_html ("</body>\n"); lasem_test_html ("</html>\n"); if (lasem_test_html_file != NULL) fclose (lasem_test_html_file); g_regex_unref (regex_mml); g_printf ("%d files processed in %g seconds.\n", statistic.rendered_count, statistic.elapsed_time); if (statistic.comparison_count > 0) g_printf ("%s%d/%d%s comparison failures.\n", statistic.failed_count > 0 ? fail_face : success_face, statistic.failed_count, statistic.comparison_count, normal_face); return 0; } /* vim: set sw=8 sts=8: -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 8 -*- */