From 5f2b26791fe1a95ce022cb5e3dc21e389410d8cb Mon Sep 17 00:00:00 2001 From: Akira Kakuto Date: Wed, 1 Sep 2021 05:58:30 +0000 Subject: freetype 2.11.0 git-svn-id: svn://tug.org/texlive/trunk@60386 c570f23f-e606-0410-a88d-b1316a301751 --- .../libs/freetype2/freetype-src/tests/README.md | 22 ++ .../freetype2/freetype-src/tests/issue-1063/main.c | 49 ++++ .../libs/freetype2/freetype-src/tests/meson.build | 14 + .../tests/scripts/download-test-fonts.py | 302 +++++++++++++++++++++ 4 files changed, 387 insertions(+) create mode 100644 Build/source/libs/freetype2/freetype-src/tests/README.md create mode 100644 Build/source/libs/freetype2/freetype-src/tests/issue-1063/main.c create mode 100644 Build/source/libs/freetype2/freetype-src/tests/meson.build create mode 100644 Build/source/libs/freetype2/freetype-src/tests/scripts/download-test-fonts.py (limited to 'Build/source/libs/freetype2/freetype-src/tests') diff --git a/Build/source/libs/freetype2/freetype-src/tests/README.md b/Build/source/libs/freetype2/freetype-src/tests/README.md new file mode 100644 index 00000000000..0d0b99aa86f --- /dev/null +++ b/Build/source/libs/freetype2/freetype-src/tests/README.md @@ -0,0 +1,22 @@ +# Unit and regression tests for the FreeType library + +## Quick Start + +### Download test fonts + +Run the `tests/scripts/download-fonts.py` script, which will +download test fonts to the `tests/data/` directory first. + +### Build the test programs + +The tests are only built with the Meson build system, and +are disabled by default, enable the 'tests' option to compile +them, as in: + + meson setup out -Dtests=enabled + meson compile -C out + +### Run the test programs + + meson test -C out + diff --git a/Build/source/libs/freetype2/freetype-src/tests/issue-1063/main.c b/Build/source/libs/freetype2/freetype-src/tests/issue-1063/main.c new file mode 100644 index 00000000000..cb06be1f5b8 --- /dev/null +++ b/Build/source/libs/freetype2/freetype-src/tests/issue-1063/main.c @@ -0,0 +1,49 @@ +#include +#include + +#include +#include + + +int +main( void ) +{ + FT_Library library; + FT_Face face = NULL; + + /* + * We assume that `FREETYPE_TESTS_DATA_DIR` was set by `meson test`. + * Otherwise we default to `../tests/data`. + * + * TODO (David): Rewrite this to pass the test directory through the + * command-line. + */ + const char* testdata_dir = getenv( "FREETYPE_TESTS_DATA_DIR" ); + char filepath[PATH_MAX]; + + + snprintf( filepath, sizeof( filepath ), "%s/%s", + testdata_dir ? testdata_dir : "../tests/data", + "As.I.Lay.Dying.ttf" ); + + FT_Init_FreeType( &library ); + if ( FT_New_Face( library, filepath, 0, &face ) != 0 ) + { + fprintf( stderr, "Could not open file: %s\n", filepath ); + return 1; + } + + for ( FT_ULong i = 59; i < 171; i++ ) + { + FT_UInt gid = FT_Get_Char_Index( face, i ); + FT_Error code = FT_Load_Glyph( face, gid, FT_LOAD_DEFAULT ); + + + if ( code ) + printf( "unknown %d for char %lu, gid %u\n", code, i, gid ); + } + + return 0; +} + +/* EOF */ diff --git a/Build/source/libs/freetype2/freetype-src/tests/meson.build b/Build/source/libs/freetype2/freetype-src/tests/meson.build new file mode 100644 index 00000000000..527998ffa46 --- /dev/null +++ b/Build/source/libs/freetype2/freetype-src/tests/meson.build @@ -0,0 +1,14 @@ +test_issue_1063 = executable('issue-1063', + files([ 'issue-1063/main.c' ]), + dependencies: freetype_dep, +) + +test_env = ['FREETYPE_TESTS_DATA_DIR=' + + join_paths(meson.current_source_dir(), 'data')] + +test('issue-1063', + test_issue_1063, + env: test_env, + suite: 'regression') + +# EOF diff --git a/Build/source/libs/freetype2/freetype-src/tests/scripts/download-test-fonts.py b/Build/source/libs/freetype2/freetype-src/tests/scripts/download-test-fonts.py new file mode 100644 index 00000000000..52b742e22ca --- /dev/null +++ b/Build/source/libs/freetype2/freetype-src/tests/scripts/download-test-fonts.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 + +"""Download test fonts used by the FreeType regression test programs. These +will be copied to $FREETYPE/tests/data/ by default.""" + +import argparse +import collections +import hashlib +import io +import os +import requests +import sys +import zipfile + +from typing import Callable, List, Optional, Tuple + +# The list of download items describing the font files to install. Each +# download item is a dictionary with one of the following schemas: +# +# - File item: +# +# file_url +# Type: URL string. +# Required: Yes. +# Description: URL to download the file from. +# +# install_name +# Type: file name string +# Required: No +# Description: Installation name for the font file, only provided if +# it must be different from the original URL's basename. +# +# hex_digest +# Type: hexadecimal string +# Required: No +# Description: Digest of the input font file. +# +# - Zip items: +# +# These items correspond to one or more font files that are embedded in a +# remote zip archive. Each entry has the following fields: +# +# zip_url +# Type: URL string. +# Required: Yes. +# Description: URL to download the zip archive from. +# +# zip_files +# Type: List of file entries (see below) +# Required: Yes +# Description: A list of entries describing a single font file to be +# extracted from the archive +# +# Apart from that, some schemas are used for dictionaries used inside +# download items: +# +# - File entries: +# +# These are dictionaries describing a single font file to extract from an +# archive. +# +# filename +# Type: file path string +# Required: Yes +# Description: Path of source file, relative to the archive's +# top-level directory. +# +# install_name +# Type: file name string +# Required: No +# Description: Installation name for the font file; only provided if +# it must be different from the original filename value. +# +# hex_digest +# Type: hexadecimal string +# Required: No +# Description: Digest of the input source file +# +_DOWNLOAD_ITEMS = [ + { + "zip_url": "https://github.com/python-pillow/Pillow/files/6622147/As.I.Lay.Dying.zip", + "zip_files": [ + { + "filename": "As I Lay Dying.ttf", + "install_name": "As.I.Lay.Dying.ttf", + "hex_digest": "ef146bbc2673b387", + }, + ], + }, +] + + +def digest_data(data: bytes): + """Compute the digest of a given input byte string, which are the first + 8 bytes of its sha256 hash.""" + m = hashlib.sha256() + m.update(data) + return m.digest()[:8] + + +def check_existing(path: str, hex_digest: str): + """Return True if |path| exists and matches |hex_digest|.""" + if not os.path.exists(path) or hex_digest is None: + return False + + with open(path, "rb") as f: + existing_content = f.read() + + return bytes.fromhex(hex_digest) == digest_data(existing_content) + + +def install_file(content: bytes, dest_path: str): + """Write a byte string to a given destination file. + + Args: + content: Input data, as a byte string + dest_path: Installation path + """ + parent_path = os.path.dirname(dest_path) + if not os.path.exists(parent_path): + os.makedirs(parent_path) + + with open(dest_path, "wb") as f: + f.write(content) + + +def download_file(url: str, expected_digest: Optional[bytes] = None): + """Download a file from a given URL. + + Args: + url: Input URL + expected_digest: Optional digest of the file + as a byte string + Returns: + URL content as binary string. + """ + r = requests.get(url, allow_redirects=True) + content = r.content + if expected_digest is not None: + digest = digest_data(r.content) + if digest != expected_digest: + raise ValueError( + "%s has invalid digest %s (expected %s)" + % (url, digest.hex(), expected_digest.hex()) + ) + + return content + + +def extract_file_from_zip_archive( + archive: zipfile.ZipFile, + archive_name: str, + filepath: str, + expected_digest: Optional[bytes] = None, +): + """Extract a file from a given zipfile.ZipFile archive. + + Args: + archive: Input ZipFile objec. + archive_name: Archive name or URL, only used to generate a + human-readable error message. + + filepath: Input filepath in archive. + expected_digest: Optional digest for the file. + Returns: + A new File instance corresponding to the extract file. + Raises: + ValueError if expected_digest is not None and does not match the + extracted file. + """ + file = archive.open(filepath) + if expected_digest is not None: + digest = digest_data(archive.open(filepath).read()) + if digest != expected_digest: + raise ValueError( + "%s in zip archive at %s has invalid digest %s (expected %s)" + % (filepath, archive_name, digest.hex(), expected_digest.hex()) + ) + return file.read() + + +def _get_and_install_file( + install_path: str, + hex_digest: Optional[str], + force_download: bool, + get_content: Callable[[], bytes], +) -> bool: + if not force_download and hex_digest is not None \ + and os.path.exists(install_path): + with open(install_path, "rb") as f: + content: bytes = f.read() + if bytes.fromhex(hex_digest) == digest_data(content): + return False + + content = get_content() + install_file(content, install_path) + return True + + +def download_and_install_item( + item: dict, install_dir: str, force_download: bool +) -> List[Tuple[str, bool]]: + """Download and install one item. + + Args: + item: Download item as a dictionary, see above for schema. + install_dir: Installation directory. + force_download: Set to True to force download and installation, even + if the font file is already installed with the right content. + + Returns: + A list of (install_name, status) tuples, where 'install_name' is the + file's installation name under 'install_dir', and 'status' is a + boolean that is True to indicate that the file was downloaded and + installed, or False to indicate that the file is already installed + with the right content. + """ + if "file_url" in item: + file_url = item["file_url"] + install_name = item.get("install_name", os.path.basename(file_url)) + install_path = os.path.join(install_dir, install_name) + hex_digest = item.get("hex_digest") + + def get_content(): + return download_file(file_url, hex_digest) + + status = _get_and_install_file( + install_path, hex_digest, force_download, get_content + ) + return [(install_name, status)] + + if "zip_url" in item: + # One or more files from a zip archive. + archive_url = item["zip_url"] + archive = zipfile.ZipFile(io.BytesIO(download_file(archive_url))) + + result = [] + for f in item["zip_files"]: + filename = f["filename"] + install_name = f.get("install_name", filename) + hex_digest = f.get("hex_digest") + + def get_content(): + return extract_file_from_zip_archive( + archive, + archive_url, + filename, + bytes.fromhex(hex_digest) if hex_digest else None, + ) + + status = _get_and_install_file( + os.path.join(install_dir, install_name), + hex_digest, + force_download, + get_content, + ) + result.append((install_name, status)) + + return result + + else: + raise ValueError("Unknown download item schema: %s" % item) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + + # Assume this script is under tests/scripts/ and tests/data/ + # is the default installation directory. + install_dir = os.path.normpath( + os.path.join(os.path.dirname(__file__), "..", "data") + ) + + parser.add_argument( + "--force", + action="store_true", + default=False, + help="Force download and installation of font files", + ) + + parser.add_argument( + "--install-dir", + default=install_dir, + help="Specify installation directory [%s]" % install_dir, + ) + + args = parser.parse_args() + + for item in _DOWNLOAD_ITEMS: + for install_name, status in download_and_install_item( + item, args.install_dir, args.force + ): + print("%s %s" % (install_name, + "INSTALLED" if status else "UP-TO-DATE")) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) + +# EOF -- cgit v1.2.3