#!/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