Vanilla + Drawbot + Glyphs question

I have a few quick question! Forgive me, this is more Drawbot-specific, but I figured someone on this forum may have the answers, and it is relevant to people writing Drawbot code, with a UI, for Glyphs.

I happened across this Robofont example of using Drawbot and Vanilla in combination: https://robofont.com/documentation/building-tools/toolspace/drawbot/drawbot-view/

Pasting this code into a macro in Glyphs boots up perfectly for me, which is great!

However I notice that adding a line like:

self.w.drawBotCanvas.saveImage('path')

on, say, line 43 of the example code above, produces an error.

AttributeError: 'DrawView' object has no attribute 'saveImage'

Any idea why this would be? Why would the DrawView have a method like setPDFDocument but not saveImage?

How would one export an image (.png, .pdf, etc) from a DrawView in this situation?

And… why is the setPDFDocument call necessary, for that matter?

Thank you!

1 Like

Hi @HannaBarbera. I’ve been working a lot with Vanilla + DrawBot + Glyphs lately, so I’ll try my hand at answering!

The purpose of setPDFDocument is to update the DrawView with the latest version of the canvas/drawing. DrawBot starts a new drawing with newDrawing(), then adds a page and some elements to it. But that drawing is hidden if you’re not using the DrawBot app (or the DrawBot window within Glyphs). The DrawView element self.w.drawBotCanvas doesn’t automatically receive and show the drawing that your script is putting together. In order to show it in the DrawView, the drawing has to be converted into PDF data with pdfImage(), then passed to the DrawView’s setPDFDocument function. This process is only necessary because we’re working with a custom Vanilla window with its own DrawView—not DrawBot’s own preview window.

Regarding your main question: the most common approach to saving an image would be to simply call saveImage() on its own (not on an object), which will accept a file path string and some other optional parameters. That will export whatever your current DrawBot canvas/drawing is—which should meet your needs most of the time. This will even work with your Vanilla window example.

If you wanted to add a Save PDF button to the Vanilla window in your example, you could add a new method called, for example, saveIt() that captures the current DrawBot drawing and exports it. Here’s an updated demo, in which I’ve also changed/added a few lines near the top in order to add the button (lines 3, 11, and 12).

from drawBot import *
from drawBot.ui.drawView import DrawView
from vanilla import Window, Slider, Button

class DrawBotViewer(object):

    def __init__(self):
        # create a window
        self.w = Window((400, 400), minSize=(200, 200))
        # add a slider
        self.w.slider = Slider((10, 10, -100, 22), callback=self.sliderCallback)
    	self.w.saveButton = Button((-90, 10, -10, 22), 'Save It', callback=self.saveIt)
        # add a drawBox view
        self.w.drawBotCanvas = DrawView((0, 40, -0, -0))
        # draw something
        self.drawIt()
        # open the window
        self.w.open()

    def sliderCallback(self, sender):
        # slider chagned so redraw it
        self.drawIt()

    def drawIt(self):
        # get the value from the slider
        value = self.w.slider.get()
        print(value)
        # initiate a new drawing
        newDrawing()
        # add a page
        newPage(300, 300)
        # set a fill color
        fill(1, value/100., 0)
        # draw a rectangle
        rect(10, 10, 100, 100)
        # set a font size
        fontSize(48 + value)
        # draw some text
        text("Hello", (10, 120))
        # get the pdf document
        pdfData = pdfImage()
        # set the pdf document into the canvas
        self.w.drawBotCanvas.setPDFDocument(pdfData)

    def saveIt(self, sender):
        # Ask user to choose a file path and file name (ensures .pdf extension)
        path = GetSaveFile(filetypes=['pdf'])
        # if a file path was chosen, export the PDF there
        if path:
            saveImage( path )

DrawBotViewer()

From what I can tell, DrawBot wasn’t really made to have users programmatically saving/exporting the contents of a DrawView. It is possible, but it’s not wrapped in friendly DrawBot methods (or documented), so I’d try to avoid this approach. To do it, you could replace the saveImage( path ) line in the example above with this:

