-
Notifications
You must be signed in to change notification settings - Fork 45
Extract a better property dict from TTF fonts #697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,14 +58,16 @@ class FontEntry(object): | |
| """ A class for storing Font properties. It is used when populating | ||
| the font lookup dictionary. | ||
| """ | ||
| def __init__(self, fname="", name="", style="normal", variant="normal", | ||
| weight="normal", stretch="normal", size="medium"): | ||
| def __init__(self, fname="", family="", style="normal", variant="normal", | ||
| weight="normal", stretch="normal", size="medium", | ||
| face_index=0): | ||
| self.fname = fname | ||
| self.name = name | ||
| self.family = family | ||
| self.style = style | ||
| self.variant = variant | ||
| self.weight = weight | ||
| self.stretch = stretch | ||
| self.face_index = face_index | ||
| try: | ||
| self.size = str(float(size)) | ||
| except ValueError: | ||
|
|
@@ -74,8 +76,8 @@ def __init__(self, fname="", name="", style="normal", variant="normal", | |
| def __repr__(self): | ||
| fname = os.path.basename(self.fname) | ||
| return ( | ||
| f"<FontEntry '{self.name}' ({fname}) {self.style} {self.variant} " | ||
| f"{self.weight} {self.stretch}>" | ||
| f"<FontEntry '{self.family}' ({fname}[{self.face_index}]) " | ||
| f"{self.style} {self.variant} {self.weight} {self.stretch}>" | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -120,8 +122,8 @@ def _build_ttf_entries(fpath): | |
| if ext.lower() == ".ttc": | ||
| collection = TTCollection(fp) | ||
| try: | ||
| for font in collection.fonts: | ||
| entries.append(_ttf_font_property(fpath, font)) | ||
| for idx, font in enumerate(collection.fonts): | ||
| entries.append(_ttf_font_property(fpath, font, idx)) | ||
| except Exception: | ||
| logger.error(_FONT_ENTRY_ERR_MSG, fpath, exc_info=True) | ||
| else: | ||
|
|
@@ -144,22 +146,22 @@ def _afm_font_property(fontpath, font): | |
|
|
||
| *font* is a class:`AFM` instance. | ||
| """ | ||
| name = font.get_familyname() | ||
| family = font.get_familyname() | ||
| fontname = font.get_fontname().lower() | ||
|
|
||
| # Styles are: italic, oblique, and normal (default) | ||
| if font.get_angle() != 0 or name.lower().find("italic") >= 0: | ||
| if font.get_angle() != 0 or family.lower().find("italic") >= 0: | ||
| style = "italic" | ||
| elif name.lower().find("oblique") >= 0: | ||
| elif family.lower().find("oblique") >= 0: | ||
| style = "oblique" | ||
| else: | ||
| style = "normal" | ||
|
|
||
| # Variants are: small-caps and normal (default) | ||
| # NOTE: Not sure how many fonts actually have these strings in their name | ||
| # NOTE: Not sure how many fonts actually have these strings in their family | ||
| variant = "normal" | ||
| for value in ("capitals", "small-caps"): | ||
| if value in name.lower(): | ||
| if value in family.lower(): | ||
| variant = "small-caps" | ||
| break | ||
|
|
||
|
|
@@ -194,34 +196,40 @@ def _afm_font_property(fontpath, font): | |
|
|
||
| # All AFM fonts are apparently scalable. | ||
| size = "scalable" | ||
| return FontEntry(fontpath, name, style, variant, weight, stretch, size) | ||
| return FontEntry(fontpath, family, style, variant, weight, stretch, size) | ||
|
|
||
|
|
||
| def _ttf_font_property(fpath, font): | ||
| def _ttf_font_property(fpath, font, face_index=0): | ||
| """ A function for populating the :class:`FontEntry` by extracting | ||
| information from the TrueType font file. | ||
|
|
||
| *font* is a :class:`TTFont` instance. | ||
| """ | ||
| props = get_ttf_prop_dict(font) | ||
| name = props.get("name") | ||
| if name is None: | ||
| raise KeyError("No name could be found for: {}".format(fpath)) | ||
| family = props.get("family") | ||
| if family is None: | ||
| raise KeyError("No family could be found for: {}".format(fpath)) | ||
|
|
||
| # Some properties | ||
| full_name = props.get("full_name", "").lower() | ||
| style_prop = props.get("style", "").lower() | ||
| if style_prop == "": | ||
| # For backwards compatibility with previous parsing behavior | ||
| style_prop = full_name | ||
|
Comment on lines
+217
to
+218
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We used to use the |
||
|
|
||
| # Styles are: italic, oblique, and normal (default) | ||
| sfnt4 = props.get("sfnt4", "").lower() | ||
| if sfnt4.find("oblique") >= 0: | ||
| if style_prop.find("oblique") >= 0: | ||
| style = "oblique" | ||
| elif sfnt4.find("italic") >= 0: | ||
| elif style_prop.find("italic") >= 0: | ||
| style = "italic" | ||
| else: | ||
| style = "normal" | ||
|
|
||
| # Variants are: small-caps and normal (default) | ||
| # NOTE: Not sure how many fonts actually have these strings in their name | ||
| # NOTE: Not sure how many fonts actually have these strings in their family | ||
| variant = "normal" | ||
| for value in ("capitals", "small-caps"): | ||
| if value in name.lower(): | ||
| for value in ("capitals", "small-caps", "smallcaps"): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made this addition because there's a font named "Bodoni 72 Smallcaps Book" on macOS. |
||
| if value in family.lower(): | ||
| variant = "small-caps" | ||
| break | ||
|
|
||
|
|
@@ -230,7 +238,7 @@ def _ttf_font_property(fpath, font): | |
| # lighter and bolder are also allowed. | ||
| weight = None | ||
| for w in weight_dict.keys(): | ||
| if sfnt4.find(w) >= 0: | ||
| if style_prop.find(w) >= 0: | ||
| weight = w | ||
| break | ||
| if not weight: | ||
|
|
@@ -243,13 +251,13 @@ def _ttf_font_property(fpath, font): | |
| # and ultra-expanded. | ||
| # Relative stretches are: wider, narrower | ||
| # Child value is: inherit | ||
| if sfnt4.find("demi cond") >= 0: | ||
| if full_name.find("demi cond") >= 0: | ||
| stretch = "semi-condensed" | ||
| elif (sfnt4.find("narrow") >= 0 | ||
| or sfnt4.find("condensed") >= 0 | ||
| or sfnt4.find("cond") >= 0): | ||
| elif (full_name.find("narrow") >= 0 | ||
| or full_name.find("condensed") >= 0 | ||
| or full_name.find("cond") >= 0): | ||
| stretch = "condensed" | ||
| elif sfnt4.find("wide") >= 0 or sfnt4.find("expanded") >= 0: | ||
| elif full_name.find("wide") >= 0 or full_name.find("expanded") >= 0: | ||
| stretch = "expanded" | ||
| else: | ||
| stretch = "normal" | ||
|
|
@@ -263,4 +271,13 @@ def _ttf_font_property(fpath, font): | |
|
|
||
| # !!!! Incomplete | ||
| size = "scalable" | ||
| return FontEntry(fpath, name, style, variant, weight, stretch, size) | ||
| return FontEntry( | ||
| fname=fpath, | ||
| family=family, | ||
| style=style, | ||
| variant=variant, | ||
| weight=weight, | ||
| stretch=stretch, | ||
| size=size, | ||
| face_index=face_index, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,23 +9,67 @@ | |
| # Thanks for using Enthought open source! | ||
| from kiva.fonttools._constants import weight_dict | ||
|
|
||
| # Unicode & Apple | ||
| _plat_ids = (0, 1) | ||
| _english_id = 0 | ||
| # MS | ||
| # https://docs.microsoft.com/en-us/typography/opentype/spec/name#windows-language-ids # noqa: E501 | ||
| _ms_plat_id = 3 | ||
| _ms_english_ids = { | ||
| 0x0C09: "Australia", | ||
| 0x2809: "Belize", | ||
| 0x1009: "Canada", | ||
| 0x2409: "Caribbean", | ||
| 0x4009: "India", | ||
| 0x1809: "Ireland", | ||
| 0x2009: "Jamaica", | ||
| 0x4409: "Malaysia", | ||
| 0x1409: "New Zealand", | ||
| 0x3409: "Republic of the Philippines", | ||
| 0x4809: "Singapore", | ||
| 0x1C09: "South Africa", | ||
| 0x2C09: "Trinidad and Tobago", | ||
| 0x0809: "United Kingdom", | ||
| 0x0409: "United States", | ||
| 0x3009: "Zimbabwe", | ||
| } | ||
| # TrueType 'name' table IDs | ||
| # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html # noqa: E501 | ||
| _name_ids = { | ||
| 0: "copyright", | ||
| 1: "family", | ||
| 2: "style", | ||
| 3: "unique_subfamily_id", | ||
| 4: "full_name", | ||
| 5: "version", | ||
| 6: "postscript_name", | ||
| } | ||
|
|
||
|
|
||
| def get_ttf_prop_dict(font): | ||
| """ Return the property dictionary from a :class:`TTFont` instance. | ||
| """ Parse the 'name' table of a :class:`TTFont` instance. | ||
| """ | ||
| n = font["name"] | ||
| propdict = {} | ||
| for prop in n.names: | ||
| try: | ||
| if "name" in propdict and "sfnt4" in propdict: | ||
| break | ||
| elif prop.nameID == 1 and "name" not in propdict: | ||
| propdict["name"] = _decode_prop(prop.string) | ||
| elif prop.nameID == 4 and "sfnt4" not in propdict: | ||
| propdict["sfnt4"] = _decode_prop(prop.string) | ||
| except UnicodeDecodeError: | ||
| table = font["name"] | ||
| for rec in table.names: | ||
| # We only care about records in English | ||
| plat, lang = rec.platformID, rec.langID | ||
| if not ((plat in _plat_ids and lang == _english_id) | ||
| or (plat == _ms_plat_id and lang in _ms_english_ids)): | ||
| continue | ||
|
Comment on lines
+55
to
+59
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need this check to pick up older TTF/TTC files and newer OTF files (which use the Microsoft |
||
| # And of those, just the ones we have names for | ||
| if rec.nameID not in _name_ids: | ||
| continue | ||
|
|
||
| # Convert the nameID to a nice string | ||
| key = _name_ids[rec.nameID] | ||
| # Skip duplicate records | ||
| if key in propdict: | ||
| continue | ||
|
|
||
| # Use the NameRecord's toStr() method instead of ad-hoc decoding | ||
| propdict[key] = rec.toStr() | ||
|
|
||
| return propdict | ||
|
|
||
|
|
||
|
|
@@ -45,19 +89,3 @@ def weight_as_number(weight): | |
| else: | ||
| raise ValueError("weight not a valid integer") | ||
| return weight | ||
|
|
||
|
|
||
| def _decode_prop(prop): | ||
| """ Decode a prop string. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| prop : bytestring | ||
|
|
||
| Returns | ||
| ------- | ||
| string | ||
| """ | ||
| # Adapted from: https://gist.github.com/pklaus/dce37521579513c574d0 | ||
| encoding = "utf-16-be" if b"\x00" in prop else "utf-8" | ||
| return prop.decode(encoding) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is integrating the
face_indexpieces that we're introduced by #605 to the refactored font parsing code.