diff options
Diffstat (limited to 'testing/tools/safetynet_image.py')
-rw-r--r-- | testing/tools/safetynet_image.py | 300 |
1 files changed, 300 insertions, 0 deletions
diff --git a/testing/tools/safetynet_image.py b/testing/tools/safetynet_image.py new file mode 100644 index 0000000000..28df3c582a --- /dev/null +++ b/testing/tools/safetynet_image.py @@ -0,0 +1,300 @@ +# Copyright 2017 The PDFium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Compares pairs of page images and generates an HTML to look at differences. +""" + +import functools +import glob +import multiprocessing +import os +import re +import subprocess +import sys +import webbrowser + +from common import DirectoryFinder + + +def GenerateOneDiffParallel(image_comparison, image): + return image_comparison.GenerateOneDiff(image) + + +class ImageComparison(object): + """Compares pairs of page images and generates an HTML to look at differences. + + The images are all assumed to have the same name and be in two directories: + [output_path]/[two_labels[0]] and [output_path]/[two_labels[1]]. For example, + if output_path is "/tmp/images" and two_labels is ("before", "after"), + images in /tmp/images/before will be compared to /tmp/images/after. The HTML + produced will be in /tmp/images/compare.html and have relative links to these + images, so /tmp/images is self-contained and can be moved around or shared. + """ + + def __init__(self, build_dir, output_path, two_labels, + num_workers, threshold_fraction): + """Constructor. + + Args: + build_dir: Path to the build directory. + output_path: Path with the pngs and where the html will be created. + two_labels: Tuple of two strings that name the subdirectories in + output_path containing the images. + num_workers: Number of worker threads to start. + threshold_fraction: Minimum percentage (0.0 to 1.0) of pixels below which + an image is considered to have only small changes. They will not be + displayed on the HTML, only listed. + """ + self.build_dir = build_dir + self.output_path = output_path + self.two_labels = two_labels + self.num_workers = num_workers + self.threshold = threshold_fraction * 100 + + def Run(self): + """Runs the comparison and generates an HTML with the results. + + Returns: + Exit status. + """ + + if len(self.two_labels) != 2: + print >> sys.stderr, 'two_labels must be a tuple of length 2' + return 1 + + finder = DirectoryFinder(self.build_dir) + self.img_diff_bin = finder.ExecutablePath('pdfium_diff') + + html_path = os.path.join(self.output_path, 'compare.html') + + self.diff_path = os.path.join(self.output_path, 'diff') + if not os.path.exists(self.diff_path): + os.makedirs(self.diff_path) + + self.image_locations = ImageLocations( + self.output_path, self.diff_path, self.two_labels) + + difference = self._GenerateDiffs() + + small_changes = [] + + with open(html_path, 'w') as f: + f.write('<html><body>') + f.write('<table>') + for image in self.image_locations.Images(): + diff = difference[image] + if diff is None: + print >> sys.stderr, 'Failed to compare image %s' % image + elif diff > self.threshold: + self._WriteImageRows(f, image, diff) + else: + small_changes.append((image, diff)) + self._WriteSmallChanges(f, small_changes) + f.write('</table>') + f.write('</body></html>') + + webbrowser.open(html_path) + return 0 + + def _GenerateDiffs(self): + """Runs a diff over all pairs of page images, producing diff images. + + As a side effect, the diff images will be saved to [output_path]/diff + with the same image name. + + Returns: + A dict mapping image names to percentage of pixels changes. + """ + difference = {} + pool = multiprocessing.Pool(self.num_workers) + worker_func = functools.partial(GenerateOneDiffParallel, self) + + try: + # The timeout is a workaround for http://bugs.python.org/issue8296 + # which prevents KeyboardInterrupt from working. + one_year_in_seconds = 3600 * 24 * 365 + worker_results = (pool.map_async(worker_func, + self.image_locations.Images()) + .get(one_year_in_seconds)) + for worker_result in worker_results: + image, result = worker_result + difference[image] = result + except KeyboardInterrupt: + pool.terminate() + sys.exit(1) + else: + pool.close() + + pool.join() + + return difference + + def GenerateOneDiff(self, image): + """Runs a diff over one pair of images, producing a diff image. + + As a side effect, the diff image will be saved to [output_path]/diff + with the same image name. + + Args: + image: Page image to compare. + + Returns: + A tuple (image, diff), where image is the parameter and diff is the + percentage of pixels changed. + """ + try: + subprocess.check_output( + [self.img_diff_bin, + self.image_locations.Left(image), + self.image_locations.Right(image)]) + except subprocess.CalledProcessError as e: + percentage_change = float(re.findall(r'\d+\.\d+', e.output)[0]) + else: + return image, 0 + + try: + subprocess.check_output( + [self.img_diff_bin, + '--diff', + self.image_locations.Left(image), + self.image_locations.Right(image), + self.image_locations.Diff(image)]) + except subprocess.CalledProcessError as e: + return image, percentage_change + else: + print >> sys.stderr, 'Warning: Should have failed the previous diff.' + return image, 0 + + def _GetRelativePath(self, absolute_path): + return os.path.relpath(absolute_path, start=self.output_path) + + def _WriteImageRows(self, f, image, diff): + """Write table rows for a page image comparing its two versions. + + Args: + f: Open HTML file to write to. + image: Image file name. + diff: Percentage of different pixels. + """ + f.write('<tr><td colspan="2">') + f.write('%s (%.4f%% changed)' % (image, diff)) + f.write('</td></tr>') + + f.write('<tr>') + self._WritePageCompareTd( + f, + self._GetRelativePath(self.image_locations.Left(image)), + self._GetRelativePath(self.image_locations.Right(image))) + self._WritePageTd( + f, + self._GetRelativePath(self.image_locations.Diff(image))) + f.write('</tr>') + + def _WritePageTd(self, f, image_path): + """Write table column with a single image. + + Args: + f: Open HTML file to write to. + image_path: Path to image file. + """ + f.write('<td>') + f.write('<img src="%s">' % image_path) + f.write('</td>') + + def _WritePageCompareTd(self, f, normal_image_path, hover_image_path): + """Write table column for an image comparing its two versions. + + Args: + f: Open HTML file to write to. + normal_image_path: Path to image to be used in the "normal" state. + hover_image_path: Path to image to be used in the "hover" state. + """ + f.write('<td>') + f.write('<img src="%s" ' + 'onmouseover="this.src=\'%s\';" ' + 'onmouseout="this.src=\'%s\';">' % (normal_image_path, + hover_image_path, + normal_image_path)) + f.write('</td>') + + def _WriteSmallChanges(self, f, small_changes): + """Write table rows for all images considered to have only small changes. + + Args: + f: Open HTML file to write to. + small_changes: List of (image, change) tuples, where image is the page + image and change is the percentage of pixels changed. + """ + for image, change in small_changes: + f.write('<tr><td colspan="2">') + if not change: + f.write('No change for: %s' % image) + else: + f.write('Small change of %.4f%% for: %s' % (change, image)) + f.write('</td></tr>') + + +class ImageLocations(object): + """Contains the locations of input and output image files. + """ + + def __init__(self, output_path, diff_path, two_labels): + """Constructor. + + Args: + output_path: Path to directory with the pngs. + diff_path: Path to directory where the diffs will be generated. + two_labels: Tuple of two strings that name the subdirectories in + output_path containing the images. + """ + self.output_path = output_path + self.diff_path = diff_path + self.two_labels = two_labels + + self.left = self._FindImages(self.two_labels[0]) + self.right = self._FindImages(self.two_labels[1]) + + self.images = list(self.left.viewkeys() & self.right.viewkeys()) + + # Sort by pdf filename, then page number + def KeyFn(s): + pieces = s.rsplit('.', 2) + return (pieces[0], int(pieces[1])) + + self.images.sort(key=KeyFn) + self.diff = {image: os.path.join(self.diff_path, image) + for image in self.images} + + def _FindImages(self, label): + """Traverses a dir and builds a dict of all page images to compare in it. + + Args: + label: name of subdirectory of output_path to traverse. + + Returns: + Dict mapping page image names to the path of the image file. + """ + image_path_matcher = os.path.join(self.output_path, label, '*.*.png') + image_paths = glob.glob(image_path_matcher) + + image_dict = {os.path.split(image_path)[1]: image_path + for image_path in image_paths} + + return image_dict + + def Images(self): + """Returns a list of all page images present in both directories.""" + return self.images + + def Left(self, test_case): + """Returns the path for a page image in the first subdirectory.""" + return self.left[test_case] + + def Right(self, test_case): + """Returns the path for a page image in the second subdirectory.""" + return self.right[test_case] + + def Diff(self, test_case): + """Returns the path for a page diff image.""" + return self.diff[test_case] |