Add the ability to select a source image to use in the code.

A much farther ranging change than I suspected.

Basically add a 'source' integer to every Try, store that in the database with every Try, add the source to the computation of the hash, and load and use the 'source' value when navigating history.

BUG=skia:
R=mtklein@google.com

Author: jcgregorio@google.com

Review URL: https://codereview.chromium.org/294903017

git-svn-id: http://skia.googlecode.com/svn/trunk@14960 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/experimental/webtry/DESIGN.md b/experimental/webtry/DESIGN.md
index 0bc2145..4c0cee2 100644
--- a/experimental/webtry/DESIGN.md
+++ b/experimental/webtry/DESIGN.md
@@ -133,34 +133,51 @@
     CREATE DATABASE webtry;
     USE webtry;
     CREATE USER 'webtry'@'%' IDENTIFIED BY '<password is in valentine>';
-    GRANT SELECT, INSERT, UPDATE ON webtry.webtry       TO 'webtry'@'%';
-    GRANT SELECT, INSERT, UPDATE ON webtry.workspace    TO 'webtry'@'%';
-    GRANT SELECT, INSERT, UPDATE ON webtry.workspacetry TO 'webtry'@'%';
+    GRANT SELECT, INSERT, UPDATE ON webtry.webtry        TO 'webtry'@'%';
+    GRANT SELECT, INSERT, UPDATE ON webtry.workspace     TO 'webtry'@'%';
+    GRANT SELECT, INSERT, UPDATE ON webtry.workspacetry  TO 'webtry'@'%';
+    GRANT SELECT, INSERT, UPDATE ON webtry.source_images TO 'webtry'@'%';
 
     // If this gets changed also update the sqlite create statement in webtry.go.
 
     CREATE TABLE webtry (
-      code      TEXT      DEFAULT ''                 NOT NULL,
-      create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
-      hash      CHAR(64)  DEFAULT ''                 NOT NULL,
-      PRIMARY KEY(hash)
+      code               TEXT      DEFAULT ''                 NOT NULL,
+      create_ts          TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
+      hash               CHAR(64)  DEFAULT ''                 NOT NULL,
+      source_image_id    INTEGER   DEFAULT 0                  NOT NULL,
+      PRIMARY KEY(hash),
+
+      FOREIGN KEY (source) REFERENCES sources(id)
     );
 
     CREATE TABLE workspace (
       name      CHAR(64)  DEFAULT ''                 NOT NULL,
       create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
-      PRIMARY KEY(name)
+      PRIMARY KEY(name),
     );
 
     CREATE TABLE workspacetry (
-      name      CHAR(64)  DEFAULT ''                 NOT NULL,
-      create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
-      hash      CHAR(64)  DEFAULT ''                 NOT NULL,
-      hidden    INTEGER   DEFAULT 0                  NOT NULL,
+      name             CHAR(64)  DEFAULT ''                 NOT NULL,
+      create_ts        TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
+      hash             CHAR(64)  DEFAULT ''                 NOT NULL,
+      source_image_id  INTEGER   DEFAULT 0                  NOT NULL,
+      hidden           INTEGER   DEFAULT 0                  NOT NULL,
 
-      FOREIGN KEY (name) REFERENCES workspace(name)
+      FOREIGN KEY (name)   REFERENCES workspace(name),
     );
 
+    CREATE TABLE source_images (
+      id        INTEGER     PRIMARY KEY                NOT NULL,
+      image     MEDIUMBLOB  DEFAULT ''                 NOT NULL, -- Stored as PNG.
+      width     INTEGER     DEFAULT 0                  NOT NULL,
+      height    INTEGER     DEFAULT 0                  NOT NULL,
+      create_ts TIMESTAMP   DEFAULT CURRENT_TIMESTAMP  NOT NULL,
+      hidden    INTEGER     DEFAULT 0                  NOT NULL
+    );
+
+    ALTER TABLE webtry       ADD COLUMN source_image_id INTEGER DEFAULT 0 NOT NULL AFTER hash;
+    ALTER TABLE workspacetry ADD COLUMN source_image_id INTEGER DEFAULT 0 NOT NULL AFTER hash;
+
 Common queries webtry.go will use:
 
     INSERT INTO webtry (code, hash) VALUES('int i = 0;...', 'abcdef...');
