Upload images to Imgur from Mathematica
Asked Answered
C

3

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.

Cromagnon answered 7/11, 2011 at 15:2 Comment(8)
Please note that uploading to http://i.stack.imgur.com/ is more difficult (you'll have to "drive" the SO interface)Subaxillary
@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 imgur.com/faq#long (at least 1 view / 6 months is needed for them to be kept)Cromagnon
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.Nilsanilsen
@Sjoerd When posting in the <tag>image-processing</tag> tag, I have to repeat that again and again ...Subaxillary
@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.Nilsanilsen
@Sjoerd Yes, but this is only the first step of another story. The idea is in the comments here #8034798Subaxillary
@Sjoerd One can also make it into a palette and make it a one-click processCromagnon
@belisarius Unfortunately there's no official way to upload to stack.imgur.com at the moment. See here: stackapps.com/questions/2664/…Cromagnon
B
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).

Banbury answered 1/12, 2011 at 17:32 Comment(3)
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 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).Cromagnon
+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?Gilliette
@Gilliette 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.Cromagnon
C
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.

Cromagnon answered 1/12, 2011 at 15:4 Comment(3)
I think the Rasterize palette issue is worth its own question.Dialectical
That's fantastic! I was reserving my vote in hope of this. +1Dialectical
@Dialectical check the chatroom for a packaged up solutionCromagnon
B
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](https://i.sstatic.net/PrP3V.png)

And the actual image:

Mathematica graphic

Banbury answered 1/12, 2011 at 3:29 Comment(10)
Is there a way to do the encoding and the uploading within Mathematica itself, without resorting to JLink? +1, btw.Acie
@Acie -- 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?Banbury
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].Dialectical
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.Acie
@Dialectical -- 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.Banbury
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,"&"].Acie
@Dialectical -- 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).Banbury
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 ...Cromagnon
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?Cromagnon
Regarding uploading to stack.imgur.com, the following works from a command line with curl: curl -F "[email protected]" 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)Cromagnon

© 2022 - 2024 — McMap. All rights reserved.