1""" 2This file provides fallback data for info attributes 3that are required for building OTFs. There are two main 4functions that are important: 5 6* :func:`~getAttrWithFallback` 7* :func:`~preflightInfo` 8 9There are a set of other functions that are used internally 10for synthesizing values for specific attributes. These can be 11used externally as well. 12""" 13 14from __future__ import print_function, division, absolute_import, unicode_literals 15 16import logging 17import math 18from datetime import datetime 19import calendar 20import time 21import unicodedata 22import os 23 24from fontTools.misc.py23 import tobytes, tostr, tounicode, unichr 25from fontTools.misc.fixedTools import otRound 26from fontTools.misc.textTools import binary2num 27from fontTools import ufoLib 28 29logger = logging.getLogger(__name__) 30 31 32# ----------------- 33# Special Fallbacks 34# ----------------- 35 36# generic 37 38def styleMapFamilyNameFallback(info): 39 """ 40 Fallback to *openTypeNamePreferredFamilyName openTypeNamePreferredSubfamilyName*. 41 """ 42 familyName = getAttrWithFallback(info, "openTypeNamePreferredFamilyName") 43 styleName = getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName") 44 if styleName is None: 45 styleName = "" 46 return (familyName + " " + styleName).strip() 47 48# head 49 50_date_format = "%Y/%m/%d %H:%M:%S" 51 52def dateStringForNow(): 53 return time.strftime(_date_format, time.gmtime()) 54 55def openTypeHeadCreatedFallback(info): 56 """ 57 Fallback to the environment variable SOURCE_DATE_EPOCH if set, otherwise 58 now. 59 """ 60 if "SOURCE_DATE_EPOCH" in os.environ: 61 t = datetime.utcfromtimestamp(int(os.environ["SOURCE_DATE_EPOCH"])) 62 return t.strftime(_date_format) 63 else: 64 return dateStringForNow() 65 66# hhea 67 68def openTypeHheaAscenderFallback(info): 69 """ 70 Fallback to *ascender + typoLineGap*. 71 """ 72 return info.ascender + getAttrWithFallback(info, "openTypeOS2TypoLineGap") 73 74def openTypeHheaDescenderFallback(info): 75 """ 76 Fallback to *descender*. 77 """ 78 return info.descender 79 80def openTypeHheaCaretSlopeRiseFallback(info): 81 """ 82 Fallback to *openTypeHheaCaretSlopeRise*. If the italicAngle is zero, 83 return 1. If italicAngle is non-zero, compute the slope rise from the 84 complementary openTypeHheaCaretSlopeRun, if the latter is defined. 85 Else, default to an arbitrary fixed reference point (1000). 86 """ 87 italicAngle = getAttrWithFallback(info, "italicAngle") 88 if italicAngle != 0: 89 if (hasattr(info, "openTypeHheaCaretSlopeRun") and 90 info.openTypeHheaCaretSlopeRun is not None): 91 slopeRun = info.openTypeHheaCaretSlopeRun 92 return otRound(slopeRun / math.tan(math.radians(-italicAngle))) 93 else: 94 return 1000 # just an arbitrary non-zero reference point 95 return 1 96 97def openTypeHheaCaretSlopeRunFallback(info): 98 """ 99 Fallback to *openTypeHheaCaretSlopeRun*. If the italicAngle is zero, 100 return 0. If italicAngle is non-zero, compute the slope run from the 101 complementary openTypeHheaCaretSlopeRise. 102 """ 103 italicAngle = getAttrWithFallback(info, "italicAngle") 104 if italicAngle != 0: 105 slopeRise = getAttrWithFallback(info, "openTypeHheaCaretSlopeRise") 106 return otRound(math.tan(math.radians(-italicAngle)) * slopeRise) 107 return 0 108 109# name 110 111def openTypeNameVersionFallback(info): 112 """ 113 Fallback to *versionMajor.versionMinor* in the form 0.000. 114 """ 115 versionMajor = getAttrWithFallback(info, "versionMajor") 116 versionMinor = getAttrWithFallback(info, "versionMinor") 117 return "Version %d.%s" % (versionMajor, str(versionMinor).zfill(3)) 118 119def openTypeNameUniqueIDFallback(info): 120 """ 121 Fallback to *openTypeNameVersion;openTypeOS2VendorID;postscriptFontName*. 122 """ 123 version = getAttrWithFallback(info, "openTypeNameVersion").replace("Version ", "") 124 vendor = getAttrWithFallback(info, "openTypeOS2VendorID") 125 fontName = getAttrWithFallback(info, "postscriptFontName") 126 return "%s;%s;%s" % (version, vendor, fontName) 127 128def openTypeNamePreferredFamilyNameFallback(info): 129 """ 130 Fallback to *familyName*. 131 """ 132 return info.familyName 133 134def openTypeNamePreferredSubfamilyNameFallback(info): 135 """ 136 Fallback to *styleName*. 137 """ 138 return info.styleName 139 140def openTypeNameCompatibleFullNameFallback(info): 141 """ 142 Fallback to *styleMapFamilyName styleMapStyleName*. 143 If *styleMapStyleName* is *regular* this will not add 144 the style name. 145 """ 146 familyName = getAttrWithFallback(info, "styleMapFamilyName") 147 styleMapStyleName = getAttrWithFallback(info, "styleMapStyleName") 148 if styleMapStyleName != "regular": 149 familyName += " " + styleMapStyleName.title() 150 return familyName 151 152def openTypeNameWWSFamilyNameFallback(info): 153 # not yet supported 154 return None 155 156def openTypeNameWWSSubfamilyNameFallback(info): 157 # not yet supported 158 return None 159 160# OS/2 161 162def openTypeOS2TypoAscenderFallback(info): 163 """ 164 Fallback to *ascender*. 165 """ 166 return info.ascender 167 168def openTypeOS2TypoDescenderFallback(info): 169 """ 170 Fallback to *descender*. 171 """ 172 return info.descender 173 174def openTypeOS2TypoLineGapFallback(info): 175 """ 176 Fallback to *UPM * 1.2 - ascender + descender*, or zero if that's negative. 177 """ 178 return max(int(info.unitsPerEm * 1.2) - info.ascender + info.descender, 0) 179 180def openTypeOS2WinAscentFallback(info): 181 """ 182 Fallback to *ascender + typoLineGap*. 183 """ 184 return info.ascender + getAttrWithFallback(info, "openTypeOS2TypoLineGap") 185 186def openTypeOS2WinDescentFallback(info): 187 """ 188 Fallback to *descender*. 189 """ 190 return abs(info.descender) 191 192# postscript 193 194_postscriptFontNameExceptions = set("[](){}<>/%") 195_postscriptFontNameAllowed = set([unichr(i) for i in range(33, 127)]) 196 197def normalizeStringForPostscript(s, allowSpaces=True): 198 s = tounicode(s) 199 normalized = [] 200 for c in s: 201 if c == " " and not allowSpaces: 202 continue 203 if c in _postscriptFontNameExceptions: 204 continue 205 if c not in _postscriptFontNameAllowed: 206 # Use compatibility decomposed form, to keep parts in ascii 207 c = unicodedata.normalize("NFKD", c) 208 if not set(c) < _postscriptFontNameAllowed: 209 c = tounicode(tobytes(c, errors="replace")) 210 normalized.append(tostr(c)) 211 return "".join(normalized) 212 213def normalizeNameForPostscript(name): 214 return normalizeStringForPostscript(name, allowSpaces=False) 215 216def postscriptFontNameFallback(info): 217 """ 218 Fallback to a string containing only valid characters 219 as defined in the specification. This will draw from 220 *openTypeNamePreferredFamilyName* and *openTypeNamePreferredSubfamilyName*. 221 """ 222 name = "%s-%s" % (getAttrWithFallback(info, "openTypeNamePreferredFamilyName"), getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName")) 223 return normalizeNameForPostscript(name) 224 225def postscriptFullNameFallback(info): 226 """ 227 Fallback to *openTypeNamePreferredFamilyName openTypeNamePreferredSubfamilyName*. 228 """ 229 return "%s %s" % (getAttrWithFallback(info, "openTypeNamePreferredFamilyName"), getAttrWithFallback(info, "openTypeNamePreferredSubfamilyName")) 230 231def postscriptSlantAngleFallback(info): 232 """ 233 Fallback to *italicAngle*. 234 """ 235 return getAttrWithFallback(info, "italicAngle") 236 237def postscriptUnderlineThicknessFallback(info): 238 """Return UPM * 0.05 (50 for 1000 UPM) and warn.""" 239 logger.warning( 240 'Underline thickness not set in UFO, defaulting to UPM * 0.05') 241 return info.unitsPerEm * 0.05 242 243def postscriptUnderlinePositionFallback(info): 244 """Return UPM * -0.075 (-75 for 1000 UPM) and warn.""" 245 logger.warning( 246 'Underline position not set in UFO, defaulting to UPM * -0.075') 247 return info.unitsPerEm * -0.075 248 249def postscriptBlueScaleFallback(info): 250 """ 251 Fallback to a calculated value: 3/(4 * *maxZoneHeight*) 252 where *maxZoneHeight* is the tallest zone from *postscriptBlueValues* 253 and *postscriptOtherBlues*. If zones are not set, return 0.039625. 254 """ 255 blues = getAttrWithFallback(info, "postscriptBlueValues") 256 otherBlues = getAttrWithFallback(info, "postscriptOtherBlues") 257 maxZoneHeight = 0 258 blueScale = 0.039625 259 if blues: 260 assert len(blues) % 2 == 0 261 for x, y in zip(blues[:-1:2], blues[1::2]): 262 maxZoneHeight = max(maxZoneHeight, abs(y-x)) 263 if otherBlues: 264 assert len(otherBlues) % 2 == 0 265 for x, y in zip(otherBlues[:-1:2], otherBlues[1::2]): 266 maxZoneHeight = max(maxZoneHeight, abs(y-x)) 267 if maxZoneHeight != 0: 268 blueScale = 3/(4*maxZoneHeight) 269 return blueScale 270 271# -------------- 272# Attribute Maps 273# -------------- 274 275staticFallbackData = dict( 276 styleMapStyleName="regular", 277 versionMajor=0, 278 versionMinor=0, 279 copyright=None, 280 trademark=None, 281 italicAngle=0, 282 # not needed 283 year=None, 284 note=None, 285 286 openTypeHeadLowestRecPPEM=6, 287 openTypeHeadFlags=[0, 1], 288 289 openTypeHheaLineGap=0, 290 openTypeHheaCaretOffset=0, 291 292 openTypeNameDesigner=None, 293 openTypeNameDesignerURL=None, 294 openTypeNameManufacturer=None, 295 openTypeNameManufacturerURL=None, 296 openTypeNameLicense=None, 297 openTypeNameLicenseURL=None, 298 openTypeNameDescription=None, 299 openTypeNameSampleText=None, 300 openTypeNameRecords=[], 301 302 openTypeOS2WidthClass=5, 303 openTypeOS2WeightClass=400, 304 openTypeOS2Selection=[], 305 openTypeOS2VendorID="NONE", 306 openTypeOS2Panose=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 307 openTypeOS2FamilyClass=[0, 0], 308 openTypeOS2UnicodeRanges=None, 309 openTypeOS2CodePageRanges=None, 310 openTypeOS2Type=[2], 311 312 openTypeOS2SubscriptXSize=None, 313 openTypeOS2SubscriptYSize=None, 314 openTypeOS2SubscriptXOffset=None, 315 openTypeOS2SubscriptYOffset=None, 316 openTypeOS2SuperscriptXSize=None, 317 openTypeOS2SuperscriptYSize=None, 318 openTypeOS2SuperscriptXOffset=None, 319 openTypeOS2SuperscriptYOffset=None, 320 openTypeOS2StrikeoutSize=None, 321 openTypeOS2StrikeoutPosition=None, 322 323 # fallback to None on these 324 # as the user should be in 325 # complete control 326 openTypeVheaVertTypoAscender=None, 327 openTypeVheaVertTypoDescender=None, 328 openTypeVheaVertTypoLineGap=None, 329 # fallback to horizontal caret: 330 # a value of 0 for the rise 331 # and a value of 1 for the run. 332 openTypeVheaCaretSlopeRise=0, 333 openTypeVheaCaretSlopeRun=1, 334 openTypeVheaCaretOffset=0, 335 336 postscriptUniqueID=None, 337 postscriptWeightName=None, 338 postscriptIsFixedPitch=False, 339 postscriptBlueValues=[], 340 postscriptOtherBlues=[], 341 postscriptFamilyBlues=[], 342 postscriptFamilyOtherBlues=[], 343 postscriptStemSnapH=[], 344 postscriptStemSnapV=[], 345 postscriptBlueFuzz=0, 346 postscriptBlueShift=7, 347 postscriptForceBold=0, 348 postscriptDefaultWidthX=200, 349 postscriptNominalWidthX=0, 350 351 # not used in OTF 352 postscriptDefaultCharacter=None, 353 postscriptWindowsCharacterSet=None, 354 355 # not used in OTF 356 macintoshFONDFamilyID=None, 357 macintoshFONDName=None 358) 359 360specialFallbacks = dict( 361 styleMapFamilyName=styleMapFamilyNameFallback, 362 openTypeHeadCreated=openTypeHeadCreatedFallback, 363 openTypeHheaAscender=openTypeHheaAscenderFallback, 364 openTypeHheaDescender=openTypeHheaDescenderFallback, 365 openTypeHheaCaretSlopeRise=openTypeHheaCaretSlopeRiseFallback, 366 openTypeHheaCaretSlopeRun=openTypeHheaCaretSlopeRunFallback, 367 openTypeNameVersion=openTypeNameVersionFallback, 368 openTypeNameUniqueID=openTypeNameUniqueIDFallback, 369 openTypeNamePreferredFamilyName=openTypeNamePreferredFamilyNameFallback, 370 openTypeNamePreferredSubfamilyName=openTypeNamePreferredSubfamilyNameFallback, 371 openTypeNameCompatibleFullName=openTypeNameCompatibleFullNameFallback, 372 openTypeNameWWSFamilyName=openTypeNameWWSFamilyNameFallback, 373 openTypeNameWWSSubfamilyName=openTypeNameWWSSubfamilyNameFallback, 374 openTypeOS2TypoAscender=openTypeOS2TypoAscenderFallback, 375 openTypeOS2TypoDescender=openTypeOS2TypoDescenderFallback, 376 openTypeOS2TypoLineGap=openTypeOS2TypoLineGapFallback, 377 openTypeOS2WinAscent=openTypeOS2WinAscentFallback, 378 openTypeOS2WinDescent=openTypeOS2WinDescentFallback, 379 postscriptFontName=postscriptFontNameFallback, 380 postscriptFullName=postscriptFullNameFallback, 381 postscriptSlantAngle=postscriptSlantAngleFallback, 382 postscriptUnderlineThickness=postscriptUnderlineThicknessFallback, 383 postscriptUnderlinePosition=postscriptUnderlinePositionFallback, 384 postscriptBlueScale=postscriptBlueScaleFallback 385) 386 387requiredAttributes = set(ufoLib.fontInfoAttributesVersion2) - (set(staticFallbackData.keys()) | set(specialFallbacks.keys())) 388 389recommendedAttributes = set([ 390 "styleMapFamilyName", 391 "versionMajor", 392 "versionMinor", 393 "copyright", 394 "trademark", 395 "openTypeHeadCreated", 396 "openTypeNameDesigner", 397 "openTypeNameDesignerURL", 398 "openTypeNameManufacturer", 399 "openTypeNameManufacturerURL", 400 "openTypeNameLicense", 401 "openTypeNameLicenseURL", 402 "openTypeNameDescription", 403 "openTypeNameSampleText", 404 "openTypeOS2WidthClass", 405 "openTypeOS2WeightClass", 406 "openTypeOS2VendorID", 407 "openTypeOS2Panose", 408 "openTypeOS2FamilyClass", 409 "openTypeOS2UnicodeRanges", 410 "openTypeOS2CodePageRanges", 411 "openTypeOS2TypoLineGap", 412 "openTypeOS2Type", 413 "postscriptBlueValues", 414 "postscriptOtherBlues", 415 "postscriptFamilyBlues", 416 "postscriptFamilyOtherBlues", 417 "postscriptStemSnapH", 418 "postscriptStemSnapV" 419]) 420 421# ------------ 422# Main Methods 423# ------------ 424 425def getAttrWithFallback(info, attr): 426 """ 427 Get the value for *attr* from the *info* object. 428 If the object does not have the attribute or the value 429 for the atribute is None, this will either get a 430 value from a predefined set of attributes or it 431 will synthesize a value from the available data. 432 """ 433 if hasattr(info, attr) and getattr(info, attr) is not None: 434 value = getattr(info, attr) 435 else: 436 if attr in specialFallbacks: 437 value = specialFallbacks[attr](info) 438 else: 439 value = staticFallbackData[attr] 440 return value 441 442def preflightInfo(info): 443 """ 444 Returns a dict containing two items. The value for each 445 item will be a list of info attribute names. 446 447 ================== === 448 missingRequired Required data that is missing. 449 missingRecommended Recommended data that is missing. 450 ================== === 451 """ 452 missingRequired = set() 453 missingRecommended = set() 454 for attr in requiredAttributes: 455 if not hasattr(info, attr) or getattr(info, attr) is None: 456 missingRequired.add(attr) 457 for attr in recommendedAttributes: 458 if not hasattr(info, attr) or getattr(info, attr) is None: 459 missingRecommended.add(attr) 460 return dict(missingRequired=missingRequired, missingRecommended=missingRecommended) 461 462# ----------------- 463# Low Level Support 464# ----------------- 465 466# these should not be used outside of this package 467 468def intListToNum(intList, start, length): 469 all = [] 470 bin = "" 471 for i in range(start, start+length): 472 if i in intList: 473 b = "1" 474 else: 475 b = "0" 476 bin = b + bin 477 if not (i + 1) % 8: 478 all.append(bin) 479 bin = "" 480 if bin: 481 all.append(bin) 482 all.reverse() 483 all = " ".join(all) 484 return binary2num(all) 485 486def dateStringToTimeValue(date): 487 try: 488 t = time.strptime(date, "%Y/%m/%d %H:%M:%S") 489 return calendar.timegm(t) 490 except ValueError: 491 return 0 492