(root)/
freetype-2.13.2/
tests/
scripts/
download-test-fonts.py
       1  #!/usr/bin/env python3
       2  
       3  """Download test fonts used by the FreeType regression test programs.  These
       4  will be copied to $FREETYPE/tests/data/ by default."""
       5  
       6  import argparse
       7  import collections
       8  import hashlib
       9  import io
      10  import os
      11  import requests
      12  import sys
      13  import zipfile
      14  
      15  from typing import Callable, List, Optional, Tuple
      16  
      17  # The list of download items describing the font files to install.  Each
      18  # download item is a dictionary with one of the following schemas:
      19  #
      20  # - File item:
      21  #
      22  #      file_url
      23  #        Type: URL string.
      24  #        Required: Yes.
      25  #        Description: URL to download the file from.
      26  #
      27  #      install_name
      28  #        Type: file name string
      29  #        Required: No
      30  #        Description: Installation name for the font file, only provided if
      31  #          it must be different from the original URL's basename.
      32  #
      33  #      hex_digest
      34  #        Type: hexadecimal string
      35  #        Required: No
      36  #        Description: Digest of the input font file.
      37  #
      38  # - Zip items:
      39  #
      40  #   These items correspond to one or more font files that are embedded in a
      41  #   remote zip archive.  Each entry has the following fields:
      42  #
      43  #      zip_url
      44  #        Type: URL string.
      45  #        Required: Yes.
      46  #        Description: URL to download the zip archive from.
      47  #
      48  #      zip_files
      49  #        Type: List of file entries (see below)
      50  #        Required: Yes
      51  #        Description: A list of entries describing a single font file to be
      52  #          extracted from the archive
      53  #
      54  # Apart from that, some schemas are used for dictionaries used inside
      55  # download items:
      56  #
      57  # - File entries:
      58  #
      59  #   These are dictionaries describing a single font file to extract from an
      60  #   archive.
      61  #
      62  #      filename
      63  #        Type: file path string
      64  #        Required: Yes
      65  #        Description: Path of source file, relative to the archive's
      66  #          top-level directory.
      67  #
      68  #      install_name
      69  #        Type: file name string
      70  #        Required: No
      71  #        Description: Installation name for the font file; only provided if
      72  #          it must be different from the original filename value.
      73  #
      74  #      hex_digest
      75  #        Type: hexadecimal string
      76  #        Required: No
      77  #        Description: Digest of the input source file
      78  #
      79  _DOWNLOAD_ITEMS = [
      80      {
      81          "zip_url": "https://github.com/python-pillow/Pillow/files/6622147/As.I.Lay.Dying.zip",
      82          "zip_files": [
      83              {
      84                  "filename": "As I Lay Dying.ttf",
      85                  "install_name": "As.I.Lay.Dying.ttf",
      86                  "hex_digest": "ef146bbc2673b387",
      87              },
      88          ],
      89      },
      90  ]
      91  
      92  
      93  def digest_data(data: bytes):
      94      """Compute the digest of a given input byte string, which are the first
      95      8 bytes of its sha256 hash."""
      96      m = hashlib.sha256()
      97      m.update(data)
      98      return m.digest()[:8]
      99  
     100  
     101  def check_existing(path: str, hex_digest: str):
     102      """Return True if |path| exists and matches |hex_digest|."""
     103      if not os.path.exists(path) or hex_digest is None:
     104          return False
     105  
     106      with open(path, "rb") as f:
     107          existing_content = f.read()
     108  
     109      return bytes.fromhex(hex_digest) == digest_data(existing_content)
     110  
     111  
     112  def install_file(content: bytes, dest_path: str):
     113      """Write a byte string to a given destination file.
     114  
     115      Args:
     116        content: Input data, as a byte string
     117        dest_path: Installation path
     118      """
     119      parent_path = os.path.dirname(dest_path)
     120      if not os.path.exists(parent_path):
     121          os.makedirs(parent_path)
     122  
     123      with open(dest_path, "wb") as f:
     124          f.write(content)
     125  
     126  
     127  def download_file(url: str, expected_digest: Optional[bytes] = None):
     128      """Download a file from a given URL.
     129  
     130      Args:
     131        url: Input URL
     132        expected_digest: Optional digest of the file
     133          as a byte string
     134      Returns:
     135        URL content as binary string.
     136      """
     137      r = requests.get(url, allow_redirects=True)
     138      content = r.content
     139      if expected_digest is not None:
     140          digest = digest_data(r.content)
     141          if digest != expected_digest:
     142              raise ValueError(
     143                  "%s has invalid digest %s (expected %s)"
     144                  % (url, digest.hex(), expected_digest.hex())
     145              )
     146  
     147      return content
     148  
     149  
     150  def extract_file_from_zip_archive(
     151      archive: zipfile.ZipFile,
     152      archive_name: str,
     153      filepath: str,
     154      expected_digest: Optional[bytes] = None,
     155  ):
     156      """Extract a file from a given zipfile.ZipFile archive.
     157  
     158      Args:
     159        archive: Input ZipFile objec.
     160        archive_name: Archive name or URL, only used to generate a
     161          human-readable error message.
     162  
     163        filepath: Input filepath in archive.
     164        expected_digest: Optional digest for the file.
     165      Returns:
     166        A new File instance corresponding to the extract file.
     167      Raises:
     168        ValueError if expected_digest is not None and does not match the
     169        extracted file.
     170      """
     171      file = archive.open(filepath)
     172      if expected_digest is not None:
     173          digest = digest_data(archive.open(filepath).read())
     174          if digest != expected_digest:
     175              raise ValueError(
     176                  "%s in zip archive at %s has invalid digest %s (expected %s)"
     177                  % (filepath, archive_name, digest.hex(), expected_digest.hex())
     178              )
     179      return file.read()
     180  
     181  
     182  def _get_and_install_file(
     183      install_path: str,
     184      hex_digest: Optional[str],
     185      force_download: bool,
     186      get_content: Callable[[], bytes],
     187  ) -> bool:
     188      if not force_download and hex_digest is not None \
     189        and os.path.exists(install_path):
     190          with open(install_path, "rb") as f:
     191              content: bytes = f.read()
     192          if bytes.fromhex(hex_digest) == digest_data(content):
     193              return False
     194  
     195      content = get_content()
     196      install_file(content, install_path)
     197      return True
     198  
     199  
     200  def download_and_install_item(
     201      item: dict, install_dir: str, force_download: bool
     202  ) -> List[Tuple[str, bool]]:
     203      """Download and install one item.
     204  
     205      Args:
     206        item: Download item as a dictionary, see above for schema.
     207        install_dir: Installation directory.
     208        force_download: Set to True to force download and installation, even
     209          if the font file is already installed with the right content.
     210  
     211      Returns:
     212        A list of (install_name, status) tuples, where 'install_name' is the
     213        file's installation name under 'install_dir', and 'status' is a
     214        boolean that is True to indicate that the file was downloaded and
     215        installed, or False to indicate that the file is already installed
     216        with the right content.
     217      """
     218      if "file_url" in item:
     219          file_url = item["file_url"]
     220          install_name = item.get("install_name", os.path.basename(file_url))
     221          install_path = os.path.join(install_dir, install_name)
     222          hex_digest = item.get("hex_digest")
     223  
     224          def get_content():
     225              return download_file(file_url, hex_digest)
     226  
     227          status = _get_and_install_file(
     228              install_path, hex_digest, force_download, get_content
     229          )
     230          return [(install_name, status)]
     231  
     232      if "zip_url" in item:
     233          # One or more files from a zip archive.
     234          archive_url = item["zip_url"]
     235          archive = zipfile.ZipFile(io.BytesIO(download_file(archive_url)))
     236  
     237          result = []
     238          for f in item["zip_files"]:
     239              filename = f["filename"]
     240              install_name = f.get("install_name", filename)
     241              hex_digest = f.get("hex_digest")
     242  
     243              def get_content():
     244                  return extract_file_from_zip_archive(
     245                      archive,
     246                      archive_url,
     247                      filename,
     248                      bytes.fromhex(hex_digest) if hex_digest else None,
     249                  )
     250  
     251              status = _get_and_install_file(
     252                  os.path.join(install_dir, install_name),
     253                  hex_digest,
     254                  force_download,
     255                  get_content,
     256              )
     257              result.append((install_name, status))
     258  
     259          return result
     260  
     261      else:
     262          raise ValueError("Unknown download item schema: %s" % item)
     263  
     264  
     265  def main():
     266      parser = argparse.ArgumentParser(description=__doc__)
     267  
     268      # Assume this script is under tests/scripts/ and tests/data/
     269      # is the default installation directory.
     270      install_dir = os.path.normpath(
     271          os.path.join(os.path.dirname(__file__), "..", "data")
     272      )
     273  
     274      parser.add_argument(
     275          "--force",
     276          action="store_true",
     277          default=False,
     278          help="Force download and installation of font files",
     279      )
     280  
     281      parser.add_argument(
     282          "--install-dir",
     283          default=install_dir,
     284          help="Specify installation directory [%s]" % install_dir,
     285      )
     286  
     287      args = parser.parse_args()
     288  
     289      for item in _DOWNLOAD_ITEMS:
     290          for install_name, status in download_and_install_item(
     291              item, args.install_dir, args.force
     292          ):
     293              print("%s %s" % (install_name,
     294                               "INSTALLED" if status else "UP-TO-DATE"))
     295  
     296      return 0
     297  
     298  
     299  if __name__ == "__main__":
     300      sys.exit(main())
     301  
     302  # EOF