How to place an image inside a notebook, with the minimum memory footprint?

The technique is mentioned in one of Wolfram CDF virtual conference talks (See the course: Developing Real-World CDF Applications), as well as being used in a lot of CDF examples (for instance, the slideshow at the beginning of this example) but I will repeat it here, with some improvement.

Recompressing images with better compression

(Note: While writing an answer, Rojo already provided the same method.)

As it is mentioned by other users here, you don't have to manually use Compress since images are already being compressed using it when being stored inside a notebook cell. Compress is based on gzip and it is overall OK, except that it can be pretty horrible with images.

Let's try to use other compression, for instance PNG (lossless) or JPEG (lossy) (or anything for that matter). Let's say our image is img. I am grabbing an image from the Hubble Gallery as an example.

url = "http://imgsrc.hubblesite.org/hu/db/images/hs-2006-01-a-1920x1200_wallpaper.jpg";

img = Import[url];

This is a pretty big image.

In[3]:= ByteCount[img]
Out[3]= 6912464

Now, compress it using the compression of your choice and store it as string using ExportString.

jpeg = ExportString[img, "JPEG"];

Of course, you can use any other compression (for instance, "PNG" or "GIF") or control compression rate for JPEG by using "CompressionLevel" option (default is 0.25).

To embedded this data in a cell, it has to be converted to Base64 (essentially using non-special characters).

base64 = ExportString[jpeg, "Base64"]

This is a slightly larger than actual binary JPEG, but still far smaller than uncompressed size, or compressed size.

In[6]:= ByteCount[base64]
Out[6]= 219224

(To be fair, Mathematica will save it using aforementioned Compress and the size will be smaller than what ByteCount[img] is reporting, but not this small).

By using ImportString, you can convert it back to the image:

ImportString[base64, "Base64"]

