1 /* Lasem - SVG and Mathml library
2 *
3 * lsm-test - Regression test utility for Lasem
4 *
5 * Copyright © 2004 Richard D. Worth
6 * Copyright © 2006 Red Hat, Inc.
7 * Copyright © 2007-2012 Emmanuel Pacaud
8 *
9 * Permission to use, copy, modify, distribute, and sell this software
10 * and its documentation for any purpose is hereby granted without
11 * fee, provided that the above copyright notice appear in all copies
12 * and that both that copyright notice and this permission notice
13 * appear in supporting documentation, and that the name of the authors
14 * not be used in advertising or publicity pertaining to distribution
15 * of the software without specific, written prior permission.
16 * The authors make no representations about the suitability of this
17 * software for any purpose. It is provided "as is" without express
18 * or implied warranty.
19 *
20 * THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
21 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
22 * NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
23 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
24 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
25 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
26 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
27 *
28 * Authors: Emmanuel Pacaud <emmanuel@gnome.org>
29 * Richard D. Worth <richard@theworths.org>
30 * Carl Worth <cworth@cworth.org>
31 */
32
33 #include "config.h"
34
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <assert.h>
39 #ifdef HAVE_UNISTD_H
40 #include <unistd.h>
41 #endif
42
43 #include <lsmmathml.h>
44 #include <glib.h>
45 #include <glib/gprintf.h>
46 #include <gio/gio.h>
47
48 #include <libxml/parser.h>
49
50 #include <../itex2mml/itex2MML.h>
51
52 #define XML_FILENAME "lasemtest.xml"
53
54 static char *option_debug_domains = NULL;
55 static char **option_input_filenames = NULL;
56 double option_ppi = 72.0;
57 static gboolean option_fatal_warning = FALSE;
58 static gboolean option_debug_filter = FALSE;
59 static gboolean option_debug_pattern = FALSE;
60 static gboolean option_debug_mask = FALSE;
61 static gboolean option_debug_group = FALSE;
62 static gboolean option_debug_text = FALSE;
63 static gboolean option_dry_run = FALSE;
64 static double option_compare_fuzz = 10.0;
65
66 static const GOptionEntry entries[] =
67 {
68 { G_OPTION_REMAINING, ' ', 0, G_OPTION_ARG_FILENAME_ARRAY,
69 &option_input_filenames, NULL, NULL},
70 { "ppi", 'p', 0, G_OPTION_ARG_DOUBLE,
71 &option_ppi, "Pixel per inch", NULL },
72 { "debug", 'd', 0, G_OPTION_ARG_STRING,
73 &option_debug_domains, "Debug domains", NULL },
74 { "fatal-warning", 'f', 0, G_OPTION_ARG_NONE,
75 &option_fatal_warning, "Make warning fatal", NULL },
76 { "debug-filter", ' ' , 0, G_OPTION_ARG_NONE,
77 &option_debug_filter, "Debug filter surfaces", NULL },
78 { "debug-pattern", ' ' , 0, G_OPTION_ARG_NONE,
79 &option_debug_pattern, "Debug pattern surfaces", NULL },
80 { "debug-mask", ' ' , 0, G_OPTION_ARG_NONE,
81 &option_debug_mask, "Debug mask surfaces", NULL },
82 { "debug-group", ' ' , 0, G_OPTION_ARG_NONE,
83 &option_debug_group, "Debug group surfaces", NULL },
84 { "debug-text", ' ' , 0, G_OPTION_ARG_NONE,
85 &option_debug_text, "Debug text layout", NULL },
86 { "dry-run", 'n' , 0, G_OPTION_ARG_NONE,
87 &option_dry_run, "Don't write files", NULL },
88 { "compare-fuzz", 'z', 0, G_OPTION_ARG_DOUBLE,
89 &option_compare_fuzz, "Compare fuzz", NULL},
90 { NULL }
91 };
92
93 static const char *fail_face = "";
94 static const char *success_face = "";
95 static const char *normal_face = "";
96 FILE *lasem_test_html_file = NULL;
97
98 typedef struct {
99 double elapsed_time;
100 unsigned int rendered_count;
101 unsigned int comparison_count;
102 unsigned int failed_count;
103 unsigned int success_count;
104 } Statistic;
105
106 static void __attribute__((format(printf,1,2)))
lasem_test_html(const char * fmt,...)107 lasem_test_html (const char *fmt, ...)
108 {
109 va_list va;
110 FILE *file = lasem_test_html_file ? lasem_test_html_file : stdout;
111
112 va_start (va, fmt);
113 vfprintf (file, fmt, va);
114 va_end (va);
115 }
116
117 static GRegex *regex_mml = NULL;
118
119 typedef struct _buffer_diff_result {
120 unsigned int pixels_changed;
121 unsigned int max_diff;
122 } buffer_diff_result_t;
123
124 typedef guint32 pixman_bits_t;
125
126 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)127 buffer_diff_core (unsigned char *_buf_a,
128 unsigned char *_buf_b,
129 unsigned char *_buf_diff,
130 int width,
131 int height,
132 int stride,
133 pixman_bits_t mask,
134 buffer_diff_result_t *result_ret)
135 {
136 int x, y;
137 pixman_bits_t *row_a, *row_b, *row;
138 buffer_diff_result_t result = {0, 0};
139 pixman_bits_t *buf_a = (pixman_bits_t*)_buf_a;
140 pixman_bits_t *buf_b = (pixman_bits_t*)_buf_b;
141 pixman_bits_t *buf_diff = (pixman_bits_t*)_buf_diff;
142
143 stride /= sizeof(pixman_bits_t);
144 for (y = 0; y < height; y++)
145 {
146 row_a = buf_a + y * stride;
147 row_b = buf_b + y * stride;
148 row = buf_diff + y * stride;
149 for (x = 0; x < width; x++)
150 {
151 /* check if the pixels are the same */
152 if ((row_a[x] & mask) != (row_b[x] & mask)) {
153 int channel;
154 pixman_bits_t diff_pixel = 0;
155
156 /* calculate a difference value for all 4 channels */
157 for (channel = 0; channel < 4; channel++) {
158 int value_a = (row_a[x] >> (channel*8)) & 0xff;
159 int value_b = (row_b[x] >> (channel*8)) & 0xff;
160 unsigned int diff;
161 diff = abs (value_a - value_b);
162 if (diff > result.max_diff)
163 result.max_diff = diff;
164 diff *= 4; /* emphasize */
165 if (diff)
166 diff += 128; /* make sure it's visible */
167 if (diff > 255)
168 diff = 255;
169 diff_pixel |= diff << (channel*8);
170 }
171
172 result.pixels_changed++;
173 row[x] = diff_pixel;
174 } else {
175 row[x] = 0;
176 }
177 row[x] |= 0xff000000; /* Set ALPHA to 100% (opaque) */
178 }
179 }
180
181 *result_ret = result;
182 }
183
184 static gboolean
compare_surfaces(const char * test_name,cairo_surface_t * surface_a,cairo_surface_t * surface_b)185 compare_surfaces (const char *test_name, cairo_surface_t *surface_a, cairo_surface_t *surface_b)
186 {
187 int width_a, width_b, height_a, height_b, stride_a, stride_b;
188
189 if (surface_b == NULL)
190 return FALSE;
191 if (surface_a == NULL)
192 return FALSE;
193
194 width_a = cairo_image_surface_get_width (surface_a);
195 height_a = cairo_image_surface_get_height (surface_a);
196 stride_a = cairo_image_surface_get_stride (surface_a);
197 width_b = cairo_image_surface_get_width (surface_b);
198 height_b = cairo_image_surface_get_height (surface_b);
199 stride_b = cairo_image_surface_get_stride (surface_b);
200
201 if (width_a == width_b && height_a == height_b && stride_a == stride_b) {
202 buffer_diff_result_t result;
203 cairo_surface_t *surface_diff;
204 char *diff_png_filename;
205 char *command;
206 char *command_result = NULL;
207 int command_status;
208
209 diff_png_filename = g_strdup_printf ("%s-diff.png", test_name);
210
211 surface_diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
212 width_a, height_a);
213
214 buffer_diff_core (cairo_image_surface_get_data (surface_a),
215 cairo_image_surface_get_data (surface_b),
216 cairo_image_surface_get_data (surface_diff),
217 cairo_image_surface_get_width (surface_a),
218 cairo_image_surface_get_height (surface_a),
219 cairo_image_surface_get_stride (surface_a),
220 0xffffffff,
221 &result);
222
223 cairo_surface_write_to_png (surface_diff, diff_png_filename);
224
225 cairo_surface_destroy (surface_diff);
226 g_free (diff_png_filename);
227
228 if (result.pixels_changed == 0) {
229 g_printf (" %sOK%s \n", success_face, normal_face);
230 return TRUE;
231 }
232
233 command = g_strdup_printf ("compare -metric AE -fuzz %g%% %s-out.png %s-ref.png %s-compare-diff.png",
234 option_compare_fuzz, test_name, test_name, test_name);
235 g_spawn_command_line_sync (command, NULL, &command_result, &command_status, NULL);
236 g_free (command);
237
238 if (command_result != NULL && atoi (command_result) == 0) {
239 g_printf (" %sOK%s \n", success_face, normal_face);
240 return TRUE;
241 }
242 }
243
244 g_printf (" %sFAIL%s \n", fail_face, normal_face);
245 return FALSE;
246 }
247
248 static void
lasem_test_render(char const * filename,gboolean compare,gboolean dry_run,gboolean save_png,Statistic * statistic)249 lasem_test_render (char const *filename, gboolean compare, gboolean dry_run, gboolean save_png, Statistic *statistic)
250 {
251 LsmDomDocument *document;
252 LsmDomView *view;
253 GTimer *timer;
254 cairo_t *cairo;
255 cairo_surface_t *surface;
256 char *buffer = NULL;
257 gssize size;
258 char *png_filename;
259 char *reference_png_filename;
260 char *test_name;
261 char *mime;
262 unsigned int width, height;
263 gboolean is_xml, success;
264 gboolean is_svg;
265 gboolean is_mathml;
266 gboolean check;
267 GRegex *regex;
268 GError *error = NULL;
269 char *filtered_buffer;
270
271 g_return_if_fail (statistic != NULL);
272
273 test_name = g_regex_replace (regex_mml, filename, -1, 0, "", 0, NULL);
274
275 png_filename = g_strdup_printf ("%s-out.png", test_name);
276 reference_png_filename = g_strdup_printf ("%s-ref.png", test_name);
277 if (g_file_test (reference_png_filename, G_FILE_TEST_IS_REGULAR)) {
278 check = compare;
279 } else {
280 g_free (reference_png_filename);
281 reference_png_filename = g_strdup_printf ("%s.png", test_name);
282 check = FALSE;
283 }
284
285 mime = g_content_type_guess (filename, NULL, 0, NULL);
286
287 is_svg = strcmp (mime, "image/svg+xml") == 0;
288 is_mathml = (strcmp (mime, "text/mathml") == 0) || (strcmp (mime, "application/mathml+xml") == 0);
289 is_xml = is_svg || is_mathml;
290
291 g_printf ("\trender %s (%s)", filename, mime);
292 g_free (mime);
293
294 success = g_file_get_contents (filename, &buffer, &size, NULL);
295 if (success) {
296 LsmBox viewport;
297 char *xml;
298
299 if (is_xml)
300 xml = buffer;
301 else {
302 xml = itex2MML_parse (buffer, size);
303 size = -1;
304 }
305
306 timer = g_timer_new ();
307
308 document = lsm_dom_document_new_from_memory (xml, size, NULL);
309
310 lsm_dom_document_set_path (document, filename);
311
312 view = lsm_dom_document_create_view (document);
313
314 viewport.x = 0.0;
315 viewport.y = 0.0;
316 viewport.width = 480.0;
317 viewport.height = 360.0;
318
319 lsm_dom_view_set_resolution (view, option_ppi);
320 lsm_dom_view_set_viewport_pixels (view, &viewport);
321 lsm_dom_view_get_size_pixels (LSM_DOM_VIEW (view), &width, &height, NULL);
322
323 if (option_debug_mask)
324 lsm_dom_view_set_debug (view, "mask", TRUE);
325 if (option_debug_pattern)
326 lsm_dom_view_set_debug (view, "pattern", TRUE);
327 if (option_debug_filter)
328 lsm_dom_view_set_debug (view, "filter", TRUE);
329 if (option_debug_group)
330 lsm_dom_view_set_debug (view, "group", TRUE);
331 if (option_debug_text)
332 lsm_dom_view_set_debug (view, "text", TRUE);
333
334 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width + 2, height + 2);
335 cairo = cairo_create (surface);
336 cairo_surface_destroy (surface);
337
338 lsm_dom_view_render (LSM_DOM_VIEW (view), cairo, 1, 1);
339
340 statistic->elapsed_time += g_timer_elapsed (timer, NULL);
341 statistic->rendered_count++;
342 g_timer_destroy (timer);
343
344 if (!dry_run && save_png)
345 cairo_surface_write_to_png (surface, png_filename);
346
347 if (check) {
348 cairo_surface_t *reference_surface;
349
350 reference_surface = cairo_image_surface_create_from_png (reference_png_filename);
351 if (reference_surface != NULL) {
352 gboolean same;
353
354 same = compare_surfaces (test_name, surface, reference_surface);
355 cairo_surface_destroy (reference_surface);
356
357 if (same)
358 statistic->success_count++;
359 else
360 statistic->failed_count++;
361 statistic->comparison_count++;
362 }
363 } else
364 g_printf ("\n");
365
366 cairo_destroy (cairo);
367
368 g_object_unref (view);
369 g_object_unref (document);
370
371 if (save_png) {
372 lasem_test_html ("<table border=\"1\" cellpadding=\"8\">\n");
373 lasem_test_html ("<tr>");
374
375 lasem_test_html ("<td><a href=\"%s\"><img border=\"0\" src=\"%s\"/></a></td>",
376 filename, png_filename);
377 lasem_test_html ("<td><img src=\"%s\"/></td>", reference_png_filename);
378
379 lasem_test_html ("<td>");
380
381 if (is_mathml) {
382 regex = g_regex_new ("<math>", 0, 0, &error);
383 assert (error == NULL);
384
385 filtered_buffer = g_regex_replace (regex, xml,
386 -1, 0,
387 "<math xmlns=\"http://www.w3.org/1998/Math/MathML\">",
388 0, NULL);
389 g_regex_unref (regex);
390
391 lasem_test_html ("%s", filtered_buffer);
392
393 g_free (filtered_buffer);
394 }
395
396 if (is_svg) {
397 lasem_test_html ("<object type=\"image/svg+xml\" data=\"");
398 lasem_test_html ("%s", filename);
399 lasem_test_html ("\" width=\"%dpx\"/>", width + 2);
400 }
401
402 lasem_test_html ("</td>");
403 lasem_test_html ("</tr>\n");
404 lasem_test_html ("</table>\n");
405 }
406
407 if (!is_xml && !g_file_test (reference_png_filename, G_FILE_TEST_IS_REGULAR) && !dry_run) {
408 FILE *file;
409 int result __attribute__((unused));
410 char *cmd;
411
412 file = fopen ("lsmmathmltest.tmp", "w");
413 fprintf (file, "\\documentclass[10pt]{article}\n");
414 fprintf (file, "\\usepackage{amsmath}\n");
415 fprintf (file, "\\usepackage{amsfonts}\n");
416 fprintf (file, "\\usepackage{amssymb}\n");
417 fprintf (file, "\\usepackage{pst-plot}\n");
418 fprintf (file, "\\usepackage{color}\n");
419 fprintf (file, "\\pagestyle{empty}\n");
420 fprintf (file, "\\begin{document}\n");
421 fprintf (file, "%s\n", buffer);
422 fprintf (file, "\\end{document}\n");
423 fclose (file);
424
425 result = system ("latex --interaction=nonstopmode lsmmathmltest.tmp");
426 result = system ("dvips -E lsmmathmltest.dvi -o lsmmathmltest.ps");
427
428 cmd = g_strdup_printf ("convert -density 120 lsmmathmltest.ps %s", reference_png_filename);
429 result = system (cmd);
430 g_free (cmd);
431
432 result = system ("rm lsmmathmltest.tmp");
433 result = system ("rm lsmmathmltest.dvi");
434 result = system ("rm lsmmathmltest.log");
435 result = system ("rm lsmmathmltest.aux");
436 result = system ("rm lsmmathmltest.ps");
437 }
438
439 if (xml != buffer && !dry_run) {
440 char *xml_filename;
441
442 xml_filename = g_strdup_printf ("%s.xml", test_name);
443
444 g_file_set_contents (xml_filename, xml, -1, NULL);
445
446 g_free (xml_filename);
447 g_free (buffer);
448 itex2MML_free_string (xml);
449 } else
450 g_free (xml);
451 }
452
453 g_free (png_filename);
454 g_free (reference_png_filename);
455
456 g_free (test_name);
457 }
458
459 static void
lasem_test_process_dir(const char * name,gboolean compare,gboolean dry_run,Statistic * statistic)460 lasem_test_process_dir (const char *name, gboolean compare, gboolean dry_run, Statistic *statistic)
461 {
462 GDir *directory;
463 GError *error = NULL;
464 const char *entry;
465 char *filename;
466 unsigned int n_files = 0;
467
468 directory = g_dir_open (name, 0, &error);
469 assert (error == NULL);
470
471 g_printf ("In directory %s\n", name);
472
473 lasem_test_html ("<h1>%s</h1>", name);
474
475 do {
476 entry = g_dir_read_name (directory);
477 if (entry != NULL &&
478 strstr (entry, "ignore-") != entry &&
479 strcmp (entry, "images") != 0)
480 {
481 gboolean save_png = strstr (entry, "dont-render-") != entry;
482
483 filename = g_build_filename (name, entry, NULL);
484
485 if (g_file_test (filename, G_FILE_TEST_IS_DIR))
486 lasem_test_process_dir (filename, compare, dry_run, statistic);
487 else if (g_file_test (filename, G_FILE_TEST_IS_REGULAR) &&
488 g_regex_match (regex_mml, filename, 0, NULL)) {
489 lasem_test_render (filename, compare, dry_run, save_png, statistic);
490 n_files++;
491 }
492
493 g_free (filename);
494 }
495 } while (entry != NULL);
496
497 g_dir_close (directory);
498 }
499
500 static gboolean
check_for_compare(void)501 check_for_compare (void)
502 {
503 char *result, *output;
504
505 g_spawn_command_line_sync ("compare --version", &output, &result, NULL, NULL);
506
507 g_free (output);
508
509 if (result != NULL) {
510 g_free (result);
511 return TRUE;
512 }
513
514 g_printf ("Compare utility not found.\nPlease install ImageMagick.\n");
515 return FALSE;
516 }
517
518 int
main(int argc,char ** argv)519 main (int argc, char **argv)
520 {
521 GOptionContext *context;
522 GError *error = NULL;
523 unsigned int i;
524 unsigned int n_input_files = 0;
525 Statistic statistic = {0, 0, 0, 0, 0};
526
527 if (!check_for_compare())
528 return EXIT_FAILURE;
529
530 #ifdef HAVE_UNISTD_H
531 if (isatty (2)) {
532 fail_face = "\033[41m\033[37m\033[1m";
533 success_face = "\033[42m\033[37m\033[1m";
534 normal_face = "\033[m";
535 }
536 #endif
537
538 lasem_test_html_file = fopen (XML_FILENAME, "w");
539
540 lasem_test_html ("<?xml version=\"1.0\"?>");
541 lasem_test_html ("<!DOCTYPE html PUBLIC "
542 "\"-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN\" "
543 "\"http://www.w3.org/Math/DTD/mathml2/xhtml-math11-f.dtd\">");
544 lasem_test_html ("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n");
545 lasem_test_html ("<body>\n");
546
547 #if !GLIB_CHECK_VERSION(2,36,0)
548 g_type_init ();
549 #endif
550
551 context = g_option_context_new (NULL);
552 g_option_context_add_main_entries (context, entries, NULL);
553
554 if (!g_option_context_parse (context, &argc, &argv, &error))
555 {
556 g_option_context_free (context);
557 g_print ("Option parsing failed: %s\n", error->message);
558 return 1;
559 }
560
561 g_option_context_free (context);
562
563 lsm_debug_enable (option_debug_domains);
564
565 if (option_fatal_warning)
566 g_log_set_fatal_mask ("Lasem", G_LOG_FATAL_MASK | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING);
567
568 regex_mml = g_regex_new ("\\.(mml|tex|svg)$", 0, 0, &error);
569 assert (error == NULL);
570
571 n_input_files = option_input_filenames != NULL ? g_strv_length (option_input_filenames) : 0;
572 if (n_input_files == 1 && g_file_test (option_input_filenames[0], G_FILE_TEST_IS_DIR))
573 lasem_test_process_dir (option_input_filenames[0], TRUE, option_dry_run, &statistic);
574 else {
575 if (n_input_files > 0)
576 for (i = 0; i < n_input_files; i++)
577 lasem_test_render (option_input_filenames[i], TRUE, option_dry_run, TRUE, &statistic);
578 else
579 lasem_test_process_dir (".", TRUE, option_dry_run, &statistic);
580 }
581
582 lasem_test_html ("</body>\n");
583 lasem_test_html ("</html>\n");
584
585 if (lasem_test_html_file != NULL)
586 fclose (lasem_test_html_file);
587
588 g_regex_unref (regex_mml);
589
590 g_printf ("%d files processed in %g seconds.\n", statistic.rendered_count, statistic.elapsed_time);
591 if (statistic.comparison_count > 0)
592 g_printf ("%s%d/%d%s comparison failures.\n",
593 statistic.failed_count > 0 ? fail_face : success_face,
594 statistic.failed_count,
595 statistic.comparison_count,
596 normal_face);
597
598 return 0;
599 }
600
601 /* vim: set sw=8 sts=8: -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 8 -*- */
602