# (inside the if statement...)
# get the PDF data from the window's canvas
canvasAsPDF = self.w.drawBotCanvas.get()
# Save that data to a file path (should end in .pdf)
canvasAsPDF.writeToFile_atomically_( path , False)
1 Like

If the script works in Robofont, maybe they have changed the API in there version of Drawbot? I didn’t update the plugin for some time. Or did you install Drawbot directly?

@nowell, this is extremely helpful, thank you!

This sample code you wrote worked perfectly.

I did have one question, if you have the time and patience to answer it!

This is perhaps a more basic python question, but I am slightly confused about how hierarchy works, in terms of how the saveImage call is being used here. I would have expected the saveImage call to be related specifically to self.w.drawBotCanvas, but it seems to be a call that the entire DrawBotViewer class understands? DrawBotViewer is a brand new class that we are using here right, not a previously defined class?

I tried a test where I was able to make another class, with a second instance of drawbot (rendering its own graphics), but with no user interface (no DrawView, no need for setPDFDocument). And it, too, was able to output an image, without appearing on screen.

Am I right in thinking that somehow the call of newDrawing() within a class means that each following saveImage() call will apply only to the contents of newDrawing within a given instance of that class?

Drawbot records the drawing calls internally and replays it when you save or display. For each output the is a class with ‘Context’ in the name (if I remember correctly, not on my Mac right now). Something like ‘pdfComtext’, ‘svgContext’…

As Georg says, DrawBot stores the drawing internally—you don’t need to call DrawBot functions on any object or within a class. The class is only in that demo to assist with the creation of the Vanilla window. Interestingly… for better or for worse… this means that you could run your “hello” DrawBot demo script, then in a separate script (or tab in the Macro Panel) you could run saveImage() to export the same drawing.

I am not sure I understand, but I would love to see more documentation about this.

Oh interesting! So is it entirely chronological then? Like if you were to run a macro with saveImage() is that then associated with the newDrawing() that was created most recently?

Exactly—it’s all chronological. Which is why you have to set all of the modifying attributes of an element before creating that element (unless those attributes are keyword arguments when creating the element). For example, setting the color on text:

fill( r,g,b,a )
text(  ‘hello’, ... )

It’s definitely limiting, from time to time, but not as often as I’d expect!

@nowell @GeorgSeifert

One quick Drawbot question if you have a moment.

I am familiar with using drawPath(layer.bezierPath) to render a glyph in Drawbot.

I read in the Drawbot documentation that there is a clipPath(path) function.

However when I call clipPath(layer.bezierPath) in glyphs, I get the following error:

AttributeError: 'GSSaveBezierPath' object has no attribute 'getNSBezierPath'

Reading elsewhere in this forum @GeorgSeifert mentioned:

GSSaveBezierpath is a subclass of NSBezierpath. So all methods work the same.

Any ideas on what the hiccup here is?

There is a difference between a vanilla-bezierpath and a NSBezierPath.
The clip function needs the first kind, the drawPath function the later.

Just to make sure I understand:

A) The bezier path stored in the Glyphs layer is an NSBezierPath?

B) DrawBot’s drawPath() function can understand the NSBezierPath

C) However DrawBot’s clipPath() is expecting a BezierPath stored another way?

Do you know where I can find documentation on the ‘vanilla-bezierpath’ you mentioned?

Alternatvely:

Is there an existing way to mask an NSBezierPath with another NSBezierPath?

Realistically for my purposes, some sort of NSBezierPath based addition/subtraction of paths would be useful to me (is this the correct terminology?). A way to subtract an NSBezierPath from another NSBezierPath (creating a mask, essentially), and a way to add two NSBezierPaths together to form a new NSBezierPath. Are there methods for this within Python?

Follow up: I eventually realized you can just use:

clippingPath = BezierPath(layer.bezierPath) # converts to a path that DrawBot understands

clipPath(clippingPath)
2 Likes