#!/usr/bin/env python3 # # SPDX-License-Identifier: BSD-2-Clause # # Copyright 2022 Raspberry Pi Ltd # # Script to pretty print a Raspberry Pi tuning config JSON structure in # version 2.0 and later formats. import argparse import json import textwrap class Encoder(json.JSONEncoder): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.indentation_level = 0 self.hard_break = 120 self.custom_elems = { 'table': 16, 'luminance_lut': 16, 'ct_curve': 3, 'ccm': 3, 'gamma_curve': 2, 'y_target': 2, 'prior': 2 } def encode(self, o, node_key=None): if isinstance(o, (list, tuple)): # Check if we are a flat list of numbers. if not any(isinstance(el, (list, tuple, dict)) for el in o): s = ', '.join(json.dumps(el) for el in o) if node_key in self.custom_elems.keys(): # Special case handling to specify number of elements in a row for tables, ccm, etc. self.indentation_level += 1 sl = s.split(', ') num = self.custom_elems[node_key] chunk = [self.indent_str + ', '.join(sl[x:x + num]) for x in range(0, len(sl), num)] t = ',\n'.join(chunk) self.indentation_level -= 1 output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]' elif len(s) > self.hard_break - len(self.indent_str): # Break a long list with wraps. self.indentation_level += 1 t = textwrap.fill(s, self.hard_break, break_long_words=False, initial_indent=self.indent_str, subsequent_indent=self.indent_str) self.indentation_level -= 1 output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]' else: # Smaller lists can remain on a single line. output = f' [ {s} ]' return output else: # Sub-structures in the list case. self.indentation_level += 1 output = [self.indent_str + self.encode(el) for el in o] self.indentation_level -= 1 output = ',\n'.join(output) return f' [\n{output}\n{self.indent_str}]' elif isinstance(o, dict): self.indentation_level += 1 output = [] for k, v in o.items(): if isinstance(v, dict) and len(v) == 0: # Empty config block special case. output.append(self.indent_str + f'{json.dumps(k)}: {{ }}') else: # Only linebreak if the next node is a config block. sep = f'\n{self.indent_str}' if isinstance(v, dict) else '' output.append(self.indent_str + f'{json.dumps(k)}:{sep}{self.encode(v, k)}') output = ',\n'.join(output) self.indentation_level -= 1 return f'{{\n{output}\n{self.indent_str}}}' else: return ' ' + json.dumps(o) @property def indent_str(self) -> str: return ' ' * self.indentation_level * self.indent def iterencode(self, o, **kwargs): return self.encode(o) def pretty_print(in_json: dict) -> str: if 'version' not in in_json or \ 'target' not in in_json or \ 'algorithms' not in in_json or \ in_json['version'] < 2.0: raise RuntimeError('Incompatible JSON dictionary has been provided') return json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False) if __name__ == "__main__": parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description= 'Prettify a version 2.0 camera tuning config JSON file.') parser.add_argument('input', type=str, help='Input tuning file.') parser.add_argument('output', type=str, nargs='?', help='Output converted tuning file. If not provided, the input file will be updated in-place.', default=None) args = parser.parse_args() with open(args.input, 'r') as f: in_json = json.load(f) out_json = pretty_print(in_json) with open(args.output if args.output is not None else args.input, 'w') as f: f.write(out_json)