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