1def OpenFont(path, showInterface=True):
2    """
3    Open font located at **path**. If **showInterface**
4    is ``False``, the font should be opened without
5    graphical interface. The default for **showInterface**
6    is ``True``.
7
8    ::
9
10        from fontParts.world import *
11
12        font = OpenFont("/path/to/my/font.ufo")
13        font = OpenFont("/path/to/my/font.ufo", showInterface=False)
14    """
15    return dispatcher["OpenFont"](pathOrObject=path, showInterface=showInterface)
16
17
18def NewFont(familyName=None, styleName=None, showInterface=True):
19    """
20    Create a new font. **familyName** will be assigned
21    to ``font.info.familyName`` and **styleName**
22    will be assigned to ``font.info.styleName``. These
23    are optional and default to ``None``. If **showInterface**
24    is ``False``, the font should be created without
25    graphical interface. The default for **showInterface**
26    is ``True``.
27
28    ::
29
30        from fontParts.world import *
31
32        font = NewFont()
33        font = NewFont(familyName="My Family", styleName="My Style")
34        font = NewFont(showInterface=False)
35    """
36    return dispatcher["NewFont"](familyName=familyName, styleName=styleName,
37                                 showInterface=showInterface)
38
39
40def CurrentFont():
41    """
42    Get the "current" font.
43    """
44    return dispatcher["CurrentFont"]()
45
46
47def CurrentGlyph():
48    """
49    Get the "current" glyph from :func:`CurrentFont`.
50
51    ::
52
53        from fontParts.world import *
54
55        glyph = CurrentGlyph()
56    """
57    return dispatcher["CurrentGlyph"]()
58
59
60def CurrentLayer():
61    """
62    Get the "current" layer from :func:`CurrentGlyph`.
63
64    ::
65
66        from fontParts.world import *
67
68        layer = CurrentLayer()
69    """
70    return dispatcher["CurrentLayer"]()
71
72
73def CurrentContours():
74    """
75    Get the "currently" selected contours from :func:`CurrentGlyph`.
76
77    ::
78
79        from fontParts.world import *
80
81        contours = CurrentContours()
82
83    This returns an immutable list, even when nothing is selected.
84    """
85    return dispatcher["CurrentContours"]()
86
87
88def _defaultCurrentContours():
89    glyph = CurrentGlyph()
90    if glyph is None:
91        return ()
92    return glyph.selectedContours
93
94
95def CurrentSegments():
96    """
97    Get the "currently" selected segments from :func:`CurrentContours`.
98
99    ::
100
101        from fontParts.world import *
102
103        segments = CurrentSegments()
104
105    This returns an immutable list, even when nothing is selected.
106    """
107    return dispatcher["CurrentSegments"]()
108
109
110def _defaultCurrentSegments():
111    glyph = CurrentGlyph()
112    if glyph is None:
113        return ()
114    segments = []
115    for contour in glyph.selectedContours:
116        segments.extend(contour.selectedSegments)
117    return tuple(segments)
118
119
120def CurrentPoints():
121    """
122    Get the "currently" selected points from :func:`CurrentContours`.
123
124    ::
125
126        from fontParts.world import *
127
128        points = CurrentPoints()
129
130    This returns an immutable list, even when nothing is selected.
131    """
132    return dispatcher["CurrentPoints"]()
133
134
135def _defaultCurrentPoints():
136    glyph = CurrentGlyph()
137    if glyph is None:
138        return ()
139    points = []
140    for contour in glyph.selectedContours:
141        points.extend(contour.selectedPoints)
142    return tuple(points)
143
144
145def CurrentComponents():
146    """
147    Get the "currently" selected components from :func:`CurrentGlyph`.
148
149    ::
150
151        from fontParts.world import *
152
153        components = CurrentComponents()
154
155    This returns an immutable list, even when nothing is selected.
156    """
157    return dispatcher["CurrentComponents"]()
158
159
160def _defaultCurrentComponents():
161    glyph = CurrentGlyph()
162    if glyph is None:
163        return ()
164    return glyph.selectedComponents
165
166
167def CurrentAnchors():
168    """
169    Get the "currently" selected anchors from :func:`CurrentGlyph`.
170
171    ::
172
173        from fontParts.world import *
174
175        anchors = CurrentAnchors()
176
177    This returns an immutable list, even when nothing is selected.
178    """
179    return dispatcher["CurrentAnchors"]()
180
181
182def _defaultCurrentAnchors():
183    glyph = CurrentGlyph()
184    if glyph is None:
185        return ()
186    return glyph.selectedAnchors
187
188
189def CurrentGuidelines():
190    """
191    Get the "currently" selected guidelines from :func:`CurrentGlyph`.
192    This will include both font level and glyph level guidelines.
193
194    ::
195
196        from fontParts.world import *
197
198        guidelines = CurrentGuidelines()
199
200    This returns an immutable list, even when nothing is selected.
201    """
202    return dispatcher["CurrentGuidelines"]()
203
204
205def _defaultCurrentGuidelines():
206    guidelines = []
207    font = CurrentFont()
208    if font is not None:
209        guidelines.extend(font.selectedGuidelines)
210    glyph = CurrentGlyph()
211    if glyph is not None:
212        guidelines.extend(glyph.selectedGuidelines)
213    return tuple(guidelines)
214
215
216def AllFonts(sortOptions=None):
217    """
218    Get a list of all open fonts. Optionally, provide a
219    value for ``sortOptions`` to sort the fonts. See
220    :meth:`world.FontList.sortBy` for options.
221
222
223    ::
224
225        from fontParts.world import *
226
227        fonts = AllFonts()
228        for font in fonts:
229            # do something
230
231        fonts = AllFonts("magic")
232        for font in fonts:
233            # do something
234
235        fonts = AllFonts(["familyName", "styleName"])
236        for font in fonts:
237            # do something
238    """
239    fontList = FontList(dispatcher["AllFonts"]())
240    if sortOptions is not None:
241        fontList.sortBy(sortOptions)
242    return fontList
243
244
245def RFont(path=None, showInterface=True):
246    return dispatcher["RFont"](pathOrObject=path, showInterface=showInterface)
247
248
249def RGlyph():
250    return dispatcher["RGlyph"]()
251
252
253# ---------
254# Font List
255# ---------
256
257def FontList(fonts=None):
258    """
259    Get a list with font specific methods.
260
261    ::
262
263        from fontParts.world import *
264
265        fonts = FontList()
266
267    Refer to :class:`BaseFontList` for full documentation.
268    """
269    l = dispatcher["FontList"]()
270    if fonts:
271        l.extend(fonts)
272    return l
273
274
275class BaseFontList(list):
276
277    # Sort
278
279    def sortBy(self, sortOptions, reverse=False):
280        """
281        Sort ``fonts`` with the ordering preferences defined
282        by ``sortBy``. ``sortBy`` must be one of the following:
283
284        * sort description string
285        * :class:`BaseInfo` attribute name
286        * sort value function
287        * list/tuple containing sort description strings, :class:`BaseInfo`
288          attribute names and/or sort value functions
289        * ``"magic"``
290
291        Sort Description Strings
292        ------------------------
293
294        The sort description strings, and how they modify the sort, are:
295
296        +--------------------+--------------------------------------+
297        | ``"familyName"``   | Family names by alphabetical order.  |
298        +--------------------+--------------------------------------+
299        | ``"styleName"``    | Style names by alphabetical order.   |
300        +--------------------+--------------------------------------+
301        | ``"isItalic"``     | Italics before romans.               |
302        +--------------------+--------------------------------------+
303        | ``"isRoman"``      | Romans before italics.               |
304        +--------------------+--------------------------------------+
305        | ``"widthValue"``   | Width values by numerical order.     |
306        +--------------------+--------------------------------------+
307        | ``"weightValue"``  | Weight values by numerical order.    |
308        +--------------------+--------------------------------------+
309        | ``"monospace"``    | Monospaced before proportional.      |
310        +--------------------+--------------------------------------+
311        | ``"proportional"`` | Proportional before monospaced.      |
312        +--------------------+--------------------------------------+
313
314        ::
315
316            >>> fonts.sortBy(("familyName", "styleName"))
317
318
319        Font Info Attribute Names
320        -------------------------
321
322        Any :class:`BaseFont` attribute name may be included as
323        a sort option. For example, to sort by x-height value,
324        you'd use the ``"xHeight"`` attribute name.
325
326        ::
327
328            >>> fonts.sortBy("xHeight")
329
330
331        Sort Value Function
332        -------------------
333
334        A sort value function must be a function that accepts
335        one argument, ``font``. This function must return
336        a sortable value for the given font. For example:
337
338        ::
339
340            def glyphCountSortValue(font):
341                return len(font)
342
343            >>> fonts.sortBy(glyphCountSortValue)
344
345        A list of sort description strings and/or sort functions
346        may also be provided. This should be in order of most
347        to least important. For example, to sort by family name
348        and then style name, do this:
349
350
351        "magic"
352        -------
353
354        If "magic" is given for ``sortBy``, the fonts will be
355        sorted based on this sort description sequence:
356
357        * ``"familyName"``
358        * ``"isProportional"``
359        * ``"widthValue"``
360        * ``"weightValue"``
361        * ``"styleName"``
362        * ``"isRoman"``
363
364        ::
365
366            >>> fonts.sortBy("magic")
367        """
368        from types import FunctionType
369        valueGetters = dict(
370            familyName=_sortValue_familyName,
371            styleName=_sortValue_styleName,
372            isRoman=_sortValue_isRoman,
373            isItalic=_sortValue_isItalic,
374            widthValue=_sortValue_widthValue,
375            weightValue=_sortValue_weightValue,
376            isProportional=_sortValue_isProportional,
377            isMonospace=_sortValue_isMonospace
378        )
379        if isinstance(sortOptions, str) or isinstance(sortOptions, FunctionType):
380            sortOptions = [sortOptions]
381        if not isinstance(sortOptions, (list, tuple)):
382            raise ValueError("sortOptions must a string, list or function.")
383        if not sortOptions:
384            raise ValueError("At least one sort option must be defined.")
385        if sortOptions == ["magic"]:
386            sortOptions = [
387                "familyName",
388                "isProportional",
389                "widthValue",
390                "weightValue",
391                "styleName",
392                "isRoman"
393            ]
394        sorter = []
395        for originalIndex, font in enumerate(self):
396            sortable = []
397            for valueName in sortOptions:
398                if isinstance(valueName, FunctionType):
399                    value = valueName(font)
400                elif valueName in valueGetters:
401                    value = valueGetters[valueName](font)
402                elif hasattr(font.info, valueName):
403                    value = getattr(font.info, valueName)
404                else:
405                    raise ValueError("Unknown sort option: %s" % repr(valueName))
406                sortable.append(value)
407            sortable.append(originalIndex)
408            sortable.append(font)
409            sorter.append(tuple(sortable))
410        sorter.sort()
411        fonts = [i[-1] for i in sorter]
412        del self[:]
413        self.extend(fonts)
414        if reverse:
415            self.reverse()
416
417    # Search
418
419    def getFontsByFontInfoAttribute(self, *attributeValuePairs):
420        """
421        Get a list of fonts that match the (attribute, value)
422        combinations in ``attributeValuePairs``.
423
424        ::
425
426            >>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20))
427            >>> subFonts = fonts.getFontsByFontInfoAttribute(("xHeight", 20), ("descender", -150))
428
429        This will return an instance of :class:`BaseFontList`.
430        """
431        found = self
432        for attr, value in attributeValuePairs:
433            found = self._matchFontInfoAttributes(found, (attr, value))
434        return found
435
436    def _matchFontInfoAttributes(self, fonts, attributeValuePair):
437        found = self.__class__()
438        attr, value = attributeValuePair
439        for font in fonts:
440            if getattr(font.info, attr) == value:
441                found.append(font)
442        return found
443
444    def getFontsByFamilyName(self, familyName):
445        """
446        Get a list of fonts that match ``familyName``.
447        This will return an instance of :class:`BaseFontList`.
448        """
449        return self.getFontsByFontInfoAttribute(("familyName", familyName))
450
451    def getFontsByStyleName(self, styleName):
452        """
453        Get a list of fonts that match ``styleName``.
454        This will return an instance of :class:`BaseFontList`.
455        """
456        return self.getFontsByFontInfoAttribute(("styleName", styleName))
457
458    def getFontsByFamilyNameStyleName(self, familyName, styleName):
459        """
460        Get a list of fonts that match ``familyName`` and ``styleName``.
461        This will return an instance of :class:`BaseFontList`.
462        """
463        return self.getFontsByFontInfoAttribute(("familyName", familyName), ("styleName", styleName))
464
465
466def _sortValue_familyName(font):
467    """
468    Returns font.info.familyName.
469    """
470    value = font.info.familyName
471    if value is None:
472        value = ""
473    return value
474
475
476def _sortValue_styleName(font):
477    """
478    Returns font.info.styleName.
479    """
480    value = font.info.styleName
481    if value is None:
482        value = ""
483    return value
484
485
486def _sortValue_isRoman(font):
487    """
488    Returns 0 if the font is roman.
489    Returns 1 if the font is not roman.
490    """
491    italic = _sortValue_isItalic(font)
492    if italic == 1:
493        return 0
494    return 1
495
496
497def _sortValue_isItalic(font):
498    """
499    Returns 0 if the font is italic.
500    Returns 1 if the font is not italic.
501    """
502    info = font.info
503    styleMapStyleName = info.styleMapStyleName
504    if styleMapStyleName is not None and "italic" in styleMapStyleName:
505        return 0
506    if info.italicAngle not in (None, 0):
507        return 0
508    return 1
509
510
511def _sortValue_widthValue(font):
512    """
513    Returns font.info.openTypeOS2WidthClass.
514    """
515    value = font.info.openTypeOS2WidthClass
516    if value is None:
517        value = -1
518    return value
519
520
521def _sortValue_weightValue(font):
522    """
523    Returns font.info.openTypeOS2WeightClass.
524    """
525    value = font.info.openTypeOS2WeightClass
526    if value is None:
527        value = -1
528    return value
529
530
531def _sortValue_isProportional(font):
532    """
533    Returns 0 if the font is proportional.
534    Returns 1 if the font is not proportional.
535    """
536    monospace = _sortValue_isMonospace(font)
537    if monospace == 1:
538        return 0
539    return 1
540
541
542def _sortValue_isMonospace(font):
543    """
544    Returns 0 if the font is monospace.
545    Returns 1 if the font is not monospace.
546    """
547    if font.info.postscriptIsFixedPitch:
548        return 0
549    if not len(font):
550        return 1
551    testWidth = None
552    for glyph in font:
553        if testWidth is None:
554            testWidth = glyph.width
555        else:
556            if testWidth != glyph.width:
557                return 1
558    return 0
559
560
561# ----------
562# Dispatcher
563# ----------
564
565class _EnvironmentDispatcher(object):
566
567    def __init__(self, registryItems):
568        self._registry = {item: None for item in registryItems}
569
570    def __setitem__(self, name, func):
571        self._registry[name] = func
572
573    def __getitem__(self, name):
574        func = self._registry[name]
575        if func is None:
576            raise NotImplementedError
577        return func
578
579
580dispatcher = _EnvironmentDispatcher([
581    "OpenFont",
582    "NewFont",
583    "AllFonts",
584    "CurrentFont",
585    "CurrentGlyph",
586    "CurrentLayer",
587    "CurrentContours",
588    "CurrentSegments",
589    "CurrentPoints",
590    "CurrentComponents",
591    "CurrentAnchors",
592    "CurrentGuidelines",
593    "FontList",
594    "RFont",
595    "RLayer",
596    "RGlyph",
597    "RContour",
598    "RPoint",
599    "RAnchor",
600    "RComponent",
601    "RGuideline",
602    "RImage",
603    "RInfo",
604    "RFeatures",
605    "RGroups",
606    "RKerning",
607    "RLib",
608
609])
610
611# Register the default functions.
612
613dispatcher["CurrentContours"] = _defaultCurrentContours
614dispatcher["CurrentSegments"] = _defaultCurrentSegments
615dispatcher["CurrentPoints"] = _defaultCurrentPoints
616dispatcher["CurrentComponents"] = _defaultCurrentComponents
617dispatcher["CurrentAnchors"] = _defaultCurrentAnchors
618dispatcher["CurrentGuidelines"] = _defaultCurrentGuidelines
619dispatcher["FontList"] = BaseFontList
620
621# -------
622# fontshell
623# -------
624
625try:
626    from fontParts import fontshell
627
628    # OpenFont, RFont
629
630    def _fontshellRFont(pathOrObject=None, showInterface=True):
631        return fontshell.RFont(pathOrObject=pathOrObject, showInterface=showInterface)
632
633    dispatcher["OpenFont"] = _fontshellRFont
634    dispatcher["RFont"] = _fontshellRFont
635
636    # NewFont
637
638    def _fontshellNewFont(familyName=None, styleName=None, showInterface=True):
639        font = fontshell.RFont(showInterface=showInterface)
640        if familyName is not None:
641            font.info.familyName = familyName
642        if styleName is not None:
643            font.info.styleName = styleName
644        return font
645
646    dispatcher["NewFont"] = _fontshellNewFont
647
648    # RLayer, RGlyph, RContour, RPoint, RAnchor, RComponent, RGuideline, RImage, RInfo, RFeatures, RGroups, RKerning, RLib
649
650    dispatcher["RLayer"] = fontshell.RLayer
651    dispatcher["RGlyph"] = fontshell.RGlyph
652    dispatcher["RContour"] = fontshell.RContour
653    dispatcher["RPoint"] = fontshell.RPoint
654    dispatcher["RAnchor"] = fontshell.RAnchor
655    dispatcher["RComponent"] = fontshell.RComponent
656    dispatcher["RGuideline"] = fontshell.RGuideline
657    dispatcher["RImage"] = fontshell.RImage
658    dispatcher["RInfo"] = fontshell.RInfo
659    dispatcher["RFeatures"] = fontshell.RFeatures
660    dispatcher["RGroups"] = fontshell.RGroups
661    dispatcher["RKerning"] = fontshell.RKerning
662    dispatcher["RLib"] = fontshell.RLib
663
664except ImportError:
665    pass
666