Attaching a file to a notebook

Is there a way to attach a file into a notebook and open it later with, for instance, a double click (or another action button)?

In Mathematica windows version, the insert menu has the "object..." entry and I can indeed embed an object into my notebook. But then, I don't know how to exploit it in an attachment kind of way. For instance, in MS Word, if we "insert an object", we can then open it by double clicking its icon: windows opens the file with its predefined application.

For the purpose of the discussion, let’s suppose I want to attach a pdf file.

If this functionality isn't native on Mathematica, probably a technique based on this post can be used, with an intermediate step of export to a temporary folder, and then a Run kind of command...

I can also imagine a Dynamic interface, with a list of attached files (whose internal data/content is “kept” by a DynamicModule internal variable), and four buttons: add, delete, export and open. Since a Dynamic cell can be easily copied from a notebook to another, I could easily use this small embedding app on different notebooks.

There’s probably another button that would be useful: import. This would make the file content available, as a string, on a global context variable, or at least export the file to a temporary folder, and make its path available on a global context variable.

(if you go the "dynamic app" way, please consider, from the beginning, app conflicts if two apps are added to the same notebook; and a more tricky, or probably impossible task, to make some of its functionality work on the Player)

EDIT - 2014-01-12

(there was an error with the OPEN, that has now been corrected)

Current version:

Any help on making it better is appreciated, specially in the domain of the compression ratio and safety (currently using GZIP).

Future developments (if anyone can help):

• attach the notebook itself without passing through files (poor man self-included versioning)
• attach a Save of a specific variable/definition or a list of them, without passing through files.
• attach a Save of the entire session, to record current state of sessions (kind of persistent memory)
• load stored Saves (with the option of overlaying the existing memory, substitute completely the existing memory (clearing other definitions), adding definitions with an indexed definition name or or an indexed context, so to allow comparisons...)
• export recorded files to variables, so to work on them (ImportString, etc) without passing through files
• and any other crazy infeasible idea...

EDIT - 2014-02-09

It is now possible to save the definitions of the current context. It is very rough, and I would greatly appreciate some help to improve it. Things that are not great:

• it stores its own definition, which could probably be avoidable

• something better should be thought for the problem of creating a new context registry when an old one is opened

• why only the current context, and not a checkbox tree list where one checks contexts and/or definitions that would like to be stored

• no warning for very large content being saved

-
Do you mean something like PDFs with attached files? Such a container format could be handy indeed. – Yves Klett Oct 11 '12 at 9:01
@YvesKlett I mean a notebook that has pdf (or other format file/content) attached and retrievable in different ways (a dynamic cell acting as a file folder... can compress be an option for each file?) – P. Fonseca Oct 11 '12 at 9:07
I was not concise: PDFs can contain other files as attachment - and you can generate such from Mathematica (see bottom of help page). What you want is the other way round (and not for PDFs only), right? I agree that would be a useful feature. – Yves Klett Oct 11 '12 at 9:25
@YvesKlett that's right. Useful and probably not very difficult to create (but I believe that there are people here much more at ease with the needed skills than I am, and that will probably lose 1/20th of the time to and get a more general bug free application than I could...) – P. Fonseca Oct 11 '12 at 9:36
It looks like you have all the basic elements already thought out. What's holding you back? – Sjoerd C. de Vries Oct 11 '12 at 15:29

I believe the following program will do all you asked for. It will generate this little grid of buttons:

You can use the "Add file" button as many times as you want to add as many files you want. Those files are stored in the notebook that contains this button grid, so you can copy the grid to an empty notebook and use the files without the need to execute any code. The other buttons do what you intended. You get a dialog to determine the specific internal file to export, open or delete.

DynamicModule[{files, fileNames, selectedFile, fileChosen, fileName,
tempFile, fileSelectDialog, afButton, dfButton, efButton, ofButton},

files = {};
fileNames = {};

fileSelectDialog[] :=
If[fileNames === {},
selectedFile = $Canceled, (*else*) selectedFile = First@fileNames; DialogInput[ Column[ { TextCell["Select File:"], PopupMenu[Dynamic[selectedFile], fileNames], "", Row[{CancelButton[], " ", DefaultButton[DialogReturn@selectedFile]}] } ] ] ]; afButton[] := Button["Add file", fileChosen = SystemDialogInput["FileOpen"]; If[fileChosen =!=$Canceled,
fileName = FileNameTake@fileChosen;
AppendTo[files, Compress@Import[fileChosen, "Byte"]];
AppendTo[fileNames, fileName];
];,
Method -> "Queued"
];

dfButton[] :=
Button["Delete file",
fileChosen = fileSelectDialog[];
If[fileChosen =!= $Canceled && fileNames != {}, files = Delete[files, First@First@Position[fileNames, fileChosen]]; fileNames = DeleteCases[fileNames, fileChosen, 1, 1], (*else*) DialogInput[ DialogNotebook[{TextCell["Nothing to delete"], Button["Proceed", DialogReturn[1]]}]]; ]; , Method -> "Queued" ]; efButton[] := Button["Export file", fileChosen = fileSelectDialog[]; If[fileChosen =!=$Canceled && fileNames != {},
If[fileName =!= $Canceled, Export[fileName, Uncompress@First@Pick[files, fileNames, fileChosen], "Byte"] ], (*else*) DialogInput[ DialogNotebook[{TextCell["Nothing to export"], Button["Proceed", DialogReturn[]]}]]; ];, Method -> "Queued" ]; ofButton[] := Button["Open file", fileChosen = fileSelectDialog[]; If[fileChosen =!=$Canceled && fileNames != {},
tempFile = FileNameJoin[{\$TemporaryDirectory, fileChosen}];
SystemOpen@
Export[tempFile, Uncompress@First@Pick[files, fileNames, fileChosen], "Byte"],
(*else*)
DialogNotebook[{TextCell["Nothing to open"],
Button["Proceed", DialogReturn[]]}]];
];,
Method -> "Queued"
];

Manipulate[
Grid[{{afButton[], dfButton[]}, {efButton[], ofButton[]}}],
SaveDefinitions -> True, TrackedSymbols -> {}
]
]

-
Works fine! Please see my edit for a different interface approach. Also, the notebook file size gets roughly the double the size of the attached files. Is there something we can do about it? Is it possible for the save as to keep the original file name as default? – P. Fonseca Oct 11 '12 at 21:19
@P.Fonseca Added a default name for save. File size will be mainly caused by the inefficiency of Compress I assume. Not much you can do if you want to code binary files as ASCII. You could try if Base64 coding helps for compressed files. – Sjoerd C. de Vries Oct 12 '12 at 21:00
This is a wonderful substitute for using a file:// hyperlink in that you don't have to remember to put the target file in a specified location. Of course you have to worry about the notebook getting too big when the added files are large. – murray Oct 12 '12 at 22:06
@murray If you'd keep the files along the notebook in the same directory you could use NotebookDirectory[] to address them. So, I'm not too sure about the advantages of my own solution. Yes, it means having to drag around only a single file instead of a directory, but is the latter so difficult? – Sjoerd C. de Vries Oct 12 '12 at 22:56

This is a bit crude, but seems to work so far: First you get the data into your notebook file via filedata = Import["kitten.jpg", "RawData"];. With this raw data you now have, you create a Base64 representation:

base64data = ExportString[filedata, "Base64"]

(this is the same algorithm which is used in emails for representing binary data as alphanumeric-only characters - on the cost that it takes about 1.5x the size of the original file.) This data you then put in a cell and write myFileData = before it. Then mark the cell as initialisation cell.

Button["Write and open file",
BinaryWrite["newkitten.jpg",
ToExpression[ImportString[myFileData, "Base64"]]];
SystemOpen["newkitten.jpg"]
]


The button will then create a new file in the current working directory and open it. If anyone has a few suggestions how to streamline the process of getting the Base64 data permanently saved in the notebook, please let me know.

-
Concerning your last question, please take a look at the post I linked on my question. – P. Fonseca Oct 11 '12 at 13:39