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