@@ -183,6 +200,10 @@
 
     SELECT name FROM workspace GROUP BY name;
 
+Common queries for sources:
+
+    SELECT id, image, width, height, create_ts FROM source_images ORDER BY create_ts DESC LIMIT 100;
+
 Password for the database will be stored in the metadata instance, if the
 metadata server can't be found, i.e. running locally, then a local sqlite
 database will be used. To see the current password stored in metadata and the
@@ -202,6 +223,29 @@
 N.B. If you need to change the MySQL password that webtry uses, you must change
 it both in MySQL and the value stored in the metadata server.
 
+Source Images
+-------------
+
+For every try the user can select an optional source image to use as an input.
+The id of the source image is just an integer and is stored in the database
+along with the other try information, such as the code.
+
+The actual image itself is also stored in a separate table, 'sources', in the
+database.  On startup we check that all the images are available in 'inout',
+and write out the images if not. Since they are all written to 'inout' we can
+use the same /i/ image handler to serve them.
+
+When a user uploads an image it is decoded and converted to PNG and stored
+as a binary blob in the database.
+
+The bitmap is available to user code as a module level variable:
+
+    SkBitmap source;
+
+The bitmap is read, decoded and stored in source before the seccomp jail is
+instantiated.
+
+
 Squid
 -----
 
diff --git a/experimental/webtry/main.cpp b/experimental/webtry/main.cpp
index 7ccb932..44f8aab 100644
--- a/experimental/webtry/main.cpp
+++ b/experimental/webtry/main.cpp
@@ -6,6 +6,7 @@
 #include "SkData.h"
 #include "SkForceLinking.h"
 #include "SkGraphics.h"
+#include "SkImageDecoder.h"
 #include "SkImageEncoder.h"
 #include "SkImageInfo.h"
 #include "SkStream.h"
@@ -16,6 +17,10 @@
 __SK_FORCE_IMAGE_DECODER_LINKING;
 
 DEFINE_string(out, "", "Filename of the PNG to write to.");
+DEFINE_string(source, "", "Filename of the source image.");
+
+// Defined in template.cpp.
+extern SkBitmap source;
 
 static bool install_syscall_filter() {
     struct sock_filter filter[] = {
@@ -89,6 +94,13 @@
       perror("The --out flag must have an argument.");
       return 1;
     }
+
+    if (FLAGS_source.count() == 1) {
+       if (!SkImageDecoder::DecodeFile(FLAGS_source[0], &source)) {
+           perror("Unable to read the source image.");
+       }
+    }
+
     SkFILEWStream stream(FLAGS_out[0]);
 
     SkImageInfo info = SkImageInfo::MakeN32(256, 256, kPremul_SkAlphaType);
diff --git a/experimental/webtry/res/css/webtry.css b/experimental/webtry/res/css/webtry.css
index 3b04d7d..09751df 100644
--- a/experimental/webtry/res/css/webtry.css
+++ b/experimental/webtry/res/css/webtry.css
@@ -80,6 +80,38 @@
     float: none;
 }
 
+#chooseList {
+  display: flex;
+  flex-flow: row wrap;
+}
+
+#chooseSource {
+  display: none;
+  background: ivory;
+  padding: 1em;
+  border: solid lightgray 2px;
+}
+
+#chooseSource.show {
+  display: block;
+}
+
+#selectedSource {
+  display: none;
+}
+
+#selectedSource.show {
+  display: block;
+}
+
+#sourceCode {
+  display: none;
+}
+
+#sourceCode.show {
+  display: block;
+}
+
 #gitInfo {
     float: right;
     font-size: 70%;
