summaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/diff
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-03-24 22:03:23 +0300
committerrobot-piglet <[email protected]>2026-03-24 22:34:09 +0300
commit6092233e61d1dc129fe1eb007399cc192c5ceb59 (patch)
tree90522e5b7449e5cdb06bd24eafb333b9e9d3e9f1 /contrib/python/fonttools/fontTools/diff
parentc8c3fda4b2e47ceaad9790b7a5fb192110162f15 (diff)
Intermediate changes
commit_hash:5e2a2254279501ad2bde571fbd53c1a27a00e898
Diffstat (limited to 'contrib/python/fonttools/fontTools/diff')
-rw-r--r--contrib/python/fonttools/fontTools/diff/__init__.py441
-rw-r--r--contrib/python/fonttools/fontTools/diff/__main__.py6
-rw-r--r--contrib/python/fonttools/fontTools/diff/color.py44
-rw-r--r--contrib/python/fonttools/fontTools/diff/diff.py294
-rw-r--r--contrib/python/fonttools/fontTools/diff/utils.py28
5 files changed, 813 insertions, 0 deletions
diff --git a/contrib/python/fonttools/fontTools/diff/__init__.py b/contrib/python/fonttools/fontTools/diff/__init__.py
new file mode 100644
index 00000000000..b876520b2a6
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/__init__.py
@@ -0,0 +1,441 @@
+import argparse
+import os
+import sys
+import shutil
+import subprocess
+from typing import Iterable, Iterator, List, Optional, Text, Tuple
+
+from .color import color_unified_diff_line
+from .diff import run_external_diff, u_diff
+from .utils import file_exists, get_tables_argument_list
+
+
+def pipe_output(output: str) -> None:
+ """Pipes output to a pager if stdout is a TTY and a pager is available."""
+
+ if not output:
+ return
+
+ if not sys.stdout.isatty():
+ sys.stdout.write(output)
+ return
+
+ pager = os.getenv("PAGER") or shutil.which("less")
+
+ if not pager:
+ sys.stdout.write(output)
+ return
+
+ pager_cmd = [pager]
+ if "less" in os.path.basename(pager):
+ pager_cmd.append("-R")
+
+ proc = subprocess.Popen(pager_cmd, stdin=subprocess.PIPE, text=True)
+ try:
+ proc.stdin.write(output)
+ proc.stdin.close()
+ proc.wait()
+ except (BrokenPipeError, KeyboardInterrupt):
+ # Pager process was terminated before all output was written.
+ # This is not an error. The main exception handler will deal with it.
+ if proc.stdin:
+ proc.stdin.close()
+ # The process might still be running, but we have closed our side of the
+ # pipe. The Popen destructor will send a SIGKILL to the child.
+ except Exception:
+ if proc.stdin:
+ proc.stdin.close()
+ raise
+
+
+def _is_gnu_diff(diff_tool: str) -> bool:
+ """Returns True if the provided diff executable is GNU diff."""
+ try:
+ proc = subprocess.run(
+ [diff_tool, "--version"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ except OSError:
+ return False
+
+ version_output = (proc.stdout or "") + (proc.stderr or "")
+ return "GNU diffutils" in version_output
+
+
+def _iter_filtered_table_tags(
+ tags: Iterable[str],
+ include_tables: Optional[List[str]] = None,
+ exclude_tables: Optional[List[str]] = None,
+) -> Iterator[str]:
+ for tag in tags:
+ if exclude_tables and tag in exclude_tables:
+ continue
+ if include_tables and tag not in include_tables:
+ continue
+ yield tag
+
+
+def summarize(
+ file1: str,
+ file2: str,
+ include_tables: Optional[List[str]] = None,
+ exclude_tables: Optional[List[str]] = None,
+ font_number_1: int = -1,
+ font_number_2: int = -1,
+) -> Tuple[bool, str]:
+ from fontTools.ttLib import TTFont
+
+ with (
+ TTFont(file1, lazy=True, fontNumber=font_number_1) as font1,
+ TTFont(file2, lazy=True, fontNumber=font_number_2) as font2,
+ ):
+ tags1 = {str(tag) for tag in font1.reader.keys()}
+ tags2 = {str(tag) for tag in font2.reader.keys()}
+
+ all_tags = sorted(
+ set(
+ _iter_filtered_table_tags(
+ tags1 | tags2,
+ include_tables=include_tables,
+ exclude_tables=exclude_tables,
+ )
+ )
+ )
+
+ only1 = [tag for tag in all_tags if tag in tags1 and tag not in tags2]
+ only2 = [tag for tag in all_tags if tag in tags2 and tag not in tags1]
+ both = [tag for tag in all_tags if tag in tags1 and tag in tags2]
+
+ identical = True
+ lines: List[str] = []
+
+ lines.append(f"Binary table summary:\n")
+ lines.append(f" file1: {file1}\n")
+ lines.append(f" file2: {file2}\n")
+
+ if only1:
+ identical = False
+ lines.append(f"\nTables only in file1 ({len(only1)}):\n")
+ for tag in only1:
+ lines.append(f"- {tag} ({len(font1.reader[tag])} bytes)\n")
+ if only2:
+ identical = False
+ lines.append(f"\nTables only in file2 ({len(only2)}):\n")
+ for tag in only2:
+ lines.append(f"+ {tag} ({len(font2.reader[tag])} bytes)\n")
+
+ lines.append(f"\nTables in both ({len(both)}):\n")
+ for tag in both:
+ data1 = font1.reader[tag]
+ data2 = font2.reader[tag]
+ if data1 == data2:
+ lines.append(f" {tag}: SAME ({len(data1)} bytes)\n")
+ else:
+ identical = False
+ lines.append(f"* {tag}: DIFF ({len(data1)} vs {len(data2)} bytes)\n")
+
+ if identical:
+ lines.append("\nResult: SAME\n")
+ else:
+ lines.append("\nResult: DIFFERENT\n")
+
+ return identical, "".join(lines)
+
+
+def get_binary_exclude_tables(
+ file1: str,
+ file2: str,
+ include_tables: Optional[List[str]] = None,
+ exclude_tables: Optional[List[str]] = None,
+ font_number_1: int = -1,
+ font_number_2: int = -1,
+) -> Tuple[bool, str]:
+ from fontTools.ttLib import TTFont
+
+ with (
+ TTFont(file1, lazy=True, fontNumber=font_number_1) as font1,
+ TTFont(file2, lazy=True, fontNumber=font_number_2) as font2,
+ ):
+ tags1 = {str(tag) for tag in font1.reader.keys()}
+ tags2 = {str(tag) for tag in font2.reader.keys()}
+
+ all_tags = sorted(
+ set(
+ _iter_filtered_table_tags(
+ tags1 | tags2,
+ include_tables=include_tables,
+ exclude_tables=exclude_tables,
+ )
+ )
+ )
+
+ both = [tag for tag in all_tags if tag in tags1 and tag in tags2]
+ out = set()
+
+ for tag in both:
+ data1 = font1.reader[tag]
+ data2 = font2.reader[tag]
+ if data1 == data2:
+ out.add(tag)
+
+ return out
+
+
+def main():
+ """Compare two fonts for differences"""
+ # try/except block rationale:
+ # handles "premature" socket closure exception that is
+ # raised by Python when stdout is piped to tools like
+ # the `head` executable and socket is closed early
+ # see: https://docs.python.org/3/library/signal.html#note-on-sigpipe
+ ret = 0
+ try:
+ ret = run(sys.argv[1:])
+ except KeyboardInterrupt:
+ pass
+ except BrokenPipeError:
+ # Python flushes standard streams on exit; redirect remaining output
+ # to devnull to avoid another BrokenPipeError at shutdown
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ os.dup2(devnull, sys.stdout.fileno())
+ return ret
+
+
+def run(argv: List[Text]):
+ # ------------------------------------------
+ # argparse command line argument definitions
+ # ------------------------------------------
+ parser = argparse.ArgumentParser(
+ description="An OpenType table diff tool for fonts."
+ )
+ parser.add_argument(
+ "-l",
+ "--summary",
+ action="store_true",
+ help="Report table presence and binary equality only",
+ )
+ parser.add_argument(
+ "-U",
+ "--lines",
+ type=int,
+ default=3,
+ help="Number of context lines for unified diff (default: 3)",
+ )
+ parser.add_argument(
+ "-t",
+ "--include",
+ type=str,
+ nargs="+",
+ default=None,
+ help="Font tables to include. Multiple options are allowed.",
+ )
+ parser.add_argument(
+ "-x",
+ "--exclude",
+ type=str,
+ nargs="+",
+ default=None,
+ help="Font tables to exclude. Multiple options are allowed.",
+ )
+ parser.add_argument(
+ "--diff", type=str, help="Run external diff tool command (default: diff)"
+ )
+ parser.add_argument(
+ "--diff-arg",
+ type=str,
+ default=None,
+ help="External diff tool arguments (default: -u)",
+ )
+ parser.add_argument(
+ "--color",
+ choices=["auto", "never", "always"],
+ default="auto",
+ help="Whether to colorize output (default: auto)",
+ )
+ parser.add_argument(
+ "--y1",
+ type=int,
+ default=-1,
+ metavar="NUMBER",
+ help="Select font number for TrueType Collection (.ttc/.otc) FILE1, starting from 0",
+ )
+ parser.add_argument(
+ "--y2",
+ type=int,
+ default=-1,
+ metavar="NUMBER",
+ help="Select font number for TrueType Collection (.ttc/.otc) FILE2, starting from 0",
+ )
+ parser.add_argument(
+ "-a",
+ "--always",
+ action="store_true",
+ help="Compare tables even if binary identical",
+ )
+ parser.add_argument(
+ "-b",
+ "--binary",
+ action="store_true",
+ help="Compare tables only if binaries differ (default)",
+ )
+ parser.add_argument(
+ "-q", "--quiet", action="store_true", help="Suppress all output"
+ )
+ parser.add_argument("FILE1", help="Font file path 1")
+ parser.add_argument("FILE2", help="Font file path 2")
+
+ args: argparse.Namespace = parser.parse_args(argv)
+
+ # /////////////////////////////////////////////////////////
+ #
+ # Validations
+ #
+ # /////////////////////////////////////////////////////////
+
+ # ----------------------------------
+ # Incompatible argument validations
+ # ----------------------------------
+
+ if args.always and args.binary:
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] Error: --always and --binary are mutually exclusive options. "
+ f"Please use ONLY one of these options in your command.{os.linesep}"
+ )
+ return 2
+ if not args.always:
+ args.binary = True
+
+ # -------------------------------
+ # File path argument validations
+ # -------------------------------
+
+ if not file_exists(args.FILE1):
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] ERROR: The file path '{args.FILE1}' can not be found.{os.linesep}"
+ )
+ return 2
+ if not file_exists(args.FILE2):
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] ERROR: The file path '{args.FILE2}' can not be found.{os.linesep}"
+ )
+ return 2
+
+ # /////////////////////////////////////////////////////////
+ #
+ # Command line logic
+ #
+ # /////////////////////////////////////////////////////////
+
+ # parse explicitly included or excluded tables in
+ # the command line arguments
+ # set as a Python list if it was defined on the command line
+ # or as None if it was not set on the command line
+ include_list: Optional[List[Text]] = get_tables_argument_list(args.include)
+ exclude_list: Optional[List[Text]] = get_tables_argument_list(args.exclude)
+
+ if args.summary:
+ try:
+ identical, output = summarize(
+ args.FILE1,
+ args.FILE2,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_1=args.y1,
+ font_number_2=args.y2,
+ )
+ if not args.quiet:
+ sys.stdout.write(output)
+ return 0 if identical else 1
+ except Exception as e:
+ if not args.quiet:
+ sys.stderr.write(f"[*] ERROR: {e}{os.linesep}")
+ return 2
+
+ if args.binary:
+ excluded_binary_tables = get_binary_exclude_tables(
+ args.FILE1,
+ args.FILE2,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_1=args.y1,
+ font_number_2=args.y2,
+ )
+ if include_list is not None:
+ include_list = [
+ tag for tag in include_list if tag not in excluded_binary_tables
+ ]
+ else:
+ if exclude_list is None:
+ exclude_list = []
+ exclude_list.extend(sorted(excluded_binary_tables))
+
+ diff_tool = args.diff
+ color_output = args.color == "always" or (
+ args.color == "auto" and sys.stdout.isatty
+ )
+
+ if diff_tool is None:
+ diff_tool = shutil.which("diff")
+ elif diff_tool:
+ diff_tool = shutil.which(diff_tool)
+ if diff_tool is None:
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] ERROR: The external diff tool executable "
+ f"'{args.diff}' was not found.{os.linesep}"
+ )
+ return 2
+
+ try:
+ if diff_tool:
+ diff_arg = args.diff_arg
+ if diff_arg is None:
+ if args.lines == 3:
+ diff_arg = ["-u"]
+ else:
+ diff_arg = ["-u{}".format(args.lines)]
+ if _is_gnu_diff(diff_tool):
+ diff_arg.append(r"-F^\s\s<")
+ else:
+ diff_arg = diff_arg.split()
+
+ output = run_external_diff(
+ diff_tool,
+ diff_arg,
+ args.FILE1,
+ args.FILE2,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_a=args.y1,
+ font_number_b=args.y2,
+ use_multiprocess=True,
+ )
+ else:
+ output = u_diff(
+ args.FILE1,
+ args.FILE2,
+ context_lines=args.lines,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_a=args.y1,
+ font_number_b=args.y2,
+ use_multiprocess=True,
+ )
+
+ if color_output:
+ output = [color_unified_diff_line(line) for line in output]
+
+ output = "".join(output)
+ if not args.quiet:
+ pipe_output(output)
+ return 1 if output else 0
+
+ except Exception as e:
+ if not args.quiet:
+ sys.stderr.write(f"[*] ERROR: {e}{os.linesep}")
+ return 2
diff --git a/contrib/python/fonttools/fontTools/diff/__main__.py b/contrib/python/fonttools/fontTools/diff/__main__.py
new file mode 100644
index 00000000000..8dbef1dfc5d
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/__main__.py
@@ -0,0 +1,6 @@
+import sys
+from fontTools.diff import main
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/contrib/python/fonttools/fontTools/diff/color.py b/contrib/python/fonttools/fontTools/diff/color.py
new file mode 100644
index 00000000000..e6ef489d67e
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/color.py
@@ -0,0 +1,44 @@
+from typing import Dict, Text
+
+ansicolors: Dict[Text, Text] = {
+ "BLACK": "\033[30m",
+ "RED": "\033[31m",
+ "GREEN": "\033[32m",
+ "YELLOW": "\033[33m",
+ "BLUE": "\033[34m",
+ "MAGENTA": "\033[35m",
+ "CYAN": "\033[36m",
+ "WHITE": "\033[37m",
+ "BOLD": "\033[1m",
+ "RESET": "\033[0m",
+}
+
+green_start: Text = ansicolors["GREEN"]
+red_start: Text = ansicolors["RED"]
+cyan_start: Text = ansicolors["CYAN"]
+reset: Text = ansicolors["RESET"]
+
+
+def color_unified_diff_line(line: Text) -> Text:
+ """Returns an ANSI escape code colored string with color based
+ on the unified diff line type."""
+ if line[0:2] == "+ ":
+ return f"{green_start}{line}{reset}"
+ elif line == "+\n":
+ # some lines are formatted as hyphen only with no other characters
+ # this indicates an added empty line
+ return f"{green_start}{line}{reset}"
+ elif line[0:2] == "- ":
+ return f"{red_start}{line}{reset}"
+ elif line == "-\n":
+ # some lines are formatted as hyphen only with no other characters
+ # this indicates a deleted empty line
+ return f"{red_start}{line}{reset}"
+ elif line[0:3] == "@@ ":
+ return f"{cyan_start}{line}{reset}"
+ elif line[0:4] == "--- ":
+ return f"{red_start}{line}{reset}"
+ elif line[0:4] == "+++ ":
+ return f"{green_start}{line}{reset}"
+ else:
+ return line
diff --git a/contrib/python/fonttools/fontTools/diff/diff.py b/contrib/python/fonttools/fontTools/diff/diff.py
new file mode 100644
index 00000000000..302f2e677ae
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/diff.py
@@ -0,0 +1,294 @@
+import os
+import subprocess
+import tempfile
+from contextlib import contextmanager
+from difflib import unified_diff
+from multiprocessing import Pool, cpu_count
+from typing import Any, Callable, Iterable, Iterator, List, Optional, Text, Tuple
+
+from fontTools.ttLib import TTFont # type: ignore
+
+from .utils import get_file_modtime
+
+#
+#
+# Private functions
+#
+#
+
+
+def _get_fonts_and_save_xml(
+ filepath_a: Text,
+ filepath_b: Text,
+ tmpdirpath: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+ font_number_a: int,
+ font_number_b: int,
+ use_multiprocess: bool,
+) -> Tuple[Text, Text, Text, Text, Text, Text]:
+ post_pathname, postpath, pre_pathname, prepath = _get_pre_post_paths(
+ filepath_a, filepath_b
+ )
+ # instantiate left and right fontTools.ttLib.TTFont objects
+ tt_left = TTFont(prepath, fontNumber=font_number_a)
+ tt_right = TTFont(postpath, fontNumber=font_number_b)
+ left_ttxpath = os.path.join(tmpdirpath, "left.ttx")
+ right_ttxpath = os.path.join(tmpdirpath, "right.ttx")
+ _mp_save_ttx_xml(
+ tt_left,
+ tt_right,
+ left_ttxpath,
+ right_ttxpath,
+ exclude_tables,
+ include_tables,
+ use_multiprocess,
+ )
+ return left_ttxpath, right_ttxpath, pre_pathname, prepath, post_pathname, postpath
+
+
+def _get_pre_post_paths(
+ filepath_a: Text,
+ filepath_b: Text,
+) -> Tuple[Text, Text, Text, Text]:
+ prepath = filepath_a
+ postpath = filepath_b
+ pre_pathname = filepath_a
+ post_pathname = filepath_b
+ return post_pathname, postpath, pre_pathname, prepath
+
+
+def _mp_save_ttx_xml(
+ tt_left: Any,
+ tt_right: Any,
+ left_ttxpath: Text,
+ right_ttxpath: Text,
+ exclude_tables: Optional[List[Text]],
+ include_tables: Optional[List[Text]],
+ use_multiprocess: bool,
+) -> None:
+ if use_multiprocess and cpu_count() > 1:
+ # Use parallel fontTools.ttLib.TTFont.saveXML dump
+ # by default on multi CPU systems. This is a performance
+ # optimization. Profiling demonstrates that this can reduce
+ # execution time by up to 30% for some fonts
+ mp_args_list = [
+ (tt_left, left_ttxpath, include_tables, exclude_tables),
+ (tt_right, right_ttxpath, include_tables, exclude_tables),
+ ]
+ with Pool(processes=2) as pool:
+ pool.starmap(_ttfont_save_xml, mp_args_list)
+ else:
+ # use sequential fontTools.ttLib.TTFont.saveXML dumps
+ # when use_multiprocess is False or single CPU system
+ # detected
+ _ttfont_save_xml(tt_left, left_ttxpath, include_tables, exclude_tables)
+ _ttfont_save_xml(tt_right, right_ttxpath, include_tables, exclude_tables)
+
+
+def _ttfont_save_xml(
+ ttf: Any,
+ filepath: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+) -> bool:
+ """Writes TTX specification formatted XML to disk on filepath."""
+ ttf.saveXML(filepath, tables=include_tables, skipTables=exclude_tables)
+ return True
+
+
+@contextmanager
+def _saved_ttx_files(
+ filepath_a: Text,
+ filepath_b: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+ font_number_a: int,
+ font_number_b: int,
+ use_multiprocess: bool,
+) -> Iterator[Tuple[Text, Text, Text, Text, Text, Text]]:
+ with tempfile.TemporaryDirectory() as tmpdirpath:
+ yield _get_fonts_and_save_xml(
+ filepath_a,
+ filepath_b,
+ tmpdirpath,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ )
+
+
+def _diff_with_saved_ttx_files(
+ filepath_a: Text,
+ filepath_b: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+ font_number_a: int,
+ font_number_b: int,
+ use_multiprocess: bool,
+ create_differ: Callable[[Text, Text, Text, Text, Text, Text], Iterable[Text]],
+) -> Iterator[Text]:
+ with _saved_ttx_files(
+ filepath_a,
+ filepath_b,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ ) as (
+ left_ttxpath,
+ right_ttxpath,
+ pre_pathname,
+ prepath,
+ post_pathname,
+ postpath,
+ ):
+ yield from create_differ(
+ left_ttxpath,
+ right_ttxpath,
+ pre_pathname,
+ prepath,
+ post_pathname,
+ postpath,
+ )
+
+
+#
+#
+# Public functions
+#
+#
+
+
+def u_diff(
+ filepath_a: Text,
+ filepath_b: Text,
+ context_lines: int = 3,
+ include_tables: Optional[List[Text]] = None,
+ exclude_tables: Optional[List[Text]] = None,
+ font_number_a: int = -1,
+ font_number_b: int = -1,
+ use_multiprocess: bool = True,
+) -> Iterator[Text]:
+ """Performs a unified diff on a TTX serialized data format dump of font binary data using
+ a modified version of the Python standard libary difflib module.
+
+ filepath_a: (string) pre-file local file path
+ filepath_b: (string) post-file local file path
+ context_lines: (int) number of context lines to include in the diff (default=3)
+ include_tables: (list of str) Python list of OpenType tables to include in the diff
+ exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff
+ use_multiprocess: (bool) use multi-processor optimizations (default=True)
+
+ include_tables and exclude_tables are mutually exclusive arguments. Only one should
+ be defined
+
+ :returns: Generator of ordered diff line strings that include newline line endings
+ :raises: KeyError if include_tables or exclude_tables includes a mis-specified table
+ that is not included in filepath_a OR filepath_b
+ """
+
+ def _create_unified_diff(
+ left_ttxpath: Text,
+ right_ttxpath: Text,
+ pre_pathname: Text,
+ prepath: Text,
+ post_pathname: Text,
+ postpath: Text,
+ ) -> Iterable[Text]:
+ with open(left_ttxpath) as ff:
+ fromlines = ff.readlines()
+ with open(right_ttxpath) as tf:
+ tolines = tf.readlines()
+
+ fromdate = get_file_modtime(prepath)
+ todate = get_file_modtime(postpath)
+
+ yield from unified_diff(
+ fromlines,
+ tolines,
+ pre_pathname,
+ post_pathname,
+ fromdate,
+ todate,
+ n=context_lines,
+ )
+
+ yield from _diff_with_saved_ttx_files(
+ filepath_a,
+ filepath_b,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ _create_unified_diff,
+ )
+
+
+def run_external_diff(
+ diff_tool: Text,
+ diff_args: List[Text],
+ filepath_a: Text,
+ filepath_b: Text,
+ include_tables: Optional[List[Text]] = None,
+ exclude_tables: Optional[List[Text]] = None,
+ font_number_a: int = -1,
+ font_number_b: int = -1,
+ use_multiprocess: bool = True,
+) -> Iterator[Text]:
+ """Performs a unified diff on a TTX serialized data format dump of font binary data using
+ an external diff executable that is requested by the caller via `command`
+
+ diff_tool: (string) command line executable string
+ diff_args: (list of strings) arguments for the diff tool
+ filepath_a: (string) pre-file local file path
+ filepath_b: (string) post-file local file path
+ include_tables: (list of str) Python list of OpenType tables to include in the diff
+ exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff
+ use_multiprocess: (bool) use multi-processor optimizations (default=True)
+
+ include_tables and exclude_tables are mutually exclusive arguments. Only one should
+ be defined
+
+ :returns: Generator of ordered diff line strings that include newline line endings
+ :raises: KeyError if include_tables or exclude_tables includes a mis-specified table
+ that is not included in filepath_a OR filepath_b
+ :raises: IOError if exception raised during execution of `command` on TTX files
+ """
+
+ def _create_external_diff(
+ left_ttxpath: Text,
+ right_ttxpath: Text,
+ _pre_pathname: Text,
+ _prepath: Text,
+ _post_pathname: Text,
+ _postpath: Text,
+ ) -> Iterable[Text]:
+ command = [diff_tool] + diff_args + [left_ttxpath, right_ttxpath]
+ process = subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf8",
+ )
+
+ for line in process.stdout:
+ yield line
+ err = process.stderr.read()
+ if err:
+ raise IOError(err)
+
+ yield from _diff_with_saved_ttx_files(
+ filepath_a,
+ filepath_b,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ _create_external_diff,
+ )
diff --git a/contrib/python/fonttools/fontTools/diff/utils.py b/contrib/python/fonttools/fontTools/diff/utils.py
new file mode 100644
index 00000000000..aed97063636
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/utils.py
@@ -0,0 +1,28 @@
+import os
+from datetime import datetime, timezone
+from typing import List, Optional, Text, Union
+
+
+def file_exists(path: Union[bytes, str, "os.PathLike[Text]"]) -> bool:
+ """Validates file path as existing local file"""
+ return os.path.isfile(path)
+
+
+def get_file_modtime(path: Union[bytes, str, "os.PathLike[Text]"]) -> Text:
+ """Returns ISO formatted file modification time in local system timezone"""
+ return (
+ datetime.fromtimestamp(os.stat(path).st_mtime, timezone.utc)
+ .astimezone()
+ .isoformat()
+ )
+
+
+def get_tables_argument_list(table_list: Optional[List[Text]]) -> Optional[List[Text]]:
+ """Converts a list of OpenType table string into a Python list or
+ return None if the table_list was not defined (i.e., it was not included
+ in an option on the command line). Tables that are composed of three
+ characters must be right padded with a space."""
+ if table_list is None:
+ return None
+ else:
+ return [table.ljust(4) for table in table_list]