Update pixel/corpus tests to handle expected results for Skia.

With pdf_use_skia enabled, the rendering results can be correct but
slightly different compared to AGG's results. This CL allows
run_pixel_tests.py and run_corpus_tests.py to also compared to
acceptable rendering results generated by Skia, named in the format of
"NAME_expected_skia(_(win|mac|linux))?.pdf.#.png". So that if the
rendering result of a PDF is not completely the same as its AGG's
expectation, as long as it finds a match with a Skia expected rendering
result, it won't cause a failure in pixel/corpus test.

This CL also provides a PNG file of Skia-based expected result of pixel
test bug_842.in to test the script change. This pixel test contains a
gradient transition that Skia uses a different algorithm to render
compared to AGG.

In case that SkiaPaths, Skia and AGG all generate correct results and
these results are different from each other, we will need to add
expected results for SkiaPaths in a new format of
"NAME_expected_skiapaths(_(win|mac|linux))?.pdf.#.png", besides the
expected results of Skia and AGG. Currently we don't have an existing
test case that can be rendered differently and correctly among using
all three rendering methods. Therefore there is no PNG file uploaded for
testing this SkiaPaths specific change.

Bug: pdfium:1501
Change-Id: I237c70bdec2640995f9cb8602d0b32addc0598e0
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/67871
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Hui Yingst <nigi@chromium.org>
diff --git a/testing/resources/pixel/bug_842_expected_skia.pdf.0.png b/testing/resources/pixel/bug_842_expected_skia.pdf.0.png
new file mode 100644
index 0000000..b454aa5
--- /dev/null
+++ b/testing/resources/pixel/bug_842_expected_skia.pdf.0.png
Binary files differ
diff --git a/testing/tools/pngdiffer.py b/testing/tools/pngdiffer.py
index 85179f8..63315c9 100755
--- a/testing/tools/pngdiffer.py
+++ b/testing/tools/pngdiffer.py
@@ -13,6 +13,31 @@
 import common
 
 
+class PathMode:
+  """PathMode indicates the available expected results' paths.
+
+  Attributes:
+    DEFAULT:   Used for default expected paths in the format of
+               'NAME_expected(_OSNAME)?.pdf.#.png'. For a test, this path always
+               exists.
+    SKIA:      Used when Skia or SkiaPaths is enabled, for paths in the format
+               of 'NAME_expected_skia(_OSNAME)?.pdf.#.png'.
+               Such paths only exist when the expected results of Skia or
+               SkiaPaths are different from those of AGG.
+    SKIAPATHS: Used when SkiaPaths is enabled, for path in the format of
+               'NAME_expected_skiapaths(_OSNAME)?.pdf.#.png'.
+               Such paths only exist when the expected results from using AGG,
+               Skia and SkiaPaths are all different from each other.
+
+  Always check PathMode in an incrementing order as the modes are listed in
+  order of its matching paths' precedence.
+  """
+
+  DEFAULT = 0
+  SKIA = 1
+  SKIAPATHS = 2
+
+
 class NotFoundError(Exception):
   """Raised when file doesn't exist"""
   pass
@@ -20,10 +45,16 @@
 
 class PNGDiffer():
 
-  def __init__(self, finder, reverse_byte_order):
+  def __init__(self, finder, features, reverse_byte_order):
     self.pdfium_diff_path = finder.ExecutablePath('pdfium_diff')
     self.os_name = finder.os_name
     self.reverse_byte_order = reverse_byte_order
