Prevent graphics from rendering inside a held expression

Recall that the rendering of Graphics has nothing to do with evaluation. It is done entirely in typesetting. And therefore, a robust solution will treat this as a problem of typesetting, and not as a problem of evaluation.

Once you frame the problem properly, the solution is fairly straightforward. What you want to do is to change the typesetting of Hold (and friends). Take a look at this:

Unprotect[Hold];
Hold /: MakeBoxes[Hold[expr_], fmt : StandardForm | TraditionalForm] :=
  Block[{Graphics, Graphics3D}, Unprotect[Graphics, Graphics3D]; 
  Clear[Graphics, Graphics3D]; 
  RowBox[{"Hold", "[", MakeBoxes[expr, fmt], "]"}]]
Protect[Hold]

Fortunately, Hold (and HoldForm and HoldComplete) has no typesetting rules directly attached to it that you're fighting, which you can determine using FormatValues[Hold]. But Graphics and Graphics3D do; it's how typesetting of graphics works at all. We want to suppress those rules, but only within the typesetting of Hold. So we use Block to contain the damage we're about to do to the Graphics and Graphics3D symbols, and then use Clear to clear them. From there on out, we let MakeBoxes do what it would normally do.

Note that this example cheats a bit; it only works if you pass one argument to Hold. I did that for purpose of code simplicity and illustration. To make the formatting rules work properly for Hold[expr___], I would have to write multiple and more sophisticated rules, or I would have to use the Villegas-Gayley trick.


Edit: As came up in the comment discussion, it really isn't necessary to Unprotect and Clear the symbols Graphics and Graphics3D, as Block is effectively doing that already. I've considered editing the code to make it shorter/simpler, but perhaps the existing code is clearer for people who don't fully understand how Block works (and, public confession here, while I understand Block scoping, I had just plumb forgotten how Block initializes variables, so this more an oversight on my part than a planned teaching moment).


John Fultz alluded to using the Villegas-Gayley pattern. Since I believe that is the correct approach to this problem here is an implementation.

mk : MakeBoxes[(Hold | HoldForm | HoldComplete | HoldPattern)[__], _] :=
  Block[{$hldGfx = True, Graphics, Graphics3D}, mk] /; ! TrueQ[$hldGfx]

I included HoldPattern to complete the Hold functions. This now works at any depth:

Hold[1, 2, foo[3, Graphics[{Green, Circle[]}], 4], 5]
Hold[1, 2, foo[3, Graphics[{Green, Circle[]}], 4], 5]

Overhead

Jacob Akkerboom questioned the overhead of this general rule attached to MakeBoxes. To test this I converted a large expression to Box form using ToBoxes (which calls MakeBoxes), and timed the operation with and without this definition as well as several alternatives. Here are the results (in version 7). Each test was performed in a fresh kernel, using this code:

expr = Expand[(1 + x + y)^4 (2 - z)^5 (q - 7 - a)^7 (b + r - 4)^6];
ToBoxes[expr] // AbsoluteTiming // First
  • Raw (no additional MakeBoxes rules): 0.7940454
  • Alternatives DownValue on MakeBoxes: 1.1030631
  • Four individual DownValues on MakeBoxes: 1.4690841
  • Four UpValues on Hold* functions: 0.7890451

(Incidentally, use of the Notations Package causes far larger overhead; the timing performed in my standard configuration was 3.5652039 seconds.)

It appears that Jacob's concern is valid as the overhead of my method above is significant, though not extreme. I usually attach rules to MakeBoxes to avoid having to Unprotect system Symbols but I may have to reconsider that practice. If you prefer unprotecting system Symbols to the overhead you may use this:

(
  Unprotect @ #;
  mk : MakeBoxes[Blank[#], _] /; ! TrueQ[$hldGfx] ^:= 
    Block[{$hldGfx = True, Graphics, Graphics3D}, mk];
  Protect @ #
) & ~Scan~ {Hold, HoldForm, HoldComplete, HoldPattern}

This is not an answer but an extended comment.

Regarding your assertion

This occurs because although the front end attempts to render the Graphics element the internal code won't replace Directives inside of a Held expression.

This is not the case. Consider

Hold[Graphics[{RGBColor[1, 0, 0], Thick, Circle[]}]]

thickred.png

and

 With[{red = Red}, Hold[Graphics[{red, Circle[]}]]]

withred.png

So the error message comes from using the built-in symbol Red and not from the front end doing anything funny with directives.