(You don't have to call ImportString again for the compression, since it will be automatically taken care of)

To embedded the raw data (jpeg) and let FrontEnd uncompress it during the time of reading a notebook, try the following code.

With[{a = base64}, Dynamic[ImportString[a, "Base64"], SingleEvaluation->True]]

or

With[{a = base64}, Dynamic[Refresh[ImportString[a, "Base64"], None]]]

The extra option (SingleEvaluation) and Refresh are used to make sure that it is evaluated just once. Also, With is needed to ensure that the embedded cell contains the content of base64, instead of a symbol base64. This can play nicely with other cells, such as texts and other graphics, using Row or other constructions.

Now, let's try to save it. First try:

nb = CreateDocument[With[{a = base64},
  Dynamic[ImportString[a, "Base64"], SingleEvaluation -> True]]];

NotebookSave[nb, "jpeg.nb"]

Try to check the size of the file, and you will be surprised...

size

It is smaller than a notebook with just the image (which will be around 5MB), but still not quite as smaller as we expected it to be. What's wrong? It is because Mathematica caches images by default. Let's disable it (CacheGraphics->False will do it. You can set it using the Option Inspector too).

nb2 = CreateDocument[With[{a = base64},
  Dynamic[ImportString[a, "Base64"], SingleEvaluation -> True]], CacheGraphics->False];

NotebookSave[nb2, "jpeg2.nb"]

Now, much reasonable:

size2

Using native compression of imported image

Sometimes, your image is coming from external source with its own native compression. The problem with the first approach is that it essentially uncompress it then recompress using different compression / rate (sort of transcoding...). In particularly with lossy compression like JPEG, it may leads to image distortion. To minimize this, you can do the following.

(Note: Szabolcs provided a nice solution using Import. Thank you)

We can read the file's native binary data by caling Import[..., "String"].

jpeg = Import[url, "String"];

First, the image has to be in local storage. Bring its native binary data using BinaryReadList. Also, use "Character8" and StringJoin so that it will turn into the string of binary data.

jpeg = StringJoin@@BinaryReadList["hs-2006-01-a-1920x1200_wallpaper.jpg", "Character8"];

It should be about the same size as the original file (+/- some due to the string representation in Mathematica). From here, you can follow the above steps to turn it into "Base64" and then embed.

Reading image from online

If you have a lot of images, then this can be applicable.

Dynamic[Refresh[Import[url], None]]

If it times out, you can increase the timeout using DynamicEvaluationTimeout or try the following to show a nice indicator while loading.

DynamicModule[{img = None}, 
 Dynamic[If[img === None, 
   ProgressIndicator[Clock[Infinity], Indeterminate], img], 
  TrackedSymbols :> {}],
 Initialization :> (img = 
    Import["http://imgsrc.hubblesite.org/hu/db/images/hs-2006-01-a-1920x1200_wallpaper.jpg"]),
 SynchronousInitialization -> False]

This course notebook contains some useful examples including a spinner for progress and such.

Also, these techniques can be used to embed any large data, such as MOV file!


There is something you can do. I just tried it with a JPEG image that I pasted into a notebook. The original image had 900kB and the notebook with only that image had 11.2 MB!

Let's say you have an image in an output cell of your notebook, then select the cell bracket, go to Cell > Convert To... > Bitmap and the resulting notebook will be much smaller.

In my test case, the result was 766kB, i.e., less than the original image. The size reduction is not due to a different compression, but it reduces the size of the pasted graphic to a size appropriate for the notebook display.

Edit

Mathematica's compression format doesn't seem to be very efficient, even compared to the ubiquitous Base64 format. Just for fun, I compared the sizes of some compressed images - here is an example:

a = Import["ExampleData/rose.gif"];
pic = ExportString[a, "GIF"];
string1 = Compress[pic];
string2 = ExportString[pic, "Base64"];

StringLength[string1]
(* ==> 28114 *)

StringLength[string2]
(* ==> 21406 *)

The last result shows that Base64 encoding is more efficient for this GIF image. To reconstruct the base64-encoded image, one would say ImportString[string2, "Base64"].

Unfortunately, I currently don't know of a way to make use of this observation for the purposes of actually displaying images...

Edit 2

Rojo made an interesting suggestion that relies on Dynamic (and which could therefore be slow if many images need to appear in the visible part of the notebook at the same time). It would work basically as if the notebook is a web page that loads its images from the disk:

Assume you have an image image.jpg in the working directory of your notebook, then the single line

Dynamic[Refresh[Import["~/Pictures/image.jpg"], None]]

will display the image in its original form when it's executed. You could make this cell an initialization cell to make sure it does its job of loading the image. Addendum: However, if (as above) I include the absolute path to the image in the Import statement, the notebook displays the image automatically as soon as you've permitted Dynamic to be enabled.

The downside is that the image is not stored inside the notebook as the original question demands.

However, the upside is that the notebook is extremely light-weight.

Edit 3

Since Yu-Sung Chang mentions in his answer that this method can also be used to embed external Quicktime movies, I tried it and was indeed successful with the following command:

Style[Dynamic[
  Refresh[Import[
    "http://pages.uoregon.edu/noeckel/computernotes/StopMotion.mov", 
    "Animation"], None]], DynamicEvaluationTimeout -> 60]

This example uses a remote URL, but the same also works with local URLs. The setting DynamicEvaluationTimeout -> 60 is added as a Style directive; without it, the command aborts.

You can copy the resulting output cell (or similarly the one from the previous image example) by selecting the cell bracket first. What you're really copying and saving can be illustrated by selecting the output cell and doing Cell > Show Expression...:

Cell[BoxData[
 StyleBox[
  DynamicBox[ToBoxes[
    Refresh[
     Import[
     "http://pages.uoregon.edu/noeckel/computernotes/StopMotion.mov",
     "Animation"], None], StandardForm],
   ImageSizeCache->{589., {265., 273.}}],
  StripOnInput->False,
  DynamicEvaluationTimeout->60]], "Output",
 CellChangeTimes->{
  3.547335674114159*^9, {3.5473357563388443`*^9, 
   3.547335785166815*^9}, {3.54733584467654*^9, 
   3.5473358540852947`*^9}, 3.547335895905892*^9, 
   3.5473365158725147`*^9, 3.547336769935759*^9}]

There is no actual movie data in this cell, and that's why the notebook is extremely small when saved to a file.


If you open a cell containing a photo with ctrlshift-E, you'll see something like:

Mathematica graphics Mathematica graphics

So, internally there's already something compressed going on. Problem is, Mathematica uses a compression schema where the end result only contains printable characters, so this is slightly less efficient than a fully binary format.

I don't think Mathematica offers the possibility of an indexed color space.

[torn paper by Heike]