+    if 'SKIAPATHS' in features:
+      self.max_path_mode = PathMode.SKIAPATHS
+    elif 'SKIA' in features:
+      self.max_path_mode = PathMode.SKIA
+    else:
+      self.max_path_mode = PathMode.DEFAULT
 
   def CheckMissingTools(self, regenerate_expected):
     if (regenerate_expected and self.os_name == 'linux' and
@@ -33,19 +64,16 @@
 
   def GetActualFiles(self, input_filename, source_dir, working_dir):
     actual_paths = []
-    path_templates = PathTemplates(input_filename, source_dir, working_dir)
+    path_templates = PathTemplates(input_filename, source_dir, working_dir,
+                                   self.os_name, self.max_path_mode)
 
     for page in itertools.count():
       actual_path = path_templates.GetActualPath(page)
-      expected_path = path_templates.GetExpectedPath(page)
-      platform_expected_path = path_templates.GetPlatformExpectedPath(
-          self.os_name, page)
-      if os.path.exists(platform_expected_path):
-        expected_path = platform_expected_path
-      elif not os.path.exists(expected_path):
+      expected_paths = path_templates.GetExpectedPaths(page)
+      if any(itertools.imap(os.path.exists, expected_paths)):
+        actual_paths.append(actual_path)
+      else:
         break
-      actual_paths.append(actual_path)
-
     return actual_paths
 
   def _RunImageDiffCommand(self, expected_path, actual_path):
@@ -59,17 +87,12 @@
     return common.RunCommand(cmd)
 
   def HasDifferences(self, input_filename, source_dir, working_dir):
-    path_templates = PathTemplates(input_filename, source_dir, working_dir)
-
+    path_templates = PathTemplates(input_filename, source_dir, working_dir,
+                                   self.os_name, self.max_path_mode)
     for page in itertools.count():
       actual_path = path_templates.GetActualPath(page)
-      expected_path = path_templates.GetExpectedPath(page)
-      # PDFium tests should be platform independent. Platform based results are
-      # used to capture platform dependent implementations.
-      platform_expected_path = path_templates.GetPlatformExpectedPath(
-          self.os_name, page)
-      if (not os.path.exists(expected_path) and
-          not os.path.exists(platform_expected_path)):
+      expected_paths = path_templates.GetExpectedPaths(page)
+      if not any(itertools.imap(os.path.exists, expected_paths)):
         if page == 0:
           print "WARNING: no expected results files for " + input_filename
         if os.path.exists(actual_path):
@@ -80,21 +103,28 @@
       print "Checking " + actual_path
       sys.stdout.flush()
 
-      error = self._RunImageDiffCommand(expected_path, actual_path)
+      error = None
+      for path in expected_paths:
+        new_error = self._RunImageDiffCommand(path, actual_path)
+        # Update error code. No need to overwrite the previous error code if
+        # |path| doesn't exist.
+        if not isinstance(new_error, NotFoundError):
+          error = new_error
+        # Found a match and proceed to next page
+        if not error:
+          break
+
       if error:
-        # When failed, we check against platform based results.
-        platform_error = self._RunImageDiffCommand(platform_expected_path,
-                                                   actual_path)
-        if platform_error:
-          if not isinstance(platform_error, NotFoundError):
-            error = platform_error
-          print "FAILURE: " + input_filename + "; " + str(error)
-          return True
+        print "FAILURE: " + input_filename + "; " + str(error)
+        return True
 
     return False
 
+  # TODO(crbug.com/pdfium/1508): Add support to automatically generate
+  # Skia/SkiaPaths specific expected results.
   def Regenerate(self, input_filename, source_dir, working_dir, platform_only):
-    path_templates = PathTemplates(input_filename, source_dir, working_dir)
+    path_templates = PathTemplates(input_filename, source_dir, working_dir,
+                                   self.os_name, self.max_path_mode)
 
     for page in itertools.count():
       # Loop through the generated page images. Stop when there is a page
@@ -103,8 +133,8 @@
       if not os.path.isfile(actual_path):
         break
 
-      platform_expected_path = path_templates.GetPlatformExpectedPath(
-          self.os_name, page)
+      platform_expected_path = path_templates.GetExpectedPathByPathMode(
+          page, PathMode.DEFAULT, self.os_name)
 
       # If there is a platform expected png, we will overwrite it. Otherwise,
       # overwrite the generic png in "all" mode, or do nothing in "platform"
@@ -112,7 +142,8 @@
       if os.path.exists(platform_expected_path):
         expected_path = platform_expected_path
       elif not platform_only:
-        expected_path = path_templates.GetExpectedPath(page)
+        expected_path = path_templates.GetExpectedPathByPathMode(
+            page, PathMode.DEFAULT)
       else:
         continue
 
@@ -121,26 +152,51 @@
 
 
 ACTUAL_TEMPLATE = '.pdf.%d.png'
-EXPECTED_TEMPLATE = '_expected' + ACTUAL_TEMPLATE
-PLATFORM_EXPECTED_TEMPLATE = '_expected_%s' + ACTUAL_TEMPLATE
-
 
 class PathTemplates(object):
 
-  def __init__(self, input_filename, source_dir, working_dir):
+  def __init__(self, input_filename, source_dir, working_dir, os_name,
+               max_path_mode):
+    assert PathMode.DEFAULT <= max_path_mode <= PathMode.SKIAPATHS, (
+        'Unexpected Maximum PathMode: %d.' % max_path_mode)
+
     input_root, _ = os.path.splitext(input_filename)
     self.actual_path_template = os.path.join(working_dir,
                                              input_root + ACTUAL_TEMPLATE)
-    self.expected_path = os.path.join(source_dir,
-                                      input_root + EXPECTED_TEMPLATE)
-    self.platform_expected_path = os.path.join(
-        source_dir, input_root + PLATFORM_EXPECTED_TEMPLATE)
+    self.source_dir = source_dir
+    self.input_root = input_root
+    self.max_path_mode = max_path_mode
+    self.os_name = os_name
+
+    # Pre-create the available templates depending on |max_path_mode|.
+    self.expected_templates = []
+    for mode in range(PathMode.DEFAULT, max_path_mode + 1):
+      self.expected_templates.extend([
+          self._GetExpectedTemplateByPathMode(mode),
+          self._GetExpectedTemplateByPathMode(mode, os_name),
+      ])
 
   def GetActualPath(self, page):
     return self.actual_path_template % page
 
-  def GetExpectedPath(self, page):
-    return self.expected_path % page
+  def _GetExpectedTemplateByPathMode(self, mode, os_name=None):
+    expected_str = '_expected'
+    if mode == PathMode.DEFAULT:
+      pass
+    elif mode == PathMode.SKIA:
+      expected_str += '_skia'
+    elif mode == PathMode.SKIAPATHS:
+      expected_str += '_skiapaths'
+    else:
+      assert False, 'Unexpected PathMode: %d.' % mode
 
-  def GetPlatformExpectedPath(self, platform, page):
-    return self.platform_expected_path % (platform, page)
+    if os_name:
+      expected_str += '_' + self.os_name
+    return os.path.join(self.source_dir,
+                        self.input_root + expected_str + ACTUAL_TEMPLATE)
+
+  def GetExpectedPathByPathMode(self, page, mode, os_name=None):
+    return self._GetExpectedTemplateByPathMode(mode, os_name) % page
+
+  def GetExpectedPaths(self, page):
+    return [template % page for template in self.expected_templates]
diff --git a/testing/tools/test_runner.py b/testing/tools/test_runner.py
index 363faf3..f4b45ac 100644
--- a/testing/tools/test_runner.py
+++ b/testing/tools/test_runner.py
@@ -119,6 +119,8 @@
       DeleteFiles(actual_images)
     return True, results
 
+  # TODO(crbug.com/pdfium/1508): Add support for an option to automatically
+  # generate Skia/SkiaPaths specific expected results.
   def RegenerateIfNeeded_(self, input_filename, source_dir):
     if (not self.options.regenerate_expected or
         self.test_suppressor.IsResultSuppressed(input_filename) or
@@ -360,7 +362,7 @@
     self.test_suppressor = suppressor.Suppressor(
         finder, self.features, self.options.disable_javascript,
         self.options.disable_xfa)
-    self.image_differ = pngdiffer.PNGDiffer(finder,
+    self.image_differ = pngdiffer.PNGDiffer(finder, self.features,
                                             self.options.reverse_byte_order)
     error_message = self.image_differ.CheckMissingTools(
         self.options.regenerate_expected)