Color fonts: “Create Outlines”, changing palettes



  • I realize this is a pretty niche request, but I found it interesting so I figured I’d share it. Perhaps there is even a way of doing this already?

    1. Normally, to convert text into outlines I would use a BezierPath object with a text method, but this doesn’t help me with a COLR/CPAL color font because the color information is lost to the monochromatic BezierPath object. Is there another way of converting text to a series of differently-colored paths?

    2. I would also love to access alternative color palettes stored in the CPAL table, as well as to designate my own (as was requested here: https://twitter.com/bicvudesign/status/1058063097485803520). Maybe this requires us to wait for OS support, but I figured I’d mention it in case you were interested in pursuing.

    As always, thanks again for all your work on this amazing tool!



  • Hello! I'm no expert on DrawBot but I found your question to be very interesting, so I looked into figuring out a way to do a "color separation" of sorts — basically, I wanted to know if it was possible, as you suggested, to get a different BezierPath for each layer of color in a given string.

    And it looks like it is (at least for Pappardelle Party)! Here's some rough & ready code (a lot of it is a copy/paste from the DrawBot BezierPath.textBox method, just to convert a string into its glyphNames — probably this method could be simplified but I don't know my way around the CoreText calls all that well), but the code seems to be working well on my computer running in the latest DrawBot app.

    The bit at the end is where the customization happens, and you can access all the alternative CPAL palettes, just with the index of the palette in the CPAL table. (The one pictured is 1 I think).

    0_1541179864083_Screen Shot 2018-11-02 at 10.30.28 AM.png

    import CoreText
    import AppKit
    
    def glyphIdentifiers(txt, font, fontSize):
        from drawBot.context.baseContext import BaseContext
        context = BaseContext()
        context.font(font, fontSize)
        path, (x, y) = context._getPathForFrameSetter((0, 0, 1200, 1200))
        attributedString = context.attributedString(txt, "left")
        setter = CoreText.CTFramesetterCreateWithAttributedString(attributedString)
        frame = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None)
        ctLines = CoreText.CTFrameGetLines(frame)
        origins = CoreText.CTFrameGetLineOrigins(frame, (0, len(ctLines)), None)
    
        glyphIDs = []
        for i, (originX, originY) in enumerate(origins):
            ctLine = ctLines[i]
            ctRuns = CoreText.CTLineGetGlyphRuns(ctLine)
            for ctRun in ctRuns:
                attributes = CoreText.CTRunGetAttributes(ctRun)
                font = attributes.get(AppKit.NSFontAttributeName)
                baselineShift = attributes.get(AppKit.NSBaselineOffsetAttributeName, 0)
                glyphCount = CoreText.CTRunGetGlyphCount(ctRun)
                for i in range(glyphCount):
                    glyph = CoreText.CTRunGetGlyphs(ctRun, (i, 1), None)[0]
                    ax, ay = CoreText.CTRunGetPositions(ctRun, (i, 1), None)[0]
                    if glyph:
                        glyphIDs.append(glyph)
        return glyphIDs
    
    def getFontToolsFont(font_path):
        from fontTools.ttLib import TTFont, TTLibError
        from fontTools.misc.macRes import ResourceReader, ResourceError
        return TTFont(font_path, lazy=True, fontNumber=None, res_name_or_index=None)
        
    def glyphIdentifierToGlyphName(ft_font, glyph_id):
        return ft_font.getGlyphOrder()[glyph_id]
    
    from fontTools.ttLib.tables.C_O_L_R_ import table_C_O_L_R_
    from fontTools.ttLib.tables.C_P_A_L_ import table_C_P_A_L_
    
    def hex_to_tuple(palette):
        return tuple([c/255 for c in (palette.red, palette.green, palette.blue, palette.alpha)])
    
    def getPathAndColorForColorID(f, font_size, txt, _rect, color_id=0, palette_id=0):
        fs2 = FormattedString()
        fs2.font(f)
        fs2.fontSize(font_size)
        
        ft_font = getFontToolsFont(fs2.fontFilePath())
        colr = table_C_O_L_R_()
        cpal = table_C_P_A_L_()
        colr.decompile(ft_font.getTableData("COLR"), ft_font)
        cpal.decompile(ft_font.getTableData("CPAL"), ft_font)
        
        ids = glyphIdentifiers(txt, f, 500)
        glyphNames = [glyphIdentifierToGlyphName(ft_font, _id) for _id in ids]
        
        for glyphName in glyphNames:
            for layer in colr[glyphName]:
                if layer.colorID == color_id:
                    fs2.fill(*hex_to_tuple(cpal.palettes[palette_id][layer.colorID]))
                    fs2.appendGlyph(layer.name)
        
        bp = BezierPath()
        bp.textBox(fs2, _rect)
        return bp, hex_to_tuple(cpal.palettes[palette_id][color_id])
    
    #############################
    ##### CUSTOMIZABLE CODE #####
    #############################
    
    font_name = "Pappardelle Party Regular"
    font_size = 300
    txt = "HELLO WORLD"
    r = (100, 100, 800, 800)
    palette_id = 1
    
    for c in range(0, 4): # <----- which layers of color to actually print
        path, color = getPathAndColorForColorID(font_name, font_size, txt, r, color_id=c, palette_id=palette_id)
        fill(*color)
        drawPath(path)
    
    


  • @robstenson Wow!! This works perfectly for me. 👏👏👏

    Of course it would be cool to see something like these solutions worked into the drawbot API, but this solves my problem. Thanks so much!


  • admin

    Waauw, nice!!

    Im opening a issue to discuss it further see https://github.com/typemytype/drawbot/issues/237


Log in to reply