Fixed issue with reading/writing PrivateDict BlueValues to ttx file. To date, BlueValue arguments have been written as absolute coordinate values, reflecting the history of the CFF ttx table format. However, Behdad Esfahbod pointed out that it is not always possible to roundtrip between the absolute values in the ttx file and the CFF2 default font value and the delta list. This update changes the ttx format so that in PrivateDict BlueValues, the default font values are written as absolute coordinates, preserving continuity with how CFF tables are written, but the region values are written as the deltas from the VariationStore delta list.

This also fixes fonttools/fonttools/issues/1030.

Although the roundtrip is generally possible when a VariationStore is built from source font data using the Superpolator model, it is possible to  build  region definitions that do not follow this model. Behdad cited the Skia "Q" example, where the tail of the Q is affected by two regions defined as:
min=0 peak=0.5 max=0.51 delta=+10
min=0.49 peak=0.5 max=0.51 delta=-10
This commit is contained in:
ReadRoberts 2017-11-02 11:31:47 -07:00
parent 045287aa25
commit 8b02b5a294
2 changed files with 58 additions and 49 deletions

View File

@ -1972,12 +1972,7 @@ class DictCompiler(object):
def arg_number(self, num):
if isinstance(num, list):
blendList = num
firstNum = blendList[0]
data = [firstNum]
for blendNum in blendList[1:]:
data.append(blendNum - firstNum)
data = [encodeNumber(val) for val in data]
data = [encodeNumber(val) for val in num]
data.append(encodeNumber(1))
data.append(bytechr(blendOp))
datum = bytesjoin(data)
@ -2011,15 +2006,33 @@ class DictCompiler(object):
data.append(encodeNumber(num))
return bytesjoin(data)
def arg_delta_blend(self, value):
# A delta list with blend lists has to be *all* blend lists.
# We have a list of master value lists, where the nth master value list
# contains the absolute values from each master for the nth entry in the
# current array.
# We first convert these to relative values from the previous entry.
""" A delta list with blend lists has to be *all* blend lists.
The value is a list is arranged as follows.
[
[V0, d0..dn]
[V1, d0..dn]
...
[Vm, d0..dn]
]
V is the absolute coordinate value from the default font, and d0-dn are
the delta values from the n regions. Each V is an absolute coordinate
from the default font.
We want to return a list:
[
[v0, v1..vm]
[d0..dn]
...
[d0..dn]
numBlends
blendOp
]
where each v is relative to the previous default font value.
"""
numMasters = len(value[0])
numValues = len(value)
numStack = (numValues * numMasters) + 1
numBlends = len(value)
numStack = (numBlends * numMasters) + 1
if numStack > self.maxBlendStack:
# Figure out the max number of value we can blend
# and divide this list up into chunks of that size.
@ -2035,28 +2048,26 @@ class DictCompiler(object):
out.extend(out1)
value = value[numVal:]
else:
firstList = [0] * numValues
deltaList = [None] * (numValues)
firstList = [0] * numBlends
deltaList = [None] * numBlends
i = 0
prevValList = numMasters * [0]
while i < numValues:
masterValList = value[i]
firstVal = firstList[i] = masterValList[0] - prevValList[0]
j = 1
deltaEntry = (numMasters - 1) * [0]
while j < numMasters:
masterValDelta = masterValList[j] - prevValList[j]
deltaEntry[j - 1] = masterValDelta - firstVal
j += 1
deltaList[i] = deltaEntry
prevVal = 0
while i < numBlends:
# For PrivateDict BlueValues, the default font
# values are absolute, not relative.
# Must convert these back to relative coordinates
# befor writing to CFF2.
defaultValue = value[i][0]
firstList[i] = defaultValue - prevVal
prevVal = defaultValue
deltaList[i] = value[i][1:]
i += 1
prevValList = masterValList
relValueList = firstList
for blendList in deltaList:
relValueList.extend(blendList)
out = [encodeNumber(val) for val in relValueList]
out.append(encodeNumber(numValues))
out.append(encodeNumber(numBlends))
out.append(bytechr(blendOp))
return out

View File

@ -1246,36 +1246,34 @@ class DictDecompiler(object):
def arg_array(self, name):
return self.popall()
def arg_blendList(self, name):
# The last item on the stack is the number of return values, aka numValues.
# before that we have [numValues: args from first master]
# then numValues blend lists, where each blend list is numMasters -1
# Total number of values is numValues + (numValues * (numMasters -1)), == numValues * numMasters.
# reformat list to be numReturnValues tuples, each tuple with nMaster values
"""
There may be non-blend args at the top of the stack. We first calculate
where the blend args start in the stack. These are the last
numMasters*numBlends) +1 args.
The blend args starts with numMasters relative coordinate values, the BlueValues in the list from the default master font. This is followed by
numBlends list of values. Each of value in one of these lists is the
Variable Font delta for the matching region.
We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by
the delta values. We then convert the default values, the first item in each entry, to an absolute value.
"""
vsindex = self.dict.get('vsindex', 0)
numMasters = self.parent.getNumRegions(vsindex) + 1 # only a PrivateDict has blended ops.
numReturnValues = self.pop()
stackIndex = -numMasters * numReturnValues
args = self.stack[stackIndex:]
del self.stack[stackIndex:]
numBlends = self.pop()
args = self.popall()
numArgs = len(args)
value = [None]*numReturnValues
# The spec says that there should be no non-blended Blue Values,.
assert(numArgs == numMasters * numBlends)
value = [None]*numBlends
numDeltas = numMasters-1
i = 0
prevVal = 0
prevValueList = [0]*numMasters
while i < numReturnValues:
while i < numBlends:
newVal = args[i] + prevVal
blendList = [newVal]*numMasters
prevVal = newVal
masterOffset = numBlends + (i* numDeltas)
blendList = [newVal] + args[masterOffset:masterOffset+numDeltas]
value[i] = blendList
j = 1
while j < numMasters:
masterOffset = numReturnValues + (i* numDeltas)
mi = masterOffset +(j-1)
delta = args[i] + args[mi]
blendList[j]= delta + prevValueList[j]
j += 1
prevValueList = blendList
i += 1
return value