PCQRSCANER/venv/Lib/site-packages/pptx/text/fonts.py
2019-12-22 21:51:47 +01:00

422 lines
13 KiB
Python

# encoding: utf-8
"""
Objects related to system font file lookup.
"""
from __future__ import absolute_import, print_function
import os
import sys
from struct import calcsize, unpack_from
from ..util import lazyproperty
class FontFiles(object):
"""
A class-based singleton serving as a lazy cache for system font details.
"""
_font_files = None
@classmethod
def find(cls, family_name, is_bold, is_italic):
"""
Return the absolute path to the installed OpenType font having
*family_name* and the styles *is_bold* and *is_italic*.
"""
if cls._font_files is None:
cls._font_files = cls._installed_fonts()
return cls._font_files[(family_name, is_bold, is_italic)]
@classmethod
def _installed_fonts(cls):
"""
Return a dict mapping a font descriptor to its font file path,
containing all the font files resident on the current machine. The
font descriptor is a (family_name, is_bold, is_italic) 3-tuple.
"""
fonts = {}
for d in cls._font_directories():
for key, path in cls._iter_font_files_in(d):
fonts[key] = path
return fonts
@classmethod
def _font_directories(cls):
"""
Return a sequence of directory paths likely to contain fonts on the
current platform.
"""
if sys.platform.startswith("darwin"):
return cls._os_x_font_directories()
if sys.platform.startswith("win32"):
return cls._windows_font_directories()
raise OSError("unsupported operating system")
@classmethod
def _iter_font_files_in(cls, directory):
"""
Generate the OpenType font files found in and under *directory*. Each
item is a key/value pair. The key is a (family_name, is_bold,
is_italic) 3-tuple, like ('Arial', True, False), and the value is the
absolute path to the font file.
"""
for root, dirs, files in os.walk(directory):
for filename in files:
file_ext = os.path.splitext(filename)[1]
if file_ext.lower() not in (".otf", ".ttf"):
continue
path = os.path.abspath(os.path.join(root, filename))
with _Font.open(path) as f:
yield ((f.family_name, f.is_bold, f.is_italic), path)
@classmethod
def _os_x_font_directories(cls):
"""
Return a sequence of directory paths on a Mac in which fonts are
likely to be located.
"""
os_x_font_dirs = [
"/Library/Fonts",
"/Network/Library/Fonts",
"/System/Library/Fonts",
]
home = os.environ.get("HOME")
if home is not None:
os_x_font_dirs.extend(
[os.path.join(home, "Library", "Fonts"), os.path.join(home, ".fonts")]
)
return os_x_font_dirs
@classmethod
def _windows_font_directories(cls):
"""
Return a sequence of directory paths on Windows in which fonts are
likely to be located.
"""
return [r"C:\Windows\Fonts"]
class _Font(object):
"""
A wrapper around an OTF/TTF font file stream that knows how to parse it
for its name and style characteristics, e.g. bold and italic.
"""
def __init__(self, stream):
self._stream = stream
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, exception_tb):
self._stream.close()
@property
def is_bold(self):
"""
|True| if this font is marked as a bold style of its font family.
"""
try:
return self._tables["head"].is_bold
except KeyError:
# some files don't have a head table
return False
@property
def is_italic(self):
"""
|True| if this font is marked as an italic style of its font family.
"""
try:
return self._tables["head"].is_italic
except KeyError:
# some files don't have a head table
return False
@classmethod
def open(cls, font_file_path):
"""
Return a |_Font| instance loaded from *font_file_path*.
"""
return cls(_Stream.open(font_file_path))
@property
def family_name(self):
"""
The name of the typeface family for this font, e.g. 'Arial'. The full
typeface name includes optional style names, such as 'Regular' or
'Bold Italic'. This attribute is only the common base name shared by
all fonts in the family.
"""
return self._tables["name"].family_name
@lazyproperty
def _fields(self):
"""
A 5-tuple containing the fields read from the font file header, also
known as the offset table.
"""
# sfnt_version, tbl_count, search_range, entry_selector, range_shift
return self._stream.read_fields(">4sHHHH", 0)
def _iter_table_records(self):
"""
Generate a (tag, offset, length) 3-tuple for each of the tables in
this font file.
"""
count = self._table_count
bufr = self._stream.read(offset=12, length=count * 16)
tmpl = ">4sLLL"
for i in range(count):
offset = i * 16
tag, checksum, off, len_ = unpack_from(tmpl, bufr, offset)
yield tag.decode("utf-8"), off, len_
@lazyproperty
def _tables(self):
"""
A mapping of OpenType table tag, e.g. 'name', to a table object
providing access to the contents of that table.
"""
return dict(
(tag, _TableFactory(tag, self._stream, off, len_))
for tag, off, len_ in self._iter_table_records()
)
@property
def _table_count(self):
"""
The number of tables in this OpenType font file.
"""
return self._fields[1]
class _Stream(object):
"""
A thin wrapper around a file that facilitates reading C-struct values
from a binary file.
"""
def __init__(self, file):
self._file = file
@classmethod
def open(cls, path):
"""
Return a |_Stream| providing binary access to the contents of the
file at *path*.
"""
return cls(open(path, "rb"))
def close(self):
"""
Close the wrapped file. Using the stream after closing raises an
exception.
"""
self._file.close()
def read(self, offset, length):
"""
Return *length* bytes from this stream starting at *offset*.
"""
self._file.seek(offset)
return self._file.read(length)
def read_fields(self, template, offset=0):
"""
Return a tuple containing the C-struct fields in this stream
specified by *template* and starting at *offset*.
"""
self._file.seek(offset)
bufr = self._file.read(calcsize(template))
return unpack_from(template, bufr)
class _BaseTable(object):
"""
Base class for OpenType font file table objects.
"""
def __init__(self, tag, stream, offset, length):
self._tag = tag
self._stream = stream
self._offset = offset
self._length = length
class _HeadTable(_BaseTable):
"""
OpenType font table having the tag 'head' and containing certain header
information for the font, including its bold and/or italic style.
"""
def __init__(self, tag, stream, offset, length):
super(_HeadTable, self).__init__(tag, stream, offset, length)
@property
def is_bold(self):
"""
|True| if this font is marked as having emboldened characters.
"""
return bool(self._macStyle & 1)
@property
def is_italic(self):
"""
|True| if this font is marked as having italicized characters.
"""
return bool(self._macStyle & 2)
@lazyproperty
def _fields(self):
"""
A 17-tuple containing the fields in this table.
"""
return self._stream.read_fields(">4s4sLLHHqqhhhhHHHHH", self._offset)
@property
def _macStyle(self):
"""
The unsigned short value of the 'macStyle' field in this head table.
"""
return self._fields[12]
class _NameTable(_BaseTable):
"""
An OpenType font table having the tag 'name' and containing the
name-related strings for the font.
"""
def __init__(self, tag, stream, offset, length):
super(_NameTable, self).__init__(tag, stream, offset, length)
@property
def family_name(self):
"""
The name of the typeface family for this font, e.g. 'Arial'.
"""
def find_first(dict_, keys, default=None):
for key in keys:
value = dict_.get(key)
if value is not None:
return value
return default
# keys for Unicode, Mac, and Windows family name, respectively
return find_first(self._names, ((0, 1), (1, 1), (3, 1)))
@staticmethod
def _decode_name(raw_name, platform_id, encoding_id):
"""
Return the unicode name decoded from *raw_name* using the encoding
implied by the combination of *platform_id* and *encoding_id*.
"""
if platform_id == 1:
# reject non-Roman Mac font names
if encoding_id != 0:
return None
return raw_name.decode("mac-roman")
elif platform_id in (0, 3):
return raw_name.decode("utf-16-be")
else:
return None
def _iter_names(self):
"""
Generate a key/value pair for each name in this table. The key is a
(platform_id, name_id) 2-tuple and the value is the unicode text
corresponding to that key.
"""
table_format, count, strings_offset = self._table_header
table_bytes = self._table_bytes
for idx in range(count):
platform_id, name_id, name = self._read_name(
table_bytes, idx, strings_offset
)
if name is None:
continue
yield ((platform_id, name_id), name)
@staticmethod
def _name_header(bufr, idx):
"""
The (platform_id, encoding_id, language_id, name_id, length,
name_str_offset) 6-tuple encoded in each name record C-struct.
"""
name_hdr_offset = 6 + idx * 12
return unpack_from(">HHHHHH", bufr, name_hdr_offset)
@staticmethod
def _raw_name_string(bufr, strings_offset, str_offset, length):
"""
Return the *length* bytes comprising the encoded string in *bufr* at
*str_offset* in the strings area beginning at *strings_offset*.
"""
offset = strings_offset + str_offset
tmpl = "%ds" % length
return unpack_from(tmpl, bufr, offset)[0]
def _read_name(self, bufr, idx, strings_offset):
"""
Return a (platform_id, name_id, name) 3-tuple like (0, 1, 'Arial')
for the name at *idx* position in *bufr*. *strings_offset* is the
index into *bufr* where actual name strings begin. The returned name
is a unicode string.
"""
platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header(
bufr, idx
)
name = self._read_name_text(
bufr, platform_id, enc_id, strings_offset, str_offset, length
)
return platform_id, name_id, name
def _read_name_text(
self, bufr, platform_id, encoding_id, strings_offset, name_str_offset, length
):
"""
Return the unicode name string at *name_str_offset* or |None| if
decoding its format is not supported.
"""
raw_name = self._raw_name_string(bufr, strings_offset, name_str_offset, length)
return self._decode_name(raw_name, platform_id, encoding_id)
@lazyproperty
def _table_bytes(self):
"""
The binary contents of this name table.
"""
return self._stream.read(self._offset, self._length)
@property
def _table_header(self):
"""
The (table_format, name_count, strings_offset) 3-tuple contained
in the header of this table.
"""
return unpack_from(">HHH", self._table_bytes)
@lazyproperty
def _names(self):
"""
A mapping of (platform_id, name_id) keys to string names for this
font.
"""
return dict(self._iter_names())
def _TableFactory(tag, stream, offset, length):
"""
Return an instance of |Table| appropriate to *tag*, loaded from
*font_file* with content of *length* starting at *offset*.
"""
TableClass = {"head": _HeadTable, "name": _NameTable}.get(tag, _BaseTable)
return TableClass(tag, stream, offset, length)