Skip to content

Commit

Permalink
font-patcher: Handle lowestRecPPEM
Browse files Browse the repository at this point in the history
[why]
Some fonts set specific lower resolutions and handle the small rendering
very nicely. But fontforge does not copy that information when it opens
a font; rather some default values are always written. That can destroy
the small font rendering.

The responsible settings are the 'ppem_to_int' flag and the
'lowestRecPPEM' setting, both in the HEAD table.

[how]
After successfully patching and generating the new font we copy that
settings over from the source font.

Instead of using fontTools (and thereby requiring all users to pull that
dependency) we handle the raw font file changes ourselves.

Fixes: 612

Reported-by: nojus297
Signed-off-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
  • Loading branch information
Finii committed Aug 20, 2022
1 parent 85324cc commit 40138be
Showing 1 changed file with 126 additions and 3 deletions.
129 changes: 126 additions & 3 deletions font-patcher
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,113 @@ except ImportError:
)


class TableHEADWriter:
""" Access to the HEAD table without external dependencies """
def getlong(self, pos = None):
""" Get four bytes from the font file as integer number """
if pos:
self.goto(pos)
return (ord(self.f.read(1)) << 24) + (ord(self.f.read(1)) << 16) + (ord(self.f.read(1)) << 8) + ord(self.f.read(1))

def getshort(self, pos = None):
""" Get two bytes from the font file as integer number """
if pos:
self.goto(pos)
return (ord(self.f.read(1)) << 8) + ord(self.f.read(1))

def putlong(self, num, pos = None):
""" Put number as four bytes into font file """
if pos:
self.goto(pos)
self.f.write(bytearray([(num >> 24) & 0xFF, (num >> 16) & 0xFF ,(num >> 8) & 0xFF, num & 0xFF]))
self.modified = True

def putshort(self, num, pos = None):
""" Put number as two bytes into font file """
if pos:
self.goto(pos)
self.f.write(bytearray([(num >> 8) & 0xFF, num & 0xFF]))
self.modified = True

def calc_checksum(self, start, end, checksum = 0):
""" Calculate a font table checksum, optionally ignoring another embedded checksum value (for table 'head') """
self.f.seek(start)
for i in range(start, end - 4, 4):
checksum += self.getlong()
checksum &= 0xFFFFFFFF
i += 4
extra = 0
for j in range(4):
if i + j <= end:
extra += ord(self.f.read(1))
extra = extra << 8
checksum = (checksum + extra) & 0xFFFFFFFF
return checksum

def find_head_table(self):
""" Search all tables for the HEAD table and store its metadata """
self.f.seek(4)
numtables = self.getshort()
self.f.seek(3*2, 1)

for i in range(numtables):
tab_name = self.f.read(4)
self.tab_check_offset = self.f.tell()
self.tab_check = self.getlong()
self.tab_offset = self.getlong()
self.tab_length = self.getlong()
if tab_name == b'head':
break

def goto(self, where):
""" Go to a named location in the file or to the specified index """
if type(where) is str:
positions = {'checksumAdjustment': 2+2+4,
'flags': 2+2+4+4+4,
'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2,
}
where = self.tab_offset + positions[where]
self.f.seek(where)


def calc_full_checksum(self, check = False):
""" Calculate the whole file's checksum """
self.f.seek(0, 2)
self.end = self.f.tell()
full_check = self.calc_checksum(0, self.end, (-self.checksum_adj) & 0xFFFFFFFF)
if check and (0xB1B0AFBA - full_check) & 0xFFFFFFFF != self.checksum_adj:
sys.exit("Checksum of whole font is bad")
return full_check

def calc_table_checksum(self, check = False):
tab_check_new = self.calc_checksum(self.tab_offset, self.tab_offset + self.tab_length - 1, (-self.checksum_adj) & 0xFFFFFFFF)
if check and tab_check_new != self.tab_check:
sys.exit("Checksum of 'head' in font is bad")
return tab_check_new

def reset_table_checksum(self):
new_check = self.calc_table_checksum()
self.putlong(new_check, self.tab_check_offset)

def reset_full_checksum(self):
new_adj = (0xB1B0AFBA - self.calc_full_checksum()) & 0xFFFFFFFF
self.putlong(new_adj, 'checksumAdjustment')

def close(self):
self.f.close()


def __init__(self, filename):
self.modified = False
self.f = open(filename, 'r+b')

self.find_head_table()

self.flags = self.getshort('flags')
self.lowppem = self.getshort('lowestRecPPEM')
self.checksum_adj = self.getlong('checksumAdjustment')


class font_patcher:
def __init__(self):
self.args = None # class 'argparse.Namespace'
Expand Down Expand Up @@ -137,11 +244,27 @@ class font_patcher:
if self.sourceFont.fullname != None:
outfile = self.args.outputdir + "/" + self.sourceFont.fullname + self.extension
self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments')))
print("\nGenerated: {} in {}".format(self.sourceFont.fontname, outfile))
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fontname, outfile)
else:
outfile = self.sourceFont.generate(self.args.outputdir + "/" + self.sourceFont.cidfontname + self.extension
outfile = self.args.outputdir + "/" + self.sourceFont.cidfontname + self.extension
self.sourceFont.generate(outfile, flags=(str('opentype'), str('PfEd-comments')))
print("\nGenerated: {} in {}".format(self.sourceFont.fullname, outfile))
message = "\nGenerated: {} in '{}'".format(self.sourceFont.fullname, outfile)

# Adjust flags that can not be changed via fontforge
source_font = TableHEADWriter(self.args.font)
source_font.close()
dest_font = TableHEADWriter(outfile)
if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0:
print("Changing flags from 0x{:X} to 0x{:X}".format(dest_font.flags, dest_font.flags & ~0x08))
dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int'
if source_font.lowppem != dest_font.lowppem:
print("Changing lowestRecPPEM from {} to {}".format(dest_font.lowppem, source_font.lowppem))
dest_font.putshort(source_font.lowppem, 'lowestRecPPEM')
if dest_font.modified:
dest_font.reset_table_checksum()
dest_font.reset_full_checksum()
dest_font.close()
print(message)

if self.args.postprocess:
subprocess.call([self.args.postprocess, outfile])
Expand Down

0 comments on commit 40138be

Please sign in to comment.