Mathematica: Rasters in 3D graphics

There are times when exporting to a pdf image is simply troublesome. If the data you are plotting contains many points then your figure will be big in size and the pdf viewer of your choice will spend most of its time rendering this high quality image. We can thus export this image as a jpeg, png or tiff. The picture will be fine from a certain view but when you zoom in it will look all distorted. This is fine to some extent for the figure we are plotting but if your image contains text then this text will look pixelated.

In order to try to get the best of both worlds we can separate this figure into two parts: Axes with labels and the 3D picture. The axes can thus be exported as pdf or eps and the 3D figure as a raster. I wish I knew how later combine the two in Mathematica, so for the moment we can use a vector graphics editor such as Inkscape or Illustrator to combine the two.

I managed to achieve this for a plot I made in a publication but this prompt me to create routines in Mathematica in order to automatize this process. Here is what I have so far:

SetDirectory[NotebookDirectory[]];
SetOptions[$FrontEnd, PrintingStyleEnvironment -> "Working"];

I like to start my notebook by setting the working directory to the notebook directory. Since I want my images to be of the size I specify I set the printing style environment to working, check this for more info.

in = 72;
G3D = Graphics3D[
  AlignmentPoint -> Center,
  AspectRatio -> 0.925,
  Axes -> {True, True, True},
  AxesEdge -> {{-1, -1}, {1, -1}, {-1, -1}},
  AxesStyle -> Directive[10, Black],
  BaseStyle -> {FontFamily -> "Arial", FontSize -> 12},
  Boxed -> False,
  BoxRatios -> {3, 3, 1},
  LabelStyle -> Directive[Black],
  ImagePadding -> All,
  ImageSize -> 5 in,
  PlotRange -> All,
  PlotRangePadding -> None,
  TicksStyle -> Directive[10],
  ViewPoint -> {2, -2, 2},
  ViewVertical -> {0, 0, 1}
 ]

Here we set the view of the plot we want to make. Now lets create our plot.

g = Show[
  Plot3D[Sin[x y], {x, 0, Pi}, {y, 0, Pi}, 
   Mesh -> None,
   AxesLabel -> {"x", "y", "z"}
   ], 
  Options[G3D]
 ]

enter image description here

Now we need to find a way of separating. Lets start by drawing the axes.

axes = Graphics3D[{}, AbsoluteOptions[g]]

enter image description here

fig = Show[g, 
  AxesStyle -> Directive[Opacity[0]],
  FaceGrids -> {{-1, 0, 0}, {0, 1, 0}}
 ]

enter image description here

I included the facegrids so that we can match the figure with the axis in the post editing process. Now we export both images.

Export["Axes.pdf", axes];
Export["Fig.pdf", Rasterize[fig, ImageResolution -> 300]];

You will obtain two pdf files which you can edit in and put together into a pdf or eps. I wish it was that simple but it isn't. If you actually did this you will obtain this:

enter image description here

The two figures are different sizes. I know axes.pdf is correct because when I open it in Inkspace the figure size is 5 inches as I had previously specified.

I mentioned before that I managed to get this with one of my plots. I will clean the file and change the plots to make it more accessible for anyone who wants to see that this is in fact true. In any case, does anyone know why I can't get the two pdf files to be the same size? Also, keep in mind that we want to obtain a pretty plot for the Rasterized figure. Thank you for your time.

PS. As a bonus, can we avoid the post editing and simply combine the two figures in mathematica? The rasterized version and the vector graphics version that is.


EDIT:

Thanks to rcollyer for his comment. I'm posting the results of his comment.

One thing to mention is that when we export the axes we need to set Background to None so that we can have a transparent picture.

Export["Axes.pdf", axes, Background -> None];
Export["Fig.pdf", Rasterize[fig, ImageResolution -> 300]];
a = Import["Axes.pdf"];
b = Import["Fig.pdf"];
Show[b, a]

enter image description here

And then, exporting the figure gives the desired effect

Export["FinalFig.pdf", Show[b, a]]

