Allow prawn to #draw an encapsulated component. #662

wants to merge 4 commits into

2 participants


This PR gives Prawn::Document a #draw method that accepts an object and a list of arguments. Prawn will then in turn pass itself and those arguments to the #call method on that object. This gives us a clean standard for bundling and encapsulating behavior we want to add to a prawn document.

Take this simple example

class CaptionedImage
  def document, *options
    document.image options[:image]
    document.text options[:text]

doc =
doc.draw CaptionedImage, image: "red_wings.jpg", "Detroit winning another hard earned Stanley Cup"

This could be expanded for more complex examples but I think it makes the point. I'm not 100% sold on the use of #draw for the method name but I think it's probably good enough. This method is also the core functionality needed to provide a possibly prettier way for prawn plugin authors to distribute their additional features as well.

What does everyone think?

prawnpdf member

The concept is interesting to me, for sure.

Here are my concerns:

  • I don't want to mix yet another module into Prawn::Document. I think we can define this method in lib/prawn/document.rb which I'm not happy with either, but it'll at least make it clear exactly how much the class is expanding by.

  • I want to see many more examples (I.e. half a dozen or more) to see whether this API will support a wide variety of use cases, and what value it gives us for each of them.

  • I haven't decided whether passing options directly to the component with no opportunity to process them in draw() is the right thing to do, but I think we can discuss that once we see some more examples.

  • Please use parentheses in method definitions when working on Prawn.


Here is a half dozen examples that I have pulled from actual projects and reformatted to use this proposed #draw style. Most of these examples fall into either abstracting repeating groups of functionality together or more semantic abstractions of larger components, regardless of reuse.

It should also be noted that in my particular implementations, I have a DEFAULT_OPTIONS hash that the passed options get merged into making these components easier to deal with in the more common cases.

1. Bordered Image

This component takes an image, places it on the document, and uses Prawn::Document#line to border it.

@document.draw BorderedImage, x_pos: x,
                              y_pos: y,
                              image: image,
                              height: height,
                              color: '000000',
                              border_width: 1

2. Bordered Image with Text

This component re-uses the above BorderedImage component but will also allow the user to caption the image with text positioned either above or below the image aligning that text either to the left, center, or right.

@document.draw BorderedImageWithText, x_pos: x,
                                      y_pos: x,
                                      image: image,
                                      height: height,
                                      color: '000000',
                                      border_width: 1,
                                      text: "Example Text",
                                      text_alignment: :top_left

3. Color Box

This is a fairly simple wrapper to set a colored box in a more concise way by setting the fill color, drawing the rectangle and reverting to the previous fill color, all on one line. This is a pattern I use frequently on some documents.

@document.draw ColorBox, color: 'FF0000',
                         width: in2pt(1),
                         height: in2pt(2),
                         x_pos: in2pt(5),
                         y_pos: in2pt(5)

4. Side Bar

Several of my documents have repeating design elements on a number of pages with slight variations, this side bar is one of them. It is identically positioned and sized with a changing background image or solid background color (which in turn uses ColorBox above)

@document.draw SideBar, color: '000000'

# or

@document.draw SideBar, image: image

5. Calendar

I have an in house version of what is practically the prawn_calendar gem. It gets re-used on multiple documents as it takes a defined dimension and dates/text and plots them out using Prawn::Document#table