diff --git a/experimental/webtry/res/js/webtry.js b/experimental/webtry/res/js/webtry.js
index b1fe3dd..b24501e 100644
--- a/experimental/webtry/res/js/webtry.js
+++ b/experimental/webtry/res/js/webtry.js
@@ -18,17 +18,127 @@
  */
 (function() {
     function onLoad() {
-      var run = document.getElementById('run');
-      var permalink = document.getElementById('permalink');
-      var embed = document.getElementById('embed');
-      var embedButton = document.getElementById('embedButton');
-      var code = document.getElementById('code');
-      var output = document.getElementById('output');
-      var stdout = document.getElementById('stdout');
-      var img = document.getElementById('img');
-      var tryHistory = document.getElementById('tryHistory');
-      var parser = new DOMParser();
-      var tryTemplate = document.getElementById('tryTemplate');
+      var run             = document.getElementById('run');
+      var permalink       = document.getElementById('permalink');
+      var embed           = document.getElementById('embed');
+      var embedButton     = document.getElementById('embedButton');
+      var code            = document.getElementById('code');
+      var output          = document.getElementById('output');
+      var stdout          = document.getElementById('stdout');
+      var img             = document.getElementById('img');
+      var tryHistory      = document.getElementById('tryHistory');
+      var parser          = new DOMParser();
+      var tryTemplate     = document.getElementById('tryTemplate');
+      var sourcesTemplate = document.getElementById('sourcesTemplate');
+
+      var enableSource   = document.getElementById('enableSource');
+      var selectedSource = document.getElementById('selectedSource');
+      var sourceCode     = document.getElementById('sourceCode');
+      var chooseSource   = document.getElementById('chooseSource');
+      var chooseList     = document.getElementById('chooseList');
+
+      // Id of the source image to use, 0 if no source image is used.
+      var sourceId = 0;
+
+      sourceId = parseInt(enableSource.getAttribute('data-id'));
+      if (sourceId) {
+        sourceSelectByID(sourceId);
+      }
+
+
+      function beginWait() {
+        document.body.classList.add('waiting');
+        run.disabled = true;
+      }
+
+
+      function endWait() {
+        document.body.classList.remove('waiting');
+        run.disabled = false;
+      }
+
+
+      function sourceSelectByID(id) {
+        sourceId = id;
+        if (id > 0) {
+          enableSource.checked = true;
+          selectedSource.innerHTML = '<img with=64 height=64 src="/i/image-'+sourceId+'.png" />';
+          selectedSource.classList.add('show');
+          sourceCode.classList.add('show');
+          chooseSource.classList.remove('show');
+        } else {
+          enableSource.checked = false;
+          selectedSource.classList.remove('show');
+          sourceCode.classList.remove('show');
+        }
+      }
+
+
+      /**
+       * A selection has been made in the choiceList.
+       */
+      function sourceSelect() {
+        sourceSelectByID(parseInt(this.getAttribute('data-id')));
+      }
+
+
+      /**
+       * Callback when the loading of the image sources is complete.
+       *
+       * Fills in the list of images from the data returned.
+       */
+      function sourcesComplete(e) {
+        endWait();
+        // The response is JSON of the form:
+        // [
+        //   {"id": 1},
+        //   {"id": 3},
+        //   ...
+        // ]
+        body = JSON.parse(e.target.response);
+        // Clear out the old list if present.
+        while (chooseList.firstChild) {
+          chooseList.removeChild(chooseList.firstChild);
+        }
+        body.forEach(function(source) {
+         var id = 'i'+source.id;
+         var imgsrc = '/i/image-'+source.id+'.png';
+         var clone = sourcesTemplate.content.cloneNode(true);
+         clone.querySelector('img').src     = imgsrc;
+         clone.querySelector('button').setAttribute('id', id);
+         clone.querySelector('button').setAttribute('data-id', source.id);
+         chooseList.insertBefore(clone, chooseList.firstChild);
+         chooseList.querySelector('#'+id).addEventListener('click', sourceSelect, true);
+        });
+        chooseSource.classList.add('show');
+      }
+
+
+      /**
+       * Toggle the use of a source image, or select a new source image.
+       *
+       * If enabling source images then load the list of available images via
+       * XHR.
+       */
+      function sourceClick(e) {
+        selectedSource.classList.remove('show');
+        sourceCode.classList.remove('show');
+        if (enableSource.checked) {
+          beginWait();
+          var req = new XMLHttpRequest();
+          req.addEventListener('load', sourcesComplete);
+          req.addEventListener('error', xhrError);
+          req.overrideMimeType('application/json');
+          req.open('GET', '/sources/', true);
+          req.send();
+        } else {
+          sourceId = 0;
+        }
+      }
+
+      enableSource.addEventListener('click', sourceClick, true);
+      selectedSource.addEventListener('click', sourceClick, true);
+
 
       var editor = CodeMirror.fromTextArea(code, {
         theme: "default",
@@ -42,17 +152,6 @@
       editor.setSize(editor.defaultCharWidth() * code.cols,
                      editor.defaultTextHeight() * code.rows);
 
-      function beginWait() {
-        document.body.classList.add('waiting');
-        run.disabled = true;
-      }
-
-
-      function endWait() {
-        document.body.classList.remove('waiting');
-        run.disabled = false;
-      }
-
 
       /**
        * Callback when there's an XHR error.
@@ -100,6 +199,7 @@
         code.value = body.code;
         editor.setValue(body.code);
         img.src = '/i/'+body.hash+'.png';
+        sourceSelectByID(body.source);
         if (permalink) {
           permalink.href = '/c/' + body.hash;
         }
@@ -172,7 +272,7 @@
         req.overrideMimeType('application/json');
         req.open('POST', '/', true);
         req.setRequestHeader('content-type', 'application/json');
-        req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceName}));
+        req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceName, 'source': sourceId}));
       }
       run.addEventListener('click', onSubmitCode);
 
diff --git a/experimental/webtry/result.cpp b/experimental/webtry/result.cpp
index d06ef9c..cc1c2f6 100644
--- a/experimental/webtry/result.cpp
+++ b/experimental/webtry/result.cpp
@@ -7,6 +7,7 @@
 #include "SkStream.h"
 #include "SkSurface.h"
 
+SkBitmap source;
 
 void draw(SkCanvas* canvas) {
 #line 1
diff --git a/experimental/webtry/templates/content.html b/experimental/webtry/templates/content.html
index 8638cf2..0f2177a 100644
--- a/experimental/webtry/templates/content.html
+++ b/experimental/webtry/templates/content.html
@@ -1,5 +1,23 @@
 
 <section id=content>
+
+  <template id=sourcesTemplate>
+      <button id="" class=source><img width=64 height=64 src=''></button>
+  </template>
+  <input type="checkbox" id="enableSource" data-id="{{.Source}}"> Use an input bitmap.
+  <br>
+  <button id=selectedSource></button>
+  <pre id=sourceCode>SkBitmap source;</pre>
+  <div id=chooseSource>
+    Choose an image below or upload a new one to use as an input bitmap.
+    <div id="chooseList">
+    </div>
+    <form action="/sources/" method="post" accept-charset="utf-8" enctype="multipart/form-data">
+      <input type="file" accept="image/*" name="upload" value="" id="upload">
+      <input type="submit" value="Add Image">
+    </form>
+  </div>
+
   <pre>
     <textarea spellcheck=false name='code' id='code' rows='15' cols='100'>{{.Code}}</textarea>
   </pre>
@@ -9,6 +27,7 @@
 
   <input type='button' value='Embed' id='embedButton' disabled/>
   <input type="text" value="" id="embed" readonly style="display:none;">
+  <br>
 
   <p>
   <img  touch-action='none' class='zoom' id='img' src='{{if .Hash}}/i/{{.Hash}}.png{{end}}'/>
diff --git a/experimental/webtry/templates/template.cpp b/experimental/webtry/templates/template.cpp
index 67d2c04..c1f40f3 100644
--- a/experimental/webtry/templates/template.cpp
+++ b/experimental/webtry/templates/template.cpp
@@ -158,4 +158,6 @@
 #include "SkXfermode.h"
 #include "SkXfermodeImageFilter.h"
 
+SkBitmap source;
+
 {{.Code}}
diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go
index 2c3d7c9..19eeb4c 100644
--- a/experimental/webtry/webtry.go
+++ b/experimental/webtry/webtry.go
@@ -5,10 +5,15 @@
 	"crypto/md5"
 	"database/sql"
 	"encoding/base64"
+	"encoding/binary"
 	"encoding/json"
 	"flag"
 	"fmt"
 	htemplate "html/template"
+	"image"
+	_ "image/gif"
+	_ "image/jpeg"
+	"image/png"
 	"io/ioutil"
 	"log"
 	"math/rand"
@@ -70,7 +75,7 @@
 	iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$")
 
 	// imageLink is the regex that matches URLs paths that are direct links to PNGs.
-	imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
+	imageLink = regexp.MustCompile("^/i/([a-z0-9-]+.png)$")
 
 	// tryInfoLink is the regex that matches URLs paths that are direct links to data about a single try.
 	tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$")
@@ -221,14 +226,28 @@
 			log.Printf("ERROR: Failed to open: %q\n", err)
 			panic(err)
 		}
-		sql := `CREATE TABLE webtry (
-             code      TEXT      DEFAULT ''                 NOT NULL,
-             create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
-             hash      CHAR(64)  DEFAULT ''                 NOT NULL,
+		sql := `CREATE TABLE source_images (
+             id        INTEGER     PRIMARY KEY                NOT NULL,
+             image     MEDIUMBLOB  DEFAULT ''                 NOT NULL, -- formatted as a PNG.
+             width     INTEGER     DEFAULT 0                  NOT NULL,
+             height    INTEGER     DEFAULT 0                  NOT NULL,
+             create_ts TIMESTAMP   DEFAULT CURRENT_TIMESTAMP  NOT NULL,
+             hidden    INTEGER     DEFAULT 0                  NOT NULL
+             )`
+		_, err = db.Exec(sql)
+		log.Printf("Info: status creating sqlite table for sources: %q\n", err)
+
+		sql = `CREATE TABLE webtry (
+             code               TEXT      DEFAULT ''                 NOT NULL,
+             create_ts          TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
+             hash               CHAR(64)  DEFAULT ''                 NOT NULL,
+             source_image_id    INTEGER   DEFAULT 0                  NOT NULL,
+
              PRIMARY KEY(hash)
             )`
 		_, err = db.Exec(sql)
 		log.Printf("Info: status creating sqlite table for webtry: %q\n", err)
+
 		sql = `CREATE TABLE workspace (
           name      CHAR(64)  DEFAULT ''                 NOT NULL,
           create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
@@ -236,13 +255,15 @@
         )`
 		_, err = db.Exec(sql)
 		log.Printf("Info: status creating sqlite table for workspace: %q\n", err)
-		sql = `CREATE TABLE workspacetry (
-          name      CHAR(64)  DEFAULT ''                 NOT NULL,
-          create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
-          hash      CHAR(64)  DEFAULT ''                 NOT NULL,
-          hidden    INTEGER   DEFAULT 0                  NOT NULL,
 
-          FOREIGN KEY (name) REFERENCES workspace(name)
+		sql = `CREATE TABLE workspacetry (
+          name               CHAR(64)  DEFAULT ''                 NOT NULL,
+          create_ts          TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
+          hash               CHAR(64)  DEFAULT ''                 NOT NULL,
+          hidden             INTEGER   DEFAULT 0                  NOT NULL,
+          source_image_id    INTEGER   DEFAULT 0                  NOT NULL,
+
+          FOREIGN KEY (name)   REFERENCES workspace(name)
         )`
 		_, err = db.Exec(sql)
 		log.Printf("Info: status creating sqlite table for workspace try: %q\n", err)
@@ -258,6 +279,34 @@
 		}
 	}()
 
+	writeOutAllSourceImages()
+}
+
+func writeOutAllSourceImages() {
+	// Pull all the source images from the db and write them out to inout.
+	rows, err := db.Query("SELECT id, image, create_ts FROM source_images ORDER BY create_ts DESC")
+
+	if err != nil {
+		log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
+		panic(err)
+	}
+	for rows.Next() {
+		var id int
+		var image []byte
+		var create_ts time.Time
+		if err := rows.Scan(&id, &image, &create_ts); err != nil {
+			log.Printf("Error: failed to fetch from database: %q", err)
+			continue
+		}
+		filename := fmt.Sprintf("../../../inout/image-%d.png", id)
+		if _, err := os.Stat(filename); os.IsExist(err) {
+			log.Printf("Skipping write since file exists: %q", filename)
+			continue
+		}
+		if err := ioutil.WriteFile(filename, image, 0666); err != nil {
+			log.Printf("Error: failed to write image file: %q", err)
+		}
+	}
 }
 
 // Titlebar is used in titlebar template expansion.
@@ -270,6 +319,7 @@
 type userCode struct {
 	Code     string
 	Hash     string
+	Source   int
 	Titlebar Titlebar
 }
 
@@ -283,10 +333,11 @@
 	return t.Execute(f, userCode{Code: code, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}})
 }
 
-// expandCode expands the template into a file and calculate the MD5 hash.
-func expandCode(code string) (string, error) {
+// expandCode expands the template into a file and calculates the MD5 hash.
+func expandCode(code string, source int) (string, error) {
 	h := md5.New()
 	h.Write([]byte(code))
+	binary.Write(h, binary.LittleEndian, int64(source))
 	hash := fmt.Sprintf("%x", h.Sum(nil))
 	// At this point we are running in skia/experimental/webtry, making cache a
 	// peer directory to skia.
@@ -360,20 +411,96 @@
 	w.Write(resp)
 }
 
-func writeToDatabase(hash string, code string, workspaceName string) {
+func writeToDatabase(hash string, code string, workspaceName string, source int) {
 	if db == nil {
 		return
 	}
-	if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil {
+	if _, err := db.Exec("INSERT INTO webtry (code, hash, source_image_id) VALUES(?, ?, ?)", code, hash, source); err != nil {
 		log.Printf("ERROR: Failed to insert code into database: %q\n", err)
 	}
 	if workspaceName != "" {
-		if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil {
+		if _, err := db.Exec("INSERT INTO workspacetry (name, hash, source_image_id) VALUES(?, ?, ?)", workspaceName, hash, source); err != nil {
 			log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err)
 		}
 	}
 }
 
+type Sources struct {
+	Id int `json:"id"`
+}
+
+// sourcesHandler serves up the PNG of a specific try.
+func sourcesHandler(w http.ResponseWriter, r *http.Request) {
+	log.Printf("Sources Handler: %q\n", r.URL.Path)
+	if r.Method == "GET" {
+		rows, err := db.Query("SELECT id, create_ts FROM source_images WHERE hidden=0 ORDER BY create_ts DESC")
+
+		if err != nil {
+			http.Error(w, fmt.Sprintf("Failed to query sources: %s.", err), 500)
+		}
+		sources := make([]Sources, 0, 0)
+		for rows.Next() {
+			var id int
+			var create_ts time.Time
+			if err := rows.Scan(&id, &create_ts); err != nil {
+				log.Printf("Error: failed to fetch from database: %q", err)
+				continue
+			}
+			sources = append(sources, Sources{Id: id})
+		}
+
+		resp, err := json.Marshal(sources)
+		if err != nil {
+			reportError(w, r, err, "Failed to serialize a response.")
+			return
+		}
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(resp)
+
+	} else if r.Method == "POST" {
+		if err := r.ParseMultipartForm(1000000); err != nil {
+			http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500)
+			return
+		}
+		if _, ok := r.MultipartForm.File["upload"]; !ok {
+			http.Error(w, "Invalid upload.", 500)
+			return
+		}
+		if len(r.MultipartForm.File["upload"]) != 1 {
+			http.Error(w, "Wrong number of uploads.", 500)
+			return
+		}
+		f, err := r.MultipartForm.File["upload"][0].Open()
+		if err != nil {
+			http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500)
+			return
+		}
+		defer f.Close()
+		m, _, err := image.Decode(f)
+		if err != nil {
+			http.Error(w, fmt.Sprintf("Failed to decode image: %s.", err), 500)
+			return
+		}
+		var b bytes.Buffer
+		png.Encode(&b, m)
+		bounds := m.Bounds()
+		width := bounds.Max.Y - bounds.Min.Y
+		height := bounds.Max.X - bounds.Min.X
+		if _, err := db.Exec("INSERT INTO source_images (image, width, height) VALUES(?, ?, ?)", b.Bytes(), width, height); err != nil {
+			log.Printf("ERROR: Failed to insert sources into database: %q\n", err)
+			http.Error(w, fmt.Sprintf("Failed to store image: %s.", err), 500)
+			return
+		}
+		go writeOutAllSourceImages()
+
+		// Now redirect back to where we came from.
+		http.Redirect(w, r, r.Referer(), 302)
+	} else {
+		http.NotFound(w, r)
+		return
+	}
+}
+
 // imageHandler serves up the PNG of a specific try.
 func imageHandler(w http.ResponseWriter, r *http.Request) {
 	log.Printf("Image Handler: %q\n", r.URL.Path)
@@ -393,6 +520,7 @@
 
 type Try struct {
 	Hash     string `json:"hash"`
+	Source   int
 	CreateTS string `json:"create_ts"`
 }
 
@@ -431,6 +559,7 @@
 	Name     string
 	Code     string
 	Hash     string
+	Source   int
 	Tries    []Try
 	Titlebar Titlebar
 }
@@ -452,13 +581,14 @@
 }
 
 // getCode returns the code for a given hash, or the empty string if not found.
-func getCode(hash string) (string, error) {
+func getCode(hash string) (string, int, error) {
 	code := ""
-	if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
+	source := 0
+	if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil {
 		log.Printf("ERROR: Code for hash is missing: %q\n", err)
-		return code, err
+		return code, source, err
 	}
-	return code, nil
+	return code, source, nil
 }
 
 func workspaceHandler(w http.ResponseWriter, r *http.Request) {
@@ -469,7 +599,7 @@
 		name := ""
 		if len(match) == 2 {
 			name = match[1]
-			rows, err := db.Query("SELECT create_ts, hash FROM workspacetry WHERE name=? ORDER BY create_ts", name)
+			rows, err := db.Query("SELECT create_ts, hash, source_image_id FROM workspacetry WHERE name=? ORDER BY create_ts", name)
 			if err != nil {
 				reportError(w, r, err, "Failed to select.")
 				return
@@ -477,23 +607,25 @@
 			for rows.Next() {
 				var hash string
 				var create_ts time.Time
-				if err := rows.Scan(&create_ts, &hash); err != nil {
+				var source int
+				if err := rows.Scan(&create_ts, &hash, &source); err != nil {
 					log.Printf("Error: failed to fetch from database: %q", err)
 					continue
 				}
-				tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
+				tries = append(tries, Try{Hash: hash, Source: source, CreateTS: create_ts.Format("2006-02-01")})
 			}
 		}
 		var code string
 		var hash string
+		source := 0
 		if len(tries) == 0 {
 			code = DEFAULT_SAMPLE
 		} else {
 			hash = tries[len(tries)-1].Hash
-			code, _ = getCode(hash)
+			code, source, _ = getCode(hash)
 		}
 		w.Header().Set("Content-Type", "text/html")
-		if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
+		if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
 			log.Printf("ERROR: Failed to expand template: %q\n", err)
 		}
 	} else if r.Method == "POST" {
@@ -518,8 +650,9 @@
 }
 
 type TryRequest struct {
-	Code string `json:"code"`
-	Name string `json:"name"` // Optional name of the workspace the code is in.
+	Code   string `json:"code"`
+	Name   string `json:"name"`   // Optional name of the workspace the code is in.
+	Source int    `json:"source"` // ID of the source image, 0 if none.
 }
 
 // iframeHandler handles the GET and POST of the main page.
@@ -540,21 +673,22 @@
 		return
 	}
 	var code string
-	code, err := getCode(hash)
+	code, source, err := getCode(hash)
 	if err != nil {
 		http.NotFound(w, r)
 		return
 	}
 	// Expand the template.
 	w.Header().Set("Content-Type", "text/html")
-	if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash}); err != nil {
+	if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source}); err != nil {
 		log.Printf("ERROR: Failed to expand template: %q\n", err)
 	}
 }
 
 type TryInfo struct {
-	Hash string `json:"hash"`
-	Code string `json:"code"`
+	Hash   string `json:"hash"`
+	Code   string `json:"code"`
+	Source int    `json:"source"`
 }
 
 // tryInfoHandler returns information about a specific try.
@@ -570,14 +704,15 @@
 		return
 	}
 	hash := match[1]
-	code, err := getCode(hash)
+	code, source, err := getCode(hash)
 	if err != nil {
 		http.NotFound(w, r)
 		return
 	}
 	m := TryInfo{
-		Hash: hash,
-		Code: code,
+		Hash:   hash,
+		Code:   code,
+		Source: source,
 	}
 	resp, err := json.Marshal(m)
 	if err != nil {
@@ -599,6 +734,7 @@
 	log.Printf("Main Handler: %q\n", r.URL.Path)
 	if r.Method == "GET" {
 		code := DEFAULT_SAMPLE
+		source := 0
 		match := directLink.FindStringSubmatch(r.URL.Path)
 		var hash string
 		if len(match) == 2 && r.URL.Path != "/" {
@@ -608,14 +744,14 @@
 				return
 			}
 			// Update 'code' with the code found in the database.
-			if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
+			if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil {
 				http.NotFound(w, r)
 				return
 			}
 		}
 		// Expand the template.
 		w.Header().Set("Content-Type", "text/html")
-		if err := indexTemplate.Execute(w, userCode{Code: code, Hash: hash, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
+		if err := indexTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
 			log.Printf("ERROR: Failed to expand template: %q\n", err)
 		}
 	} else if r.Method == "POST" {
@@ -641,12 +777,12 @@
 			reportTryError(w, r, err, "Preprocessor macros aren't allowed.", "")
 			return
 		}
-		hash, err := expandCode(LineNumbers(request.Code))
+		hash, err := expandCode(LineNumbers(request.Code), request.Source)
 		if err != nil {
 			reportTryError(w, r, err, "Failed to write the code to compile.", hash)
 			return
 		}
-		writeToDatabase(hash, request.Code, request.Name)
+		writeToDatabase(hash, request.Code, request.Name, request.Source)
 		message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
 		if err != nil {
 			message = cleanCompileOutput(message, hash)
@@ -661,6 +797,9 @@
 		}
 		message += linkMessage
 		cmd := hash + " --out " + hash + ".png"
+		if request.Source > 0 {
+			cmd += fmt.Sprintf("  --source image-%d.png", request.Source)
+		}
 		if *useChroot {
 			cmd = "schroot -c webtry --directory=/inout -- /inout/" + cmd
 		} else {
@@ -706,6 +845,7 @@
 	http.HandleFunc("/recent/", autogzip.HandleFunc(recentHandler))
 	http.HandleFunc("/iframe/", autogzip.HandleFunc(iframeHandler))
 	http.HandleFunc("/json/", autogzip.HandleFunc(tryInfoHandler))
+	http.HandleFunc("/sources/", autogzip.HandleFunc(sourcesHandler))
 
 	// Resources are served directly
 	// TODO add support for caching/etags/gzip