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