enter image description here

The axes preserve the nice components of vector graphics while the figure is now a Rasterized version of the what we plotted. But the main question still remains. How do you make the two figures match?

UPDATE:

My question has been answered by Alexey Popkov. I would like to thank him for taking the time to look into my problem. The following code is an example for those of you want to use the technique I previously mentioned. Please see Alexey Popkov's answer for useful comments in his code. He managed to make it work in Mathematica 7 and it works even better in Mathematica 8. Here is the result:

SetDirectory[NotebookDirectory[]];
SetOptions[$FrontEnd, PrintingStyleEnvironment -> "Working"];
$HistoryLength = 0;
in = 72;
G3D = Graphics3D[
 AlignmentPoint -> Center, AspectRatio -> 0.925, Axes -> {True, True, True},
 AxesEdge -> {{-1, -1}, {1, -1}, {-1, -1}}, AxesStyle -> Directive[10, Black],
 BaseStyle -> {FontFamily -> "Arial", FontSize -> 12}, Boxed -> False, 
 BoxRatios -> {3, 3, 1}, LabelStyle -> Directive[Black], ImagePadding -> 40,
 ImageSize -> 5 in, PlotRange -> All, PlotRangePadding -> 0,
 TicksStyle -> Directive[10], ViewPoint -> {2, -2, 2}, ViewVertical -> {0, 0, 1}
];
axesLabels = Graphics3D[{
 Text[Style["x axis (units)", Black, 12], Scaled[{.5, -.1, 0}], {0, 0}, {1, -.9}],
 Text[Style["y axis (units)", Black, 12], Scaled[{1.1, .5, 0}], {0, 0}, {1, .9}],
 Text[Style["z axis (units)", Black, 12], Scaled[{0, -.15, .7}], {0, 0}, {-.1, 1.5}]
}];
fig = Show[
  Plot3D[Sin[x y], {x, 0, Pi}, {y, 0, Pi}, Mesh -> None],
  ImagePadding -> {{40, 0}, {15, 0}}, Options[G3D]
];
axes = Show[
  Graphics3D[{}, FaceGrids -> {{-1, 0, 0}, {0, 1, 0}}, 
    AbsoluteOptions[fig]], axesLabels, 
    Epilog -> Text[Style["Panel A", Bold, Black, 12], ImageScaled[{0.075, 0.975}]]
];
fig = Show[fig, AxesStyle -> Directive[Opacity[0]]];
Row[{fig, axes}]

At this point you should see this:

enter image description here

The magnification takes care of the resolution of your image. You should try different values to see how this changes your picture.

fig = Magnify[fig, 5];
fig = Rasterize[fig, Background -> None];

Combine the graphics

axes = First@ImportString[ExportString[axes, "PDF"], "PDF"];
result = Show[axes, Epilog -> Inset[fig, {0, 0}, {0, 0}, ImageDimensions[axes]]];

Export them

Export["Result.pdf", result];
Export["Result.eps", result];

The only difference I found between M7 and M8 using the above code is that M7 does not export the eps file correctly. Other than that everything is working fine now. :)

enter image description here

The first column shows the output obtained from M7. Top is the eps version with file size of 614 kb, bottom is the pdf version with file size of 455 kb. The second column shows the output obtained from M8. Top is the eps version with file size of 643 kb, bottom is the pdf version with file size of 463 kb.

I hope you find this useful. Please check Alexey's answer to see the comments in his code, they will help you avoid pitfalls with Mathematica.


The complete solution for Mathematica 7.0.1: fixing bugs

The code with comments:

(*controls the resolution of rasterized graphics*)
magnification = 5;

SetOptions[$FrontEnd, PrintingStyleEnvironment -> "Working"]
(*Turn off history for saving memory*)
$HistoryLength = 0;
(*Epilog will give us the bounding box of the graphics*)
g1 = Plot3D[Sin[x y], {x, 0, Pi}, {y, 0, Pi}, 
   AlignmentPoint -> Center, AspectRatio -> 0.925, 
   Axes -> {True, True, True}, 
   AxesEdge -> {{-1, -1}, {1, -1}, {-1, -1}}, 
   BaseStyle -> {FontFamily -> "Arial", FontSize -> 12}, 
   Boxed -> False, BoxRatios -> {3, 3, 1}, 
   LabelStyle -> Directive[Black], ImagePadding -> All, 
   ImageSize -> 5*72, PlotRange -> All, PlotRangePadding -> None, 
   TicksStyle -> Directive[10], ViewPoint -> {2, -2, 2}, 
   ViewVertical -> {0, 0, 1}, AxesStyle -> Directive[Opacity[0]], 
   FaceGrids -> {{-1, 0, 0}, {0, 1, 0}}, Mesh -> None, 
   ImagePadding -> 40, 
   Epilog -> {Red, AbsoluteThickness[1], 
     Line[{ImageScaled[{0, 0}], ImageScaled[{0, 1}], 
       ImageScaled[{1, 1}], ImageScaled[{1, 0}], 
       ImageScaled[{0, 0}]}]}];
(*The options list should NOT contain ImagePadding->Full.Even it is \
before ImagePadding->40 it is not replaced by the latter-another bug!*)
axes = Graphics3D[{Opacity[0], 
    Point[PlotRange /. AbsoluteOptions[g1] // Transpose]}, 
   AlignmentPoint -> Center, AspectRatio -> 0.925, 
   Axes -> {True, True, True}, 
   AxesEdge -> {{-1, -1}, {1, -1}, {-1, -1}}, 
   AxesStyle -> Directive[10, Black], 
   BaseStyle -> {FontFamily -> "Arial", FontSize -> 12}, 
   Boxed -> False, BoxRatios -> {3, 3, 1}, 
   LabelStyle -> Directive[Black], ImageSize -> 5*72, 
   PlotRange -> All, PlotRangePadding -> None, 
   TicksStyle -> Directive[10], ViewPoint -> {2, -2, 2}, 
   ViewVertical -> {0, 0, 1}, ImagePadding -> 40, 
   Epilog -> {Red, AbsoluteThickness[1], 
     Line[{ImageScaled[{0, 0}], ImageScaled[{0, 1}], 
       ImageScaled[{1, 1}], ImageScaled[{1, 0}], 
       ImageScaled[{0, 0}]}]}];
(*fixing bug with ImagePadding loosed when specifyed as option in \
Plot3D*)
g1 = AppendTo[g1, ImagePadding -> 40];
(*Increasing ImageSize without damage.Explicit setting for \
ImagePadding is important (due to a bug in behavior of \
ImagePadding->Full)!*)
g1 = Magnify[g1, magnification];
g2 = Rasterize[g1, Background -> None];
(*Fixing bug with non-working option Background->None when graphics \
is Magnifyed*)
g2 = g2 /. {255, 255, 255, 255} -> {0, 0, 0, 0};
(*Fixing bug with icorrect exporting of Ticks in PDF when Graphics3D \
and 2D Raster are combined*)
axes = First@ImportString[ExportString[axes, "PDF"], "PDF"];
(*Getting explicid ImageSize of graphics imported form PDF*)
imageSize = 
 Last@Transpose[{First@#, Last@#} & /@ 
    Sort /@ Transpose@
      First@Cases[axes, 
        Style[{Line[x_]}, ___, RGBColor[1.`, 0.`, 0.`, 1.`], ___] :> 
         x, Infinity]]
(*combining Graphics3D and Graphics*)
result = Show[axes, Epilog -> Inset[g2, {0, 0}, {0, 0}, imageSize]]
Export["C:\\result.pdf", result]

Here is what I see in the Notebook:

screenshot

And here is what I get in the PDF:

screenshot


Just checking (Mma8):

SetOptions[$FrontEnd, PrintingStyleEnvironment -> "Working"];
in = 72;
G3D = Graphics3D[AlignmentPoint -> Center, AspectRatio -> 0.925, 
   Axes -> {True, True, True}, 
   AxesEdge -> {{-1, -1}, {1, -1}, {-1, -1}}, 
   AxesStyle -> Directive[10, Black], 
   BaseStyle -> {FontFamily -> "Arial", FontSize -> 12}, 
   Boxed -> False, BoxRatios -> {3, 3, 1}, 
   LabelStyle -> Directive[Black], ImagePadding -> All, 
   ImageSize -> 5 in, PlotRange -> All, PlotRangePadding -> None, 
   TicksStyle -> Directive[10], ViewPoint -> {2, -2, 2}, 
   ViewVertical -> {0, 0, 1}];
g = Show[Plot3D[Sin[x y], {x, 0, Pi}, {y, 0, Pi}, Mesh -> None, 
    AxesLabel -> {"x", "y", "z"}], Options[G3D]];
axes = Graphics3D[{}, AbsoluteOptions[g]];
fig = Show[g, AxesStyle -> Directive[Opacity[0]], 
   FaceGrids -> {{-1, 0, 0}, {0, 1, 0}}];
Export["c:\\Axes.pdf", axes, Background -> None];
Export["c:\\Fig.pdf", Rasterize[fig, ImageResolution -> 300]];
a = Import["c:\\Axes.pdf"];
b = Import["c:\\Fig.pdf"];
Export["c:\\FinalFig.pdf", Show[b, a]]

enter image description here


In Mathematica 8 the problem may be solved even simpler using new Overlay function.

Here is the code from the UPDATE section of the question:

SetOptions[$FrontEnd, PrintingStyleEnvironment -> "Working"];
$HistoryLength = 0;
in = 72;
G3D = Graphics3D[AlignmentPoint -> Center, AspectRatio -> 0.925, 
   Axes -> {True, True, True}, 
   AxesEdge -> {{-1, -1}, {1, -1}, {-1, -1}}, 
   AxesStyle -> Directive[10, Black], 
   BaseStyle -> {FontFamily -> "Arial", FontSize -> 12}, 
   Boxed -> False, BoxRatios -> {3, 3, 1}, 
   LabelStyle -> Directive[Black], ImagePadding -> 40, 
   ImageSize -> 5 in, PlotRange -> All, PlotRangePadding -> 0, 
   TicksStyle -> Directive[10], ViewPoint -> {2, -2, 2}, 
   ViewVertical -> {0, 0, 1}];
axesLabels = 
  Graphics3D[{Text[Style["x axis (units)", Black, 12], 
     Scaled[{.5, -.1, 0}], {0, 0}, {1, -.9}], 
    Text[Style["y axis (units)", Black, 12], 
     Scaled[{1.1, .5, 0}], {0, 0}, {1, .9}], 
    Text[Style["z axis (units)", Black, 12], 
     Scaled[{0, -.15, .7}], {0, 0}, {-.1, 1.5}]}];
fig = Show[Plot3D[Sin[x y], {x, 0, Pi}, {y, 0, Pi}, Mesh -> None], 
   ImagePadding -> {{40, 0}, {15, 0}}, Options[G3D]];
axes = Show[
   Graphics3D[{}, FaceGrids -> {{-1, 0, 0}, {0, 1, 0}}, 
    AbsoluteOptions[fig]], axesLabels, 
   Epilog -> 
    Text[Style["Panel A", Bold, Black, 12], 
     ImageScaled[{0.075, 0.975}]]];
fig = Show[fig, AxesStyle -> Directive[Opacity[0]]];

And here is the solution:

gr = Overlay[{axes, 
   Rasterize[fig, Background -> None, ImageResolution -> 300]}]
Export["Result.pdf", gr]

In this case we need not to convert fonts to outlines.

UPDATE

As jmlopez pointed out in the comments to this answer, the option Background -> None does not work properly under Mac OS X in Mathematica 8.0.1. One workaround is to replace white non-transparent points by transparent:

gr = Overlay[{axes, 
   Rasterize[fig, Background -> None, 
     ImageResolution -> 300] /. {255, 255, 255, 255} -> {0, 0, 0, 0}}]
Export["Result.pdf", gr]

Here I present another version of the original solution which uses the second argument of Raster instead of Inset. I think that this way is a little more straightforward.

Here is the code from the UPDATE section of the question (modified a bit):

SetOptions[$FrontEnd, PrintingStyleEnvironment -> "Working"];
$HistoryLength = 0;
in = 72;
G3D = Graphics3D[AlignmentPoint -> Center, AspectRatio -> 0.925, 
   Axes -> {True, True, True}, 
   AxesEdge -> {{-1, -1}, {1, -1}, {-1, -1}}, 
   AxesStyle -> Directive[10, Black], 
   BaseStyle -> {FontFamily -> "Arial", FontSize -> 12}, 
   Boxed -> False, BoxRatios -> {3, 3, 1}, 
   LabelStyle -> Directive[Black], ImagePadding -> 40, 
   ImageSize -> 5 in, PlotRange -> All, PlotRangePadding -> 0, 
   TicksStyle -> Directive[10], ViewPoint -> {2, -2, 2}, 
   ViewVertical -> {0, 0, 1}];
axesLabels = 
  Graphics3D[{Text[Style["x axis (units)", Black, 12], 
     Scaled[{.5, -.1, 0}], {0, 0}, {1, -.9}], 
    Text[Style["y axis (units)", Black, 12], 
     Scaled[{1.1, .5, 0}], {0, 0}, {1, .9}], 
    Text[Style["z axis (units)", Black, 12], 
     Scaled[{0, -.15, .7}], {0, 0}, {-.1, 1.5}]}];
fig = Show[Plot3D[Sin[x y], {x, 0, Pi}, {y, 0, Pi}, Mesh -> None], 
   ImagePadding -> {{40, 0}, {15, 0}}, Options[G3D]];
axes = Show[
   Graphics3D[{}, FaceGrids -> {{-1, 0, 0}, {0, 1, 0}}, 
    AbsoluteOptions[fig]], axesLabels, 
   Prolog -> 
    Text[Style["Panel A", Bold, Black, 12], 
     ImageScaled[{0.075, 0.975}]]];
fig = Show[fig, AxesStyle -> Directive[Opacity[0]]];
fig = Magnify[fig, 5];
fig = Rasterize[fig, Background -> None];
axes2D = First@ImportString[ExportString[axes, "PDF"], "PDF"];

The rest of the answer is the new solution.

At first, we set the second argument of Raster so that it will fill the complete PlotRange of axes2D. The general way to do this is:

fig = fig /. 
   Raster[data_, rectangle_, opts___] :> 
    Raster[data, {Scaled[{0, 0}], Scaled[{1, 1}]}, opts];

Another way is to make direct assignment to the corresponding Part of the original expression:

fig[[1, 2]] = {Scaled[{0, 0}], Scaled[{1, 1}]}

Note that this last code is based on the knowledge of internal structure of the expression generated by Rasterize which is potentially version-dependent.

Now we combine two graphical objects in a very straightforward way:

result = Show[axes2D, fig]

And export the result:

Export["C:/Result.pdf", result];
Export["C:/Result.eps", result];

Both .eps and .pdf are exported perfectly with Mathematica 8.0.4 under Windows XP 32 bit and look identical to the files exported with the original code:

result = Show[axes2D, 
  Epilog -> Inset[fig, Center, Center, ImageScaled[{1, 1}]]]
Export["C:/Result.pdf", result];
Export["C:/Result.eps", result];

Note that we need not necessarily to convert axes to outlines at least when exporting to PDF. The code

result = Show[axes, 
  Epilog -> Inset[fig, Center, Center, ImageScaled[{1, 1}]]]
Export["C:/Result.pdf", result];

and the code

fig[[1, 2]] = {ImageScaled[{0, 0}], ImageScaled[{1, 1}]};
result = Show[axes, Epilog -> First@fig]
Export["C:/Result.pdf", result];

produce PDF files looking identical to both previous versions.