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

When creating a notebook with images, like photos, the file tends to quickly get big, which makes it difficult to use the notebook format for training material containing a lot of images.

Are there different ways of passing an image into a notebook, in what concerns memory used?

(please note that the purpose is to show the images in the “middle” of other content, like text, etc, and not simply have the image information in some kind of abstract code)

I've noticed that every time I copy an image to a notebook, its FullForm reveals that it is "stored" with an "exhaustive" description of each pixel color value(s).

• Is there a way of storing it with indexed color definition (like the GIF format)?
• Is there a way of storing it with some kind of compression (like JPG or similar)?

EDIT:

Meanwhile I discovered the following curiosity: if I Import a gif file (of my printscreen), and then save it, my file gets to 150 kb. If I save it again (without changing anything), it gets reduced to 32 kb.

If instead of Importing it, I just copy it from the clipboard to the notebook, it always keeps the 150 kb.

-
Is the issue big memory footprint when opening the notebook, or large file size? I might have something for large file size, but not for large memory usage... –  Szabolcs May 29 '12 at 9:39
@Szabolcs large file size (which also ends up in a long time to open) –  P. Fonseca May 29 '12 at 11:37
My answer should help the large file size, but not the time to open ... –  Szabolcs May 29 '12 at 11:44
Thanks for the question, P.Fonseca - I think we're learning some new things here. –  Jens May 29 '12 at 20:08
@Jen Thank you all for the contribution :) –  P. Fonseca May 30 '12 at 6:58

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

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...

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:

## 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.

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!

-
Thanks for the additional links (+1) - looks like we figured out many of these things independently, but your explanations make me more confident that we're not abusing Dynamic too badly, if Wolfram suggests it for the same purpose. –  Jens May 30 '12 at 0:21
Instead of BinaryReadList and StringJoin, could we use Import[..., "String"] instead, or are there any differences? Import[..., "String"] is very fast in my experience, so performance shouldn't be an issue. –  Szabolcs May 30 '12 at 7:04
If you're interested, this topic would make a nice blog post. –  Szabolcs May 30 '12 at 7:06
One problem with this technique is that one must take care when copying the images around. If I click the image to select it (with the orange frame and resize grips around it), and copy to another notebook, the actual large size image will be copied. If I select the image by clicking and dragging (the same way we usually select text), then the whole DynamicBox will be copied. Is there any trick to prevent the user from accidentally copying the image data only, and losing the DynamicBox? We could use Deploy around Dynamic, but then we lose resizing. –  Szabolcs May 30 '12 at 7:13
@Szabolcs, you are right about Import[..., "String"]. It was an old habit. I modified the post. Thanks. –  Yu-Sung Chang May 30 '12 at 13:25

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.

-
An idea I haven't tried... What about something like Dynamic[Refresh[ImportString[string2, "Base64"], None]]? –  Rojo May 28 '12 at 23:59
@Rojo That idea points to a completely different approach: first, I thought this won't help because the string2 that you want to display in Dynamic has to be generated from an image anyway, so you still have to waste the memory for the original image. But then it becomes clear that we could just do Dynamic[Refresh[Import["image.jpg"], None]] with image.jpg nicely compressed somewhere on the hard drive. All one needs to insure is that you load from the correct directory. That actually works very well. –  Jens May 29 '12 at 0:39
That last one with Import is a nice idea. The other one has the extra that it stores everything in the notebook. And you only need the image when you run, e.g, With[{string2=ExportString[ExportString[<<img>>, "GIF"], "Base64"]}, Dynamic[Refresh[ImportString[string2, "Base64"], None]]]. After you run that, you can delete your original image and stick with the output, right? –  Rojo May 29 '12 at 0:48
In any case, the Import["image seems to be very interesting indeed. I don't think you need to keep the cell or make it an initialization cell –  Rojo May 29 '12 at 0:50
@Rojo Yes, your suggestion with ImportString does indeed reduce the file size of the notebook significantly once I delete the original image. With my test photo of 900kB I get the size of the notebook down to 5MB from 11MB! But with the Import of the external image I end up with a total size of 900kB because the notebook is essentially zero size! –  Jens May 29 '12 at 0:58

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

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]

-
I guess that was devised so you can transfer the .nb by using Baudot encoding –  belisarius May 28 '12 at 20:55
@belisarius Don't think so, as Baudot coding doesn't use lowercase characters. –  Sjoerd C. de Vries May 28 '12 at 21:01
@belisarius It's amusing to compare the outputs of string1=Compress["Hello"] and string2=ExportString["Hello", "Base64"]. The compressed string2 is a quarter the length than what Wolfram uses in string1`... Why didn't they go with base64 encoding? –  Jens May 28 '12 at 21:12
@Jens Try the string "HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello" instead and you see Compress takes much less space. Compression of small files isn't normally efficient. –  Sjoerd C. de Vries May 28 '12 at 21:22
Base64 is not a compression scheme. It is an encoding scheme to ensure that output is within readable ASCII characters (by grouping three 8 bits to four 7 bits)--to avoid escape chars and high-bit chars. You can see it all the time in usenet binary groups (since it only serves pure texts). Compress[] already returns a result in Base64. You can roughly think Compress[] as ExportString[ExportString[..., "GZIP"], "Base64"] (roughly...). –  Yu-Sung Chang May 30 '12 at 1:01