Blob Blame History Raw
# 
# Slow interface to fontconfig for Dblatex, that only parses some commmand line
# output to store the fonts available on the system and their characteristics.
#
# An efficient solution should use some python bindings to directly call the
# C fontconfig library.
#
import sys
import logging
from subprocess import Popen, PIPE

def execute(cmd):
    p = Popen(cmd, stdout=PIPE)
    data = p.communicate()[0]
    if isinstance(data, bytes):
        data = data.decode(sys.getdefaultencoding())
    rc = p.wait()
    if rc != 0:
        raise OSError("'%s' failed (%d)" % (" ".join(cmd), rc))
    return data


class FcFont:
    """
    Font Object with properties filled with the fc-match command output.
    """
    def __init__(self, fontnames, partial=False):
        self.log = logging.getLogger("dblatex")
        self.name = fontnames[0]
        self.aliases = fontnames[1:]
        self._completed = False
        if not(partial):
            self.complete()

    def complete(self):
        if not(self._completed):
            d = execute(["fc-match", "--verbose", self.name])
            d = d.strip()
            self._build_attr_from(d)
            self._completed = True

    def _build_attr_from(self, data):
        ninfos = self._splitinfos(data)

        # Remove the first line
        ninfos[0] = ninfos[0].split("\n")[1]
        for i in ninfos:
            if i: self._buildattr(i)
        
        # Check the consistency
        if self.family != self.name.replace("\-", "-"):
            raise ValueError("Unknown font '%s' vs '%s'" % (self.name,
            self.family))

    def _splitinfos(self, data):
        ninfos = [data]
        for sep in ("(s)", "(w)", "(=)"):
            infos = ninfos
            ninfos = []
            for i in infos:
                ni = i.split(sep)
                ninfos += ni
        return ninfos

    def _buildattr(self, infos):
        """
        Parse things like:
           'fullname: "Mukti"(s)
            fullnamelang: "en"(s)
            slant: 0(i)(s)
            weight: 80(i)(s)
            width: 100(i)(s)
            size: 12(f)(s)'
        """
        try:
            attrname, attrdata = infos.split(":", 1)
        except:
            # Skip this row
            self.log.warning("Wrong data? '%s'" % infos)
            return
        
        #print infos
        attrname = attrname.strip() # Remove \t
        attrdata = attrdata.strip() # Remove space

        # Specific case
        if attrname == "charset":
            self._build_charset(attrdata)
            return

        # Get the data type
        if (not(attrdata) or (attrdata[0] == '"' and attrdata[-1] == '"')):
            setattr(self, attrname, attrdata.strip('"'))
            return
        
        if (attrdata.endswith("(i)")):
            setattr(self, attrname, int(attrdata.strip("(i)")))
            return

        if (attrdata.endswith("(f)")):
            setattr(self, attrname, float(attrdata.strip("(f)")))
            return

        if (attrdata == "FcTrue"):
            setattr(self, attrname, True)
            return

        if (attrdata == "FcFalse"):
            setattr(self, attrname, False)
            return

    def _build_charset(self, charset):
        """
        Parse something like:
           '0000: 00000000 ffffffff ffffffff 7fffffff 00000000 00002001 00800000 00800000
            0009: 00000000 00000000 00000000 00000030 fff99fee f3c5fdff b080399f 07ffffcf
            0020: 30003000 00000000 00000010 00000000 00000000 00001000 00000000 00000000
            0025: 00000000 00000000 00000000 00000000 00000000 00000000 00001000 00000000'
        """
        self.charsetstr = charset
        self.charset = []
        lines = charset.split("\n")
        for l in lines:
            umajor, row = l.strip().split(":", 1)
            int32s = row.split()
            p = 0
            for w in int32s:
                #print "=> %s" % w
                v = int(w, 16)
                for i in range(0, 32):
                    m = 1 << i
                    #m = 0x80000000 >> i
                    if (m & v):
                        uchar = umajor + "%02X" % (p + i)
                        #print uchar
                        self.charset.append(int(uchar, 16))
                p += 32

    def remove_char(self, char):
        try:
            self.charset.remove(char)
        except:
            pass

    def has_char(self, char):
        #print self.family, char, self.charset
        return (ord(char) in self.charset)


class FcManager:
    """
    Collect all the fonts available in the system. The building can be partial,
    i.e. the font objects can be partially created, and updated later (when
    used).

    The class tries to build three ordered list of fonts, one per standard
    generic font family:
    - Serif      : main / body font
    - Sans-serif : used to render sans-serif forms
    - Monospace  : used to render verbatim / monospace characters
    """
    def __init__(self):
        self.log = logging.getLogger("dblatex")
        self.fonts = {}
        self.fonts_family = {}

    def get_font(self, fontname):
        font = self.fonts.get(fontname)
        if font:
            font.complete()
        return font

    def get_font_handling(self, char, all=False, family_type=""):
        if not(family_type):
            font_family = self.fonts.values()
        else:
            font_family = self.fonts_family.get(family_type, None)
        
        if not(font_family):
            return []

        fonts = self.get_font_handling_from(font_family, char, all=all)
        return fonts

    def get_font_handling_from(self, fontlist, char, all=False):
        fonts = []
        # Brutal method to get something...
        for f in fontlist:
            f.complete()
            if f.has_char(char):
                if all:
                    fonts.append(f)
                else:
                    return f
        return fonts

    def build_fonts(self, partial=False):
        self.build_fonts_all(partial=partial)
        self.build_fonts_family("serif")
        self.build_fonts_family("sans-serif")
        self.build_fonts_family("monospace")

    def build_fonts_all(self, partial=False):
        # Grab all the fonts installed on the system
        d = execute(["fc-list"])
        fonts = d.strip().split("\n")
        for f in fonts:
            fontnames = f.split(":")[0].split(",")
            mainname = fontnames[0]
            if not(mainname):
                continue
            if self.fonts.get(mainname):
                self.log.debug("'%s': duplicated" % mainname)
                continue

            #print fontnames
            font = FcFont(fontnames, partial=partial)
            self.fonts[mainname] = font

    def build_fonts_family(self, family_type):
        # Create a sorted list matching a generic family
        # Use --sort to have only fonts completing unicode range
        font_family = []
        self.fonts_family[family_type] = font_family
        d = execute(["fc-match", "--sort", family_type, "family"])
        fonts = d.strip().split("\n")
        for f in fonts:
            font = self.fonts.get(f)
            if not(font in font_family):
                font_family.append(font)
        #print family_type
        #print font_family