@document.draw Calendar, width: in2pt(10),
                         height: in2pt(6),
                         entries: [
                           {date: #<Date:0x0000>, text: "Signed"},
                           {date: #<Date:0x0000>, text: "Sealed"},
                           {date: #<Date:0x0000>, text: "Delivered"},

6. User Chart

I also have a component that will take a user, generate a custom gruff chart for that user and insert it into a cover page of a metrics document.

@document.draw UserChart, x_pos: in2pt(10),
                          y_pos: in2pt(10),
                          user: user

Most other examples I have off hand are similar groupings to what I showed above, more reused complex table layouts like the calendar or other repeating patterns like the color box.


prawnpdf member


These are great use cases. The thing I'm probably missing is what tangible benefit the proposed API has over ordinary object creation.

I.e. what is the tangible gain of the former example over the latter in the code below?

# Your proposal
@document.draw(Component, options)

# An alternative that does not require changing Prawn 

Note: I'm not saying there aren't benefits to be had here if we develop the idea a little more, just saying we need to figure out what they are.


Normally using an external object that accepts the document is exactly how I accomplish all of these currently. Here are a few thoughts on why Prawn::Document#draw may be better.

  1. It allows for consistent external / internal subdivisions. For example Prawn::Document::Table is called very similarly. I like the idea of including external code the same way we could include higher level internal code.

  2. I like the idea of providing an explicit method to to nudge prawn users to break up their more complex documents into sub-components.

  3. I was thinking of levering this to register component code, but we already discussed why that's not a great plan on IRC but it did go into my thinking for this PR.

  4. It does give us an opportunity to do things for the component on it's behalf, like possibly abstracting out positioning, fill colors or other more generic needs. This PR itself doesn't do that but by proxying the call though prawn itself it leaves the possibility open.

Also, this could totally be implemented using the Prawn::Document.extension << api as it's mostly syntax sugar. I have no problem making a prawn-draw gem and going that route.

prawnpdf member

I agree with all the points you mentioned as good design goals, but I feel like (1) is solvable by ordinary object composition, (3) is no longer relevant, and (4) is still hypothetical.

Because (2) is not a strong enough reason on its own to go with this particular implementation, I'd be interested in exploring (4) more. In other words, I want to consider what we could do to make this more than syntactic sugar, so that users have an incentive to model things this way.


Looking though my components, I think I have at least two patterns that if we could abstract their solution into this call, would simplify the end code as well.

Child Views

This I think would be a huge win if we could sort out a good way to do it. Very often I'm breaking up a page into self contained areas, similar to partials in a rails view. However the component must know how big of an area it has to work with and where it is positioned. In my case I'll pass that information in and the component itself will calculate the correct sizes relative to the whole document. If we could optionally scope the whole call to an area of the page, then that arithmetic would be entirely not needed. For example my Sidebar example knows it's 2.5 inches wide, 8.5 inches tall (landscape PDF) and anchored to the left side. If we could adjust the call such to provide that scoping information to prawn, the component itself doesn't have to track that. It just fills 100% width and height of it's space. This becomes even better when we drill down.

For example I'll have a category, that has multiple columns, that has multiple items, that has multiple text boxes, each inset within each other. That's passing around a lot of numbers and forcing the components track that. if instead each component could just use the max width afforded to it,

Now, this is of course possible by passing those values down into each component successively, which is what I do, but centralizing that translation into once prawn wrapper and removing it from every component would be a very real simplification and not just a nicer looking method call.


Many times I'll copy the existing fill color, set a new fill color, then reset the fill color back to the original setting. In some cases prawn allows block versions of methods that do something similar but I don't believe fill_color is one of them. If we had a generic version of that pattern that would scope all state changes like that, we could wrap wrap #call in that to make the components not have to care about cleaning up after themselves, which could be a real nice bonus.

Really it comes down to isolating the state of the document from the state of the component and the area of the component from the area of the document.

Any thoughts on if these are good ideas or how we could implement them in a generic way? Even if we built into core prawn the ability for those global state wrappers and global positioning code the #draw method that calls it could still be a sugar wrapper that gets coded in an external gem and not included in prawn itself.

prawnpdf member

Thanks @packetmonkey. I'm still mulling this over. Since whatever we do come up with here will probably start out as an experimental API or extension API rather than a stable API, we're not constrained by the stable API freeze coming up in a few days. I think I'll focus on getting the 0.15.0 release out first, then we can continue the conversation from there.


I just pushed up an extra method that gets called in #draw called #preserve_state which will save and restore the settable methods on the prawn document. I looked though the prawn API and I think I caught all settings that can get changed but I may have missed some.

For example it now lets this script

require 'prawn'

doc =

doc.text 'foo'

doc.preserve_state do
  doc.fill_color = 'FF0000'
  doc.font 'Courier'
  doc.font_size 24
  doc.text 'bar'

doc.text 'baz'

doc.render_file 'output.pdf'

render a document looking like this

screen shot 2014-02-15 at 10 44 11 pm

This is the first step in better isolating components. The next step I'll play with is what we discussed in IRC with a proxy object. I didn't test this code as it's more proof of concept at this point for discussion, if we get ready to merge I'll get test written but for now don't merge.

prawnpdf member

The idea of making components sandboxed is definitely what we want, and preserve_state is a good proof of concept for the intended behavior, but it's not going to be a sane way of solving the problem due to Prawn's messy design.

Here are just a few pieces of mutable state you missed:

I feel like hunting down all these different bits of state and manually setting them is going to be like playing a game of whack-a-mole: without a centralized control point it will be easy for this method to get out of sync, and corrupt the state of documents.

So I think the next step involves either a separate pull request that figures out how to unify our state mutations safely, or the creation of a proxy object that whitelists document features to avoid the ones that have global effects, and then create some sort of style object to simplify passing the necessary parameters.

@practicingruby practicingruby added this to the 1.0 Wishlist milestone Feb 24, 2014
prawnpdf member

We are not giving up on this idea, but we're going to work on it from a different angle. Talked to @packetmonkey in #prawn and our basic plan is to first create a unified drawing API for all of Prawn's current components (text box, table, table cell, image, and maybe a Drawing object for vector graphics), and then figure out how to put a nice harness on top of that which will establish a formal component drawing API for Document.

@practicingruby practicingruby deleted the draw-component branch Jul 27, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment