53

Here's a challenge to all mathematica tag followers. Let's make it a lot more convenient to insert images into SO post from Mathematica by creating an imgur uploader.

How can we create a function imgur[g_] that will rasterize its argument (making sure that the final size is not wider than the width of StackOverflow posts), convert it to PNG, upload it to imgur, and return a ready to be pasted MarkDown line such as ![Mathematica graphic](http://i.imgur.com/ZENa4.jpg) ?

Useful references:

I failed to adapt this latter method to uploading an image without exporting it to a file first.


Warning, use with care! StackOverflow uses a separate imgur installation that keep images indefinitely. If you use the main imgur, the images will disappear after 6 months if no one views them. Unfortunately as of 2011 November there seems to be no official way to upload images to StackOverflow programmatically.


Update: See below a solution for uploading to StackOverflow directly.

Community
  • 1
  • 1
Szabolcs
  • 21,860
  • 5
  • 72
  • 158
  • 2
    Please note that uploading to `http://i.stack.imgur.com/` is more difficult (you'll have to "drive" the SO interface) – Dr. belisarius Nov 07 '11 at 17:02
  • @belisarius Oops, I didn't realize that StackOverflow uses a separate imgur site ... On the main imgur site the images might not be kept forever, so perhaps it's not a good idea to use it for SO http://imgur.com/faq#long (at least 1 view / 6 months is needed for them to be kept) – Szabolcs Nov 07 '11 at 17:13
  • 4
    Posting graphics manually isn't really that hard (in V8). Right mouse a graphic, choose "Save Image as...". Then the file dialog opens where it was last time, which usually is my desktop where a file called output.png is already waiting to be overwritten by its next incarnation. Two clicks is all it takes, two more clicks and its posted in my SO answer box. Takes 15 seconds at most. Getting the Markdown line and pasting that will take about the same time. – Sjoerd C. de Vries Nov 07 '11 at 22:22
  • @Sjoerd When posting in the image-processing tag, I have to repeat that again and again ... – Dr. belisarius Nov 07 '11 at 23:06
  • @belisarius But with the proposed solution you would have to type `imgur[g]` for every figure you want to include too, plus copying and pasting the markdown text. There really doesn't seem to be much of a difference in terms of effort. – Sjoerd C. de Vries Nov 07 '11 at 23:12
  • @Sjoerd Yes, but this is only the first step of another story. The idea is in the comments here http://stackoverflow.com/questions/8034298/paste-mathematica-code-so-thats-its-broken-into-separate-input-cells/8034649#8034649 – Dr. belisarius Nov 07 '11 at 23:39
  • @Sjoerd One can also make it into a palette and make it a one-click process – Szabolcs Nov 08 '11 at 06:12
  • @belisarius Unfortunately there's no official way to upload to `stack.imgur.com` at the moment. See here: http://stackapps.com/questions/2664/is-there-an-api-to-upload-images-to-ses-imgur-installation/2667#2667 – Szabolcs Nov 09 '11 at 16:36

3 Answers3

16

A little bird just informed me of a Mathematica solution to this question (the underlying implementation still uses JLink, but this answer hides all the java related code):

imgur[expr_] := Module[
 {url, key, image, data, xml, imgurUrl},
 url = "http://api.imgur.com/2/upload";
 key = "c07bc3fb59ef878d5e23a0c4972fbb29";
 image = Fold[ExportString, expr, {"PNG", "Base64"}];
 xml = Import[url, 
  "XML", "RequestMethod" -> "POST", 
  "RequestParameters" -> {"key" -> key, "image" -> image}];
 imgurUrl = Cases[xml, XMLElement["original", {}, {string_}] :> string, 
  Infinity][[1]];
 "![Mathematica graphic](" <> imgurUrl <> ")"
]

This is V8 only and the XML import options "RequestMethod" and "RequestParameters" are undocumented and experimental (and therefore subject to change).

Arnoud Buzing
  • 15,177
  • 3
  • 18
  • 50
  • Thank you for sharing this Arnoud! Unfortunately this doesn't seem to work when trying to upload to StackOverflow (using the method I mentioned in a comment on your other answer). I think the problem is that the image must be submitted as [multiplart/form-data](http://en.wikipedia.org/wiki/Multipart/form-data#Form_Data) Do you know how to do this? Also, do you have any ideas on how to make this into a palette button that will upload the selection (after previewing it)? See my other answer for what I tried and how it didn't work (the too narrow palette width is used for rasterization). – Szabolcs Dec 01 '11 at 20:19
  • +1: Very nice! Now I'm trying to get this to work with OAuth so I can upload images directly to my account albums and such :). Also how did you find those undocumented options? – nixeagle Dec 14 '11 at 21:37
  • @nixeagle If you get that working, I woulnd't mind if you posted the code (with explanations of the challenges and how you solved them) as an additional answer here. – Szabolcs Dec 20 '11 at 14:50
13

Note: Get an ready-made palette with this functionality here.


Arnoud's solution got me excited and impatient, so here's an improvement to it. I couldn't have done this without studying his code. This version seems to be somewhat more reliable and less prone to timeout errors, but to be honest, I know no Java at all, so any improvements are welcome.

Most importantly: this version uploads to stack.imgur.com directly, so it's safe to use here on StackOverflow, without having to worry that uploaded images will disappear after a while.

I provide three functions:

  • stackImage uploads the expression, exported as PNG, and returns the URL
  • stackMarkdown returns the markdown, ready to be copied
  • stackCopyMarkdown copies the markdown to the clipboard

Next step: create a palette button that does this automatically for the selected graphic in the notebook. Improvements to the code are very welcome.


Needs["JLink`"]


stackImage::httperr = "Server returned respose code: `1`";
stackImage::err = "Server returner error: `1`";

stackImage[g_] :=
 Module[
  {getVal, url, client, method, data, partSource, part, entity, code, 
   response, error, result},

  (* this function attempts to parse the response fro the SO server *)
  getVal[res_, key_String] :=
   With[{k = "var " <> key <> " = "},
    StringTrim[
     First@StringCases[First@Select[res, StringMatchQ[#, k ~~ ___] &], 
       k ~~ v___ ~~ ";" :> v],
     "'"]
    ];

  data = ExportString[g, "PNG"];

  JavaBlock[
    url = "https://stackoverflow.com/upload/image";
    client = JavaNew["org.apache.commons.httpclient.HttpClient"];
    method = JavaNew["org.apache.commons.httpclient.methods.PostMethod", url];
    partSource = JavaNew["org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource", "mmagraphics.png", MakeJavaObject[data]@toCharArray[]];
    part = JavaNew["org.apache.commons.httpclient.methods.multipart.FilePart", "name", partSource];
    part@setContentType["image/png"];
    entity = JavaNew["org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity", {part}, method@getParams[]];
    method@setRequestEntity[entity];
    code = client@executeMethod[method];
    response = method@getResponseBodyAsString[];
  ]

  If[code =!= 200, Message[stackImage::httperr, code]; Return[$Failed]];
  response = StringTrim /@ StringSplit[response, "\n"];

  error = getVal[response, "error"];
  result = getVal[response, "result"];
  If[StringMatchQ[result, "http*"],
   result,
   Message[stackImage::err, error]; $Failed]
  ]


stackMarkdown[g_] := "![Mathematica graphics](" <> stackImage[g] <> ")"


stackCopyMarkdown[g_] := Module[{nb, markdown},
  markdown = Check[stackMarkdown[g], $Failed];
  If[markdown =!= $Failed,
   nb = NotebookCreate[Visible -> False];
   NotebookWrite[nb, Cell[markdown, "Text"]];
   SelectionMove[nb, All, Notebook];
   FrontEndTokenExecute[nb, "Copy"];
   NotebookClose[nb];
   ]
  ]

Update:

Here's a button that will show a preview of the selection and will offer uploading (or cancelling). It requires the previous functions to be defined.

Button["Upload to SO",
 Module[{cell = NotebookRead@InputNotebook[], img},
  If[cell =!= {}, img = Rasterize[cell];
   MessageDialog[
    Column[{"Upload image to StackExchange sites?", 
      img}], {"Upload and copy MarkDown" :> stackCopyMarkdown[img], 
     "Cancel" :> Null}, WindowTitle -> "Upload to StackExchange"]]]]

Unfortunately I can't put the button in a palette (CreatePalette) because the palette dimensions will influence the rasterization. Solutions to this problem are welcome.

Update 2:

Based on the answer to this question, here's a working Windows-only palette button:

button = Button["Upload to SO",
  Module[{sel},
   FrontEndExecute[
    FrontEndToken[FrontEnd`SelectedNotebook[], "CopySpecial", "MGF"]];
   sel = Cases[NotebookGet@ClipboardNotebook[], 
     RasterBox[data_, ___] :> 
      Image[data, "Byte", ColorSpace -> "RGB", Magnification -> 1], 
     Infinity];
   If[sel =!= {},
    With[{img = First[sel]},
     MessageDialog[
      Column[{"Upload image to StackExchange sites?", 
        img}], {"Upload and copy MarkDown" :> stackCopyMarkdown[img], 
       "Cancel" :> Null}, WindowTitle -> "Upload to StackExchange"]
     ]
    ]
   ]
  ]

CreatePalette[button]

Warning: it destroys the clipboard contents even if you click cancel in the preview box.

Community
  • 1
  • 1
Szabolcs
  • 21,860
  • 5
  • 72
  • 158
12

Note: This is using the anonymous imgur uploader with my anonymous key. The imgur site restricts uploads to 50 uploads/hour which should be fine normally, but this may cause a problem if a lot of people try this simultaneously. So please get your own anonymous key here:

http://imgur.com/register/api_anon

And then replace the key in the code below with your own key (thanks!).

The trickiest part to code was the conversion from a Mathematica expression to PNG image to Base64 encoding to URL encoding. There are about a 1,000 ways to do it wrong and I think I managed to try them all.

The code breaks down into a few pieces:

  • Construct the POST url
  • Make the HTTP connection
  • Send the POST url
  • Read back the result, which is XML
  • Extract the imgur url from the XML
  • Format the imgur url as markdown (or as a Mathematica Hyperlink function).

Here is the code:

imgur[expr_] :=
 Module[{url, key, image, data, jUrl, jConn, jWriter, jInput, buffer,
   byte, xml, imgurUrl},
  Needs["JLink`"];
  JLink`JavaBlock[
   JLink`LoadJavaClass["java.net.URLEncoder"];
   url = "http://api.imgur.com/2/upload";
   key = "c07bc3fb59ef878d5e23a0c4972fbb29";
   image = ExportString[ExportString[expr, "PNG"], "Base64"];
   data =
    URLEncoder`encode["key"   , "UTF-8"] <> "=" <>
    URLEncoder`encode[ key    , "UTF-8"] <> "&" <>
    URLEncoder`encode["image" , "UTF-8"] <> "=" <>
    URLEncoder`encode[ image  , "UTF-8"] ;
   jUrl = JLink`JavaNew["java.net.URL", url];
   jConn = jUrl@openConnection[];
   jConn@setDoOutput[True];
   jWriter =
    JLink`JavaNew["java.io.OutputStreamWriter",
     jConn@getOutputStream[]];
   jWriter@write[data];
   jWriter@flush[];
   jInput = jConn@getInputStream[];
   buffer = {};
   While[(byte = jInput@read[]; byte >= 0), AppendTo[buffer, byte]];
   ];
  xml = ImportString[FromCharacterCode[buffer], "XML"];
  imgurUrl =
   Cases[xml,
     XMLElement["original", {}, {string_}] :>
      string, \[Infinity]][[1]];
  "![Mathematica graphic](" <> imgurUrl <> ")"
  ]

Testing:

In[]:= g = Graphics[{Blue, Disk[]}, PlotRange -> 1.2, ImageSize -> Small];
       pic = Overlay[{Blur[Binarize@g, 10], g}];
       imgur[pic]

Out[]= ![Mathematica graphic](http://i.imgur.com/eGOlL.png)

And the actual image:

Mathematica graphic

Arnoud Buzing
  • 15,177
  • 3
  • 18
  • 50
  • Is there a way to do the encoding and the uploading within Mathematica itself, without resorting to `JLink`? +1, btw. – rcollyer Dec 01 '11 at 03:48
  • @rcollyer -- Not in V8, to the best of my knowledge. Is encoding and uploading something you would like to see as a built-in feature of Mathematica? – Arnoud Buzing Dec 01 '11 at 03:58
  • 1
    I got errors: ``Java::excptn: A Java exception occurred: java.io.IOException: Server returned HTTP response code: 400 for URL: http://api.imgur.com/2/upload at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1290). XML`Parser`XMLGetString::string: String expected at position 1 in XML`Parser`XMLGetString[EndOfFile].`` – Mr.Wizard Dec 01 '11 at 03:59
  • That's an interesting question. As of the moment, I have no need for it, but I could see its usefulness as a native package. Also, being able to use `Export` to output to web addresses would make it symmetric to `Import`, and I can see some value in that. – rcollyer Dec 01 '11 at 04:04
  • 2
    @Mr.Wizard -- I see this with V7 too now. V7 is doing something different with Base64 encoding: ExportString[ExportString[1, "PNG"], "Base64"]. I'm checking for a V7 solution. – Arnoud Buzing Dec 01 '11 at 04:10
  • 1
    Also, having to put together the query string like that seems a bit clunky. I'd probably replace it with ``queryString[enc_String, q:{{_String, _String}..}] := StringJoin@Riffle[ URLEncoder`encode[#1,enc]<> "="<> URLEncoder`encode[#2,enc]& @@@ q,"&"]``. – rcollyer Dec 01 '11 at 04:20
  • 2
    @Mr.Wizard -- In V7, replace the line that sets image with this: image = Developer`EncodeBase64[ExportString[expr, "PNG"]]; Let me know if that solves the V7 issue (it did for me). – Arnoud Buzing Dec 01 '11 at 04:32
  • Great answer, and a simpler Java solution than in the links I gave! I added a warning in the main question about imgur.com not being the same as stack.imgur.com. I was not aware of this at the time I posted the question. I'm in a dillemma now: it's so much easier to upload images using this the temptation is irresisitible, btu I know the right thing is to use stack.imgur.com so I shouldn't ... – Szabolcs Dec 01 '11 at 08:21
  • With larger images (as in the `StringLength` of `image`, try e.g. `g = Plot3D[Sin[x^2 + y^2], {x, -3, 3}, {y, -3, 3}]` which is about 100 kB) I most of the time get `Java::excptn: "A Java exception occurred: "java.net.SocketTimeoutException: Read timed out "` ... `at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1049).` Is there anything that can be done to remedy this? – Szabolcs Dec 01 '11 at 08:43
  • Regarding uploading to stack.imgur.com, the following works from a command line with [curl](http://curl.haxx.se/latest.cgi?curl=win32-ssl-devel-msvc): `curl -F "name=@myfile.png" http://stackoverflow.com/upload/image`. Perhaps we could modify your script to work with this? (Unfortunately I know neither Java nor any web technologies, so I'm a bit insecure about this) – Szabolcs Dec 01 '11 at 09:36