xref: /aosp_15_r20/external/pdfium/testing/tools/pngdiffer.py (revision 3ac0a46f773bac49fa9476ec2b1cf3f8da5ec3a4)
1*3ac0a46fSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*3ac0a46fSAndroid Build Coastguard Worker# Copyright 2015 The PDFium Authors
3*3ac0a46fSAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
4*3ac0a46fSAndroid Build Coastguard Worker# found in the LICENSE file.
5*3ac0a46fSAndroid Build Coastguard Worker
6*3ac0a46fSAndroid Build Coastguard Workerfrom dataclasses import dataclass
7*3ac0a46fSAndroid Build Coastguard Workerimport itertools
8*3ac0a46fSAndroid Build Coastguard Workerimport os
9*3ac0a46fSAndroid Build Coastguard Workerimport shutil
10*3ac0a46fSAndroid Build Coastguard Workerimport subprocess
11*3ac0a46fSAndroid Build Coastguard Workerimport sys
12*3ac0a46fSAndroid Build Coastguard Worker
13*3ac0a46fSAndroid Build Coastguard WorkerEXACT_MATCHING = 'exact'
14*3ac0a46fSAndroid Build Coastguard WorkerFUZZY_MATCHING = 'fuzzy'
15*3ac0a46fSAndroid Build Coastguard Worker
16*3ac0a46fSAndroid Build Coastguard Worker_PNG_OPTIMIZER = 'optipng'
17*3ac0a46fSAndroid Build Coastguard Worker
18*3ac0a46fSAndroid Build Coastguard Worker# Each suffix order acts like a path along a tree, with the leaves being the
19*3ac0a46fSAndroid Build Coastguard Worker# most specific, and the root being the least specific.
20*3ac0a46fSAndroid Build Coastguard Worker_COMMON_SUFFIX_ORDER = ('_{os}', '')
21*3ac0a46fSAndroid Build Coastguard Worker_AGG_SUFFIX_ORDER = ('_agg_{os}', '_agg') + _COMMON_SUFFIX_ORDER
22*3ac0a46fSAndroid Build Coastguard Worker_GDI_SUFFIX_ORDER = ('_gdi_{os}', '_gdi') + _COMMON_SUFFIX_ORDER
23*3ac0a46fSAndroid Build Coastguard Worker_SKIA_SUFFIX_ORDER = ('_skia_{os}', '_skia') + _COMMON_SUFFIX_ORDER
24*3ac0a46fSAndroid Build Coastguard Worker
25*3ac0a46fSAndroid Build Coastguard Worker
26*3ac0a46fSAndroid Build Coastguard Worker@dataclass
27*3ac0a46fSAndroid Build Coastguard Workerclass ImageDiff:
28*3ac0a46fSAndroid Build Coastguard Worker  """Details about an image diff.
29*3ac0a46fSAndroid Build Coastguard Worker
30*3ac0a46fSAndroid Build Coastguard Worker  Attributes:
31*3ac0a46fSAndroid Build Coastguard Worker    actual_path: Path to the actual image file.
32*3ac0a46fSAndroid Build Coastguard Worker    expected_path: Path to the expected image file, or `None` if no matches.
33*3ac0a46fSAndroid Build Coastguard Worker    diff_path: Path to the diff image file, or `None` if no diff.
34*3ac0a46fSAndroid Build Coastguard Worker    reason: Optional reason for the diff.
35*3ac0a46fSAndroid Build Coastguard Worker  """
36*3ac0a46fSAndroid Build Coastguard Worker  actual_path: str
37*3ac0a46fSAndroid Build Coastguard Worker  expected_path: str = None
38*3ac0a46fSAndroid Build Coastguard Worker  diff_path: str = None
39*3ac0a46fSAndroid Build Coastguard Worker  reason: str = None
40*3ac0a46fSAndroid Build Coastguard Worker
41*3ac0a46fSAndroid Build Coastguard Workerclass PNGDiffer():
42*3ac0a46fSAndroid Build Coastguard Worker
43*3ac0a46fSAndroid Build Coastguard Worker  def __init__(self, finder, reverse_byte_order, rendering_option):
44*3ac0a46fSAndroid Build Coastguard Worker    self.pdfium_diff_path = finder.ExecutablePath('pdfium_diff')
45*3ac0a46fSAndroid Build Coastguard Worker    self.os_name = finder.os_name
46*3ac0a46fSAndroid Build Coastguard Worker    self.reverse_byte_order = reverse_byte_order
47*3ac0a46fSAndroid Build Coastguard Worker
48*3ac0a46fSAndroid Build Coastguard Worker    if rendering_option == 'agg':
49*3ac0a46fSAndroid Build Coastguard Worker      self.suffix_order = _AGG_SUFFIX_ORDER
50*3ac0a46fSAndroid Build Coastguard Worker    elif rendering_option == 'gdi':
51*3ac0a46fSAndroid Build Coastguard Worker      self.suffix_order = _GDI_SUFFIX_ORDER
52*3ac0a46fSAndroid Build Coastguard Worker    elif rendering_option == 'skia':
53*3ac0a46fSAndroid Build Coastguard Worker      self.suffix_order = _SKIA_SUFFIX_ORDER
54*3ac0a46fSAndroid Build Coastguard Worker    else:
55*3ac0a46fSAndroid Build Coastguard Worker      raise ValueError(f'rendering_option={rendering_option}')
56*3ac0a46fSAndroid Build Coastguard Worker
57*3ac0a46fSAndroid Build Coastguard Worker  def CheckMissingTools(self, regenerate_expected):
58*3ac0a46fSAndroid Build Coastguard Worker    if regenerate_expected and not shutil.which(_PNG_OPTIMIZER):
59*3ac0a46fSAndroid Build Coastguard Worker      return f'Please install "{_PNG_OPTIMIZER}" to regenerate expected images.'
60*3ac0a46fSAndroid Build Coastguard Worker    return None
61*3ac0a46fSAndroid Build Coastguard Worker
62*3ac0a46fSAndroid Build Coastguard Worker  def GetActualFiles(self, input_filename, source_dir, working_dir):
63*3ac0a46fSAndroid Build Coastguard Worker    actual_paths = []
64*3ac0a46fSAndroid Build Coastguard Worker    path_templates = _PathTemplates(input_filename, source_dir, working_dir,
65*3ac0a46fSAndroid Build Coastguard Worker                                    self.os_name, self.suffix_order)
66*3ac0a46fSAndroid Build Coastguard Worker
67*3ac0a46fSAndroid Build Coastguard Worker    for page in itertools.count():
68*3ac0a46fSAndroid Build Coastguard Worker      actual_path = path_templates.GetActualPath(page)
69*3ac0a46fSAndroid Build Coastguard Worker      if path_templates.GetExpectedPath(page, default_to_base=False):
70*3ac0a46fSAndroid Build Coastguard Worker        actual_paths.append(actual_path)
71*3ac0a46fSAndroid Build Coastguard Worker      else:
72*3ac0a46fSAndroid Build Coastguard Worker        break
73*3ac0a46fSAndroid Build Coastguard Worker    return actual_paths
74*3ac0a46fSAndroid Build Coastguard Worker
75*3ac0a46fSAndroid Build Coastguard Worker  def _RunCommand(self, cmd):
76*3ac0a46fSAndroid Build Coastguard Worker    try:
77*3ac0a46fSAndroid Build Coastguard Worker      subprocess.run(cmd, capture_output=True, check=True)
78*3ac0a46fSAndroid Build Coastguard Worker      return None
79*3ac0a46fSAndroid Build Coastguard Worker    except subprocess.CalledProcessError as e:
80*3ac0a46fSAndroid Build Coastguard Worker      return e
81*3ac0a46fSAndroid Build Coastguard Worker
82*3ac0a46fSAndroid Build Coastguard Worker  def _RunImageCompareCommand(self, image_diff, image_matching_algorithm):
83*3ac0a46fSAndroid Build Coastguard Worker    cmd = [self.pdfium_diff_path]
84*3ac0a46fSAndroid Build Coastguard Worker    if self.reverse_byte_order:
85*3ac0a46fSAndroid Build Coastguard Worker      cmd.append('--reverse-byte-order')
86*3ac0a46fSAndroid Build Coastguard Worker    if image_matching_algorithm == FUZZY_MATCHING:
87*3ac0a46fSAndroid Build Coastguard Worker      cmd.append('--fuzzy')
88*3ac0a46fSAndroid Build Coastguard Worker    cmd.extend([image_diff.actual_path, image_diff.expected_path])
89*3ac0a46fSAndroid Build Coastguard Worker    return self._RunCommand(cmd)
90*3ac0a46fSAndroid Build Coastguard Worker
91*3ac0a46fSAndroid Build Coastguard Worker  def _RunImageDiffCommand(self, image_diff):
92*3ac0a46fSAndroid Build Coastguard Worker    # TODO(crbug.com/pdfium/1925): Diff mode ignores --reverse-byte-order.
93*3ac0a46fSAndroid Build Coastguard Worker    return self._RunCommand([
94*3ac0a46fSAndroid Build Coastguard Worker        self.pdfium_diff_path, '--subtract', image_diff.actual_path,
95*3ac0a46fSAndroid Build Coastguard Worker        image_diff.expected_path, image_diff.diff_path
96*3ac0a46fSAndroid Build Coastguard Worker    ])
97*3ac0a46fSAndroid Build Coastguard Worker
98*3ac0a46fSAndroid Build Coastguard Worker  def ComputeDifferences(self, input_filename, source_dir, working_dir,
99*3ac0a46fSAndroid Build Coastguard Worker                         image_matching_algorithm):
100*3ac0a46fSAndroid Build Coastguard Worker    """Computes differences between actual and expected image files.
101*3ac0a46fSAndroid Build Coastguard Worker
102*3ac0a46fSAndroid Build Coastguard Worker    Returns:
103*3ac0a46fSAndroid Build Coastguard Worker      A list of `ImageDiff` instances, one per differing page.
104*3ac0a46fSAndroid Build Coastguard Worker    """
105*3ac0a46fSAndroid Build Coastguard Worker    image_diffs = []
106*3ac0a46fSAndroid Build Coastguard Worker
107*3ac0a46fSAndroid Build Coastguard Worker    path_templates = _PathTemplates(input_filename, source_dir, working_dir,
108*3ac0a46fSAndroid Build Coastguard Worker                                    self.os_name, self.suffix_order)
109*3ac0a46fSAndroid Build Coastguard Worker    for page in itertools.count():
110*3ac0a46fSAndroid Build Coastguard Worker      page_diff = ImageDiff(actual_path=path_templates.GetActualPath(page))
111*3ac0a46fSAndroid Build Coastguard Worker      if not os.path.exists(page_diff.actual_path):
112*3ac0a46fSAndroid Build Coastguard Worker        # No more actual pages.
113*3ac0a46fSAndroid Build Coastguard Worker        break
114*3ac0a46fSAndroid Build Coastguard Worker
115*3ac0a46fSAndroid Build Coastguard Worker      expected_path = path_templates.GetExpectedPath(page)
116*3ac0a46fSAndroid Build Coastguard Worker      if os.path.exists(expected_path):
117*3ac0a46fSAndroid Build Coastguard Worker        page_diff.expected_path = expected_path
118*3ac0a46fSAndroid Build Coastguard Worker
119*3ac0a46fSAndroid Build Coastguard Worker        compare_error = self._RunImageCompareCommand(page_diff,
120*3ac0a46fSAndroid Build Coastguard Worker                                                     image_matching_algorithm)
121*3ac0a46fSAndroid Build Coastguard Worker        if compare_error:
122*3ac0a46fSAndroid Build Coastguard Worker          page_diff.reason = str(compare_error)
123*3ac0a46fSAndroid Build Coastguard Worker
124*3ac0a46fSAndroid Build Coastguard Worker          # TODO(crbug.com/pdfium/1925): Compare and diff simultaneously.
125*3ac0a46fSAndroid Build Coastguard Worker          page_diff.diff_path = path_templates.GetDiffPath(page)
126*3ac0a46fSAndroid Build Coastguard Worker          if not self._RunImageDiffCommand(page_diff):
127*3ac0a46fSAndroid Build Coastguard Worker            print(f'WARNING: No diff for {page_diff.actual_path}')
128*3ac0a46fSAndroid Build Coastguard Worker            page_diff.diff_path = None
129*3ac0a46fSAndroid Build Coastguard Worker        else:
130*3ac0a46fSAndroid Build Coastguard Worker          # Validate that no other paths match.
131*3ac0a46fSAndroid Build Coastguard Worker          for unexpected_path in path_templates.GetExpectedPaths(page)[1:]:
132*3ac0a46fSAndroid Build Coastguard Worker            page_diff.expected_path = unexpected_path
133*3ac0a46fSAndroid Build Coastguard Worker            if not self._RunImageCompareCommand(page_diff,
134*3ac0a46fSAndroid Build Coastguard Worker                                                image_matching_algorithm):
135*3ac0a46fSAndroid Build Coastguard Worker              page_diff.reason = f'Also matches {unexpected_path}'
136*3ac0a46fSAndroid Build Coastguard Worker              break
137*3ac0a46fSAndroid Build Coastguard Worker          page_diff.expected_path = expected_path
138*3ac0a46fSAndroid Build Coastguard Worker      else:
139*3ac0a46fSAndroid Build Coastguard Worker        if page == 0:
140*3ac0a46fSAndroid Build Coastguard Worker          print(f'WARNING: no expected results files for {input_filename}')
141*3ac0a46fSAndroid Build Coastguard Worker        page_diff.reason = f'{expected_path} does not exist'
142*3ac0a46fSAndroid Build Coastguard Worker
143*3ac0a46fSAndroid Build Coastguard Worker      if page_diff.reason:
144*3ac0a46fSAndroid Build Coastguard Worker        image_diffs.append(page_diff)
145*3ac0a46fSAndroid Build Coastguard Worker
146*3ac0a46fSAndroid Build Coastguard Worker    return image_diffs
147*3ac0a46fSAndroid Build Coastguard Worker
148*3ac0a46fSAndroid Build Coastguard Worker  def Regenerate(self, input_filename, source_dir, working_dir,
149*3ac0a46fSAndroid Build Coastguard Worker                 image_matching_algorithm):
150*3ac0a46fSAndroid Build Coastguard Worker    path_templates = _PathTemplates(input_filename, source_dir, working_dir,
151*3ac0a46fSAndroid Build Coastguard Worker                                    self.os_name, self.suffix_order)
152*3ac0a46fSAndroid Build Coastguard Worker    for page in itertools.count():
153*3ac0a46fSAndroid Build Coastguard Worker      expected_paths = path_templates.GetExpectedPaths(page)
154*3ac0a46fSAndroid Build Coastguard Worker
155*3ac0a46fSAndroid Build Coastguard Worker      first_match = None
156*3ac0a46fSAndroid Build Coastguard Worker      last_match = None
157*3ac0a46fSAndroid Build Coastguard Worker      page_diff = ImageDiff(actual_path=path_templates.GetActualPath(page))
158*3ac0a46fSAndroid Build Coastguard Worker      if os.path.exists(page_diff.actual_path):
159*3ac0a46fSAndroid Build Coastguard Worker        # Match against all expected page images.
160*3ac0a46fSAndroid Build Coastguard Worker        for index, expected_path in enumerate(expected_paths):
161*3ac0a46fSAndroid Build Coastguard Worker          page_diff.expected_path = expected_path
162*3ac0a46fSAndroid Build Coastguard Worker          if not self._RunImageCompareCommand(page_diff,
163*3ac0a46fSAndroid Build Coastguard Worker                                              image_matching_algorithm):
164*3ac0a46fSAndroid Build Coastguard Worker            if first_match is None:
165*3ac0a46fSAndroid Build Coastguard Worker              first_match = index
166*3ac0a46fSAndroid Build Coastguard Worker            last_match = index
167*3ac0a46fSAndroid Build Coastguard Worker
168*3ac0a46fSAndroid Build Coastguard Worker        if last_match == 0:
169*3ac0a46fSAndroid Build Coastguard Worker          # Regeneration not needed. This case may be reached if only some, but
170*3ac0a46fSAndroid Build Coastguard Worker          # not all, pages need to be regenerated.
171*3ac0a46fSAndroid Build Coastguard Worker          continue
172*3ac0a46fSAndroid Build Coastguard Worker      elif expected_paths:
173*3ac0a46fSAndroid Build Coastguard Worker        # Remove all expected page images.
174*3ac0a46fSAndroid Build Coastguard Worker        print(f'WARNING: {input_filename} has extra expected page {page}')
175*3ac0a46fSAndroid Build Coastguard Worker        first_match = 0
176*3ac0a46fSAndroid Build Coastguard Worker        last_match = len(expected_paths)
177*3ac0a46fSAndroid Build Coastguard Worker      else:
178*3ac0a46fSAndroid Build Coastguard Worker        # No more expected or actual pages.
179*3ac0a46fSAndroid Build Coastguard Worker        break
180*3ac0a46fSAndroid Build Coastguard Worker
181*3ac0a46fSAndroid Build Coastguard Worker      # Try to reuse expectations by removing intervening non-matches.
182*3ac0a46fSAndroid Build Coastguard Worker      #
183*3ac0a46fSAndroid Build Coastguard Worker      # TODO(crbug.com/pdfium/1988): This can make mistakes due to a lack of
184*3ac0a46fSAndroid Build Coastguard Worker      # global knowledge about other test configurations, which is why it just
185*3ac0a46fSAndroid Build Coastguard Worker      # creates backup files rather than immediately removing files.
186*3ac0a46fSAndroid Build Coastguard Worker      if last_match is not None:
187*3ac0a46fSAndroid Build Coastguard Worker        if first_match > 1:
188*3ac0a46fSAndroid Build Coastguard Worker          print(f'WARNING: {input_filename}.{page} has non-adjacent match')
189*3ac0a46fSAndroid Build Coastguard Worker        if first_match != last_match:
190*3ac0a46fSAndroid Build Coastguard Worker          print(f'WARNING: {input_filename}.{page} has redundant matches')
191*3ac0a46fSAndroid Build Coastguard Worker
192*3ac0a46fSAndroid Build Coastguard Worker        for expected_path in expected_paths[:last_match]:
193*3ac0a46fSAndroid Build Coastguard Worker          os.rename(expected_path, expected_path + '.bak')
194*3ac0a46fSAndroid Build Coastguard Worker        continue
195*3ac0a46fSAndroid Build Coastguard Worker
196*3ac0a46fSAndroid Build Coastguard Worker      # Regenerate the most specific expected path that exists. If there are no
197*3ac0a46fSAndroid Build Coastguard Worker      # existing expectations, regenerate the base case.
198*3ac0a46fSAndroid Build Coastguard Worker      expected_path = path_templates.GetExpectedPath(page)
199*3ac0a46fSAndroid Build Coastguard Worker      shutil.copyfile(page_diff.actual_path, expected_path)
200*3ac0a46fSAndroid Build Coastguard Worker      self._RunCommand([_PNG_OPTIMIZER, expected_path])
201*3ac0a46fSAndroid Build Coastguard Worker
202*3ac0a46fSAndroid Build Coastguard Worker
203*3ac0a46fSAndroid Build Coastguard Worker_ACTUAL_TEMPLATE = '.pdf.%d.png'
204*3ac0a46fSAndroid Build Coastguard Worker_DIFF_TEMPLATE = '.pdf.%d.diff.png'
205*3ac0a46fSAndroid Build Coastguard Worker
206*3ac0a46fSAndroid Build Coastguard Worker
207*3ac0a46fSAndroid Build Coastguard Workerclass _PathTemplates:
208*3ac0a46fSAndroid Build Coastguard Worker
209*3ac0a46fSAndroid Build Coastguard Worker  def __init__(self, input_filename, source_dir, working_dir, os_name,
210*3ac0a46fSAndroid Build Coastguard Worker               suffix_order):
211*3ac0a46fSAndroid Build Coastguard Worker    input_root, _ = os.path.splitext(input_filename)
212*3ac0a46fSAndroid Build Coastguard Worker    self.actual_path_template = os.path.join(working_dir,
213*3ac0a46fSAndroid Build Coastguard Worker                                             input_root + _ACTUAL_TEMPLATE)
214*3ac0a46fSAndroid Build Coastguard Worker    self.diff_path_template = os.path.join(working_dir,
215*3ac0a46fSAndroid Build Coastguard Worker                                           input_root + _DIFF_TEMPLATE)
216*3ac0a46fSAndroid Build Coastguard Worker
217*3ac0a46fSAndroid Build Coastguard Worker    # Pre-create the available templates from most to least specific. We
218*3ac0a46fSAndroid Build Coastguard Worker    # generally expect the most specific case to match first.
219*3ac0a46fSAndroid Build Coastguard Worker    self.expected_templates = []
220*3ac0a46fSAndroid Build Coastguard Worker    for suffix in suffix_order:
221*3ac0a46fSAndroid Build Coastguard Worker      formatted_suffix = suffix.format(os=os_name)
222*3ac0a46fSAndroid Build Coastguard Worker      self.expected_templates.append(
223*3ac0a46fSAndroid Build Coastguard Worker          os.path.join(
224*3ac0a46fSAndroid Build Coastguard Worker              source_dir,
225*3ac0a46fSAndroid Build Coastguard Worker              f'{input_root}_expected{formatted_suffix}{_ACTUAL_TEMPLATE}'))
226*3ac0a46fSAndroid Build Coastguard Worker    assert self.expected_templates
227*3ac0a46fSAndroid Build Coastguard Worker
228*3ac0a46fSAndroid Build Coastguard Worker  def GetActualPath(self, page):
229*3ac0a46fSAndroid Build Coastguard Worker    return self.actual_path_template % page
230*3ac0a46fSAndroid Build Coastguard Worker
231*3ac0a46fSAndroid Build Coastguard Worker  def GetDiffPath(self, page):
232*3ac0a46fSAndroid Build Coastguard Worker    return self.diff_path_template % page
233*3ac0a46fSAndroid Build Coastguard Worker
234*3ac0a46fSAndroid Build Coastguard Worker  def _GetPossibleExpectedPaths(self, page):
235*3ac0a46fSAndroid Build Coastguard Worker    return [template % page for template in self.expected_templates]
236*3ac0a46fSAndroid Build Coastguard Worker
237*3ac0a46fSAndroid Build Coastguard Worker  def GetExpectedPaths(self, page):
238*3ac0a46fSAndroid Build Coastguard Worker    return list(filter(os.path.exists, self._GetPossibleExpectedPaths(page)))
239*3ac0a46fSAndroid Build Coastguard Worker
240*3ac0a46fSAndroid Build Coastguard Worker  def GetExpectedPath(self, page, default_to_base=True):
241*3ac0a46fSAndroid Build Coastguard Worker    """Returns the most specific expected path that exists."""
242*3ac0a46fSAndroid Build Coastguard Worker    last_not_found_expected_path = None
243*3ac0a46fSAndroid Build Coastguard Worker    for expected_path in self._GetPossibleExpectedPaths(page):
244*3ac0a46fSAndroid Build Coastguard Worker      if os.path.exists(expected_path):
245*3ac0a46fSAndroid Build Coastguard Worker        return expected_path
246*3ac0a46fSAndroid Build Coastguard Worker      last_not_found_expected_path = expected_path
247*3ac0a46fSAndroid Build Coastguard Worker    return last_not_found_expected_path if default_to_base else None
248