Skip to content
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

Combine nested components in TTF-Fonts #1506

Closed
artcs opened this issue Feb 13, 2019 · 9 comments
Closed

Combine nested components in TTF-Fonts #1506

artcs opened this issue Feb 13, 2019 · 9 comments

Comments

@artcs
Copy link

artcs commented Feb 13, 2019

Older MacOS versions (10.6.8) have a problem with nested components in TTF-Fonts. E.g. Fira Fonts have some accents misplaced, cause they are built from nested components (see this issue).
This was fixed in Fira 4.1.6 but returned in later versions. I currently work around this issue (I still have clients using 10.6) by fixing this manually in the ttx-output (e.g. replacing uni0308.case by uni0308 and adjusting offsets). Could this be automated via a fonttools snippet or maybe a pyftsubset option? Any hints welcome.

@artcs
Copy link
Author

artcs commented Feb 16, 2019

So this is how far I got after some hours of trial and error:

font = TTFont('fontfile.ttf')
glyphTable = font["glyf"]
glyphNames = font.getGlyphNames()
for glyphname in glyphNames:
	glyph = glyphTable[glyphname]
	if glyph.isComposite():
		for compo in glyph.components:
			cname = compo.glyphName
			# only needed if component is moved
			if compo.x == 0 and compo.y == 0:
				continue
			cglyph = glyphTable[cname]
			if cglyph.isComposite() and len(cglyph.components) == 1:
				# replace with nested component and adjust x/y position
				compo.glyphName = cglyph.components[0].glyphName
				compo.x += cglyph.components[0].x
				compo.y += cglyph.components[0].y
				compo.flags = cglyph.components[0].flags

glyphTable.compile(font)
font.save('fontfile-fixed.ttf')

This works fine so far, the accents appear at the correct place on MacOS 10.6. However I don't know if it's correct to just copy the compo.flags attribute from the nested component and moreover if and how the instructions present in the components should be handled. E.g. Adieresis is originally composed with a uni0308.case component, which is itself composed of a uni0308 component, both of them including instructions:

    <TTGlyph name="Adieresis" xMin="8" yMin="0" xMax="563" yMax="888">
      <component glyphName="A" x="0" y="0" flags="0x0"/>
      <component glyphName="uni0308.case" x="471" y="0" flags="0x0"/>
    </TTGlyph>
    
    <TTGlyph name="uni0308.case" xMin="-342" yMin="782" xMax="-30" yMax="888">
      <component glyphName="uni0308" x="0" y="126" flags="0x4"/>
      <instructions>
        <assembly>
          PUSHB[ ]	/* 2 values pushed */
          0 2
          PUSHB[ ]	/* 1 value pushed */
          126
          PUSHB[ ]	/* 1 value pushed */
          53
          CALL[ ]	/* CallFunction */
        </assembly>
      </instructions>
    </TTGlyph>
    
    <TTGlyph name="uni0308" xMin="-342" yMin="656" xMax="-30" yMax="762">
      <contour>
        ...
      </contour>
      <contour>
        ...
      </contour>
      <instructions>
        <assembly>
          PUSHB[ ]	/* 2 values pushed */
          6 100
          WCVTP[ ]	/* WriteCVTInPixels */
          NPUSHB[ ]	/* 41 values pushed */
          5 3 4 3 1 0 0 1 89 5 3 4 3 1 1 0 97 2 1 0 1 0 81 12 12
          0 0 12 23 12 22 18 16 0 11 0 10 36 6 11 23
          CALL[ ]	/* CallFunction */
          PUSHB[ ]	/* 2 values pushed */
          6 0
          WCVTP[ ]	/* WriteCVTInPixels */
        </assembly>
      </instructions>
    </TTGlyph>

Do I have to care about the instructions? What would be the correct way to handle this? Could anyone with more insights in the TT Spec and fonttools point me to the right direction? Thanks in advance for any hints.

@artcs
Copy link
Author

artcs commented Feb 22, 2019

So after some more hours of trial and error, here is my final script, which deals with components consisting of more than one nested component like tildemacroncomb.case. Feel free to include it into Snippets if you think somone could find it useful.
The question of how to deal with instructions in the original component remains however.
Closing here due to lack of response.

#! /usr/bin/env python
"""Script to combine multi-level components with component offsets. While this is allowed by the spec, fonts using this (e.g. Fira Sans) hit a bug in pre-10.9 Apple font engines, so this script makes them work on older MacOS versions."""

from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent


def processComponent(glyphTable, glyph, compo, dx, dy):
	changed = False
	cname = compo.glyphName
	cglyph = glyphTable[cname]
	if cglyph.isComposite():
		# combine all nested components recursively
		for i in range(len(cglyph.components)):
			g = glyphTable[cglyph.components[i].glyphName]
			if g.isComposite():
				if processComponent(glyphTable, glyph, cglyph.components[i], cglyph.components[i].x + dx, cglyph.components[i].y + dy):
					changed = True
			else:
				glyph.components.append(GlyphComponent())
				nc = glyph.components[-1]
				nc.glyphName = cglyph.components[i].glyphName
				nc.x = cglyph.components[i].x + dx
				nc.y = cglyph.components[i].y + dy
				nc.flags = cglyph.components[i].flags
				changed = True
	return changed


def combineNestedComponents(font):
	if not 'glyf' in font:
		return False
	changed = False
	glyphTable = font["glyf"]
	glyphNames = font.getGlyphNames()
	# loop over all glyphs
	for glyphname in glyphNames:
		glyph = glyphTable[glyphname]
		if glyph.isComposite():
			for compo in glyph.components:
				dx = compo.x
				dy = compo.y
				# if component is not shifted, everything works fine
				if dx == 0 and dy == 0:
					continue
				if processComponent(glyphTable, glyph, compo, dx, dy):
					# delete original component if combined
					glyph.components.remove(compo)
					changed = True
	
	glyphTable.compile(font)
	return changed


def usage():
	prog = os.path.basename(sys.argv[0])
	print("usage: ", prog, " font.ttf [...]")
	sys.exit(1)


def main():
	ttfonts = []
	if len(sys.argv) > 1:
		for arg in sys.argv[1:]:
			base, ext = os.path.splitext(arg)
			if ext == '.ttf':
				ttfonts.append(arg)
			else:
				print("Not ttf: " + arg)
				usage()
	else:
		usage()
	
	for filename in ttfonts:
		base, ext = os.path.splitext(filename)
		font = TTFont(filename)
		if combineNestedComponents(font):
			font.save(base + "-fixed" + ext)
		else:
			print("nothing to do for " + filename)


if __name__ == "__main__":
	import sys, os
	sys.exit(main())

@artcs artcs closed this as completed Feb 22, 2019
@hyvyys
Copy link

hyvyys commented Dec 13, 2020

Does this have a chance to work on variable fonts? Returned "nothing to do" for mine.

@artcs
Copy link
Author

artcs commented Dec 14, 2020

This was a fix to make static TTFs with nested components work on MacOS 10.6 to 10.8. These Systems don't support variable fonts, so you don't need it.

@hyvyys
Copy link

hyvyys commented Dec 14, 2020 via email

@justvanrossum
Copy link
Collaborator

Looking at the script, the output you're getting seems to say your font does not contain any nested components to begin with. That message should be as valid for a VF as for a static font. That said, the script probably doesn't do the right thing for a VF if it does contain nested components.

@hyvyys
Copy link

hyvyys commented Dec 14, 2020

Thanks for taking the time trying to help me @justvanrossum and @artcs!

I ran the script for the VF writing to the same file and I must've ran it twice which is why it told me "nothing to do". It is able to flatten the components for VFs but doesn't handle marks above the flattened components well (they lose their alignment with variation I think).

Luckily there's a solution printed right there in fontbakery: fonttools/fontbakery#2961
I missed it because I ran fontbakery with the --succinct parameter 🤦 And yes, GF requires VFs without nested components too.

In case anyone bumps into this again: I'm adding the ufo2ft filter like this:

#fixMaster.py
import sys
from plistlib import load, dump

path = sys.argv[-1]

def fixLib(path):
    with open(path, 'rb') as fp:
        pl = load(fp)
    pl['com.github.googlei18n.ufo2ft.filters'] = [{ 'name': 'flattenComponents', 'pre': 1 }]
    with open(path, 'wb') as fp:
        dump(pl, fp)

fixLib(path + 'lib.plist')

ran like this python3 fixMaster.py "./UFO/masters/FontFamily.ufo/"

@anthrotype
Copy link
Member

anthrotype commented Dec 14, 2020

@hyvyys FYI, Simon added a --flatten-components option to fontmake's command line

https://github.com/googlefonts/fontmake/releases/tag/v2.3.0

@artcs
Copy link
Author

artcs commented Dec 14, 2020

The original script does only combine components, that have an offset defined, so it does not flatten all the components. For that you should better use fontmake as suggested by anthrotype above or use the decompose-ttf snippet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants