diff --git a/core/views/editor_helpers.py b/core/views/editor_helpers.py
new file mode 100644
index 0000000..e0ff5cd
--- /dev/null
+++ b/core/views/editor_helpers.py
@@ -0,0 +1,111 @@
+from django.shortcuts import render, redirect
+from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse
+
+from django.urls import reverse, resolve
+from django.template import Context, loader
+import re, io
+from PIL import Image
+from pathlib import Path
+import django.forms as forms
+import troggle.settings as settings
+
+from troggle.lib import version_control
+
+MAX_IMAGE_WIDTH = 1000
+MAX_IMAGE_HEIGTH = 800
+
+THUMBNAIL_WIDTH = 200
+THUMBNAIL_HEIGTH = 200
+
+def image_selector(request, path):
+    '''Returns available images'''
+    directory = path.rsplit('/', 1)[0]
+    thumbnailspath = Path(settings.EXPOWEB) / directory / "t"
+    thumbnails = []
+    for f in thumbnailspath.iterdir():
+        if f.is_file():
+            thumbnail_url = reverse('expopage', args=["%s/t/%s" % (directory, f.name)])
+            name_base = f.name.rsplit('.', 1)[0]
+            page_path_base = Path(settings.EXPOWEB) / directory / "l"
+            if ((page_path_base / ("%s.htm" % name_base)).is_file()):
+                page_url = reverse('expopage', args=["%s/l/%s.htm" % (directory, name_base)])
+            else:
+                page_url = reverse('expopage', args=["%s/l/%s.html" % (directory, name_base)])
+            
+            thumbnails.append({"thumbnail_url": thumbnail_url, "page_url": page_url})
+    
+    return render(request, 'image_selector.html', {'thumbnails': thumbnails})
+
+def new_image_form(request, path):
+    '''Manages a form to upload new images'''
+    directory = path.rsplit('/', 1)[0]
+    if request.method == 'POST':
+        form = NewWebImageForm(request.POST, request.FILES, directory = directory)
+        if form.is_valid():
+            f = request.FILES["file_"]
+            binary_data = io.BytesIO()
+            for chunk in f.chunks():
+                binary_data.write(chunk)
+            i = Image.open(binary_data)
+            width, height = i.size
+            if width > MAX_IMAGE_WIDTH or height > MAX_IMAGE_HEIGTH:
+                scale = max(width / MAX_IMAGE_WIDTH, height / MAX_IMAGE_HEIGTH)
+                i  = i.resize((int(width / scale), int(height / scale)), Image.ANTIALIAS)
+            tscale = max(width / THUMBNAIL_WIDTH, height / THUMBNAIL_HEIGTH)
+            thumbnail  = i.resize((int(width / tscale), int(height / tscale)), Image.ANTIALIAS)
+            ib = io.BytesIO()
+            i.save(ib, format="png")
+            tb = io.BytesIO()
+            thumbnail.save(tb, format="png")
+            image_rel_path, thumb_rel_path, desc_rel_path = form.get_rel_paths()
+            image_page_template = loader.get_template('image_page_template.html')
+            image_page = image_page_template.render({'header': form.cleaned_data["header"], 'description': form.cleaned_data["description"], 
+                                                     'photographer': form.cleaned_data["photographer"], 'year': form.cleaned_data["year"],
+                                                     'filepath': f'/{image_rel_path}'
+                                                     })
+            image_path, thumb_path, desc_path = form.get_full_paths()
+            try:
+                change_message = form.cleaned_data["change_message"]
+                version_control.write_and_commit([(desc_path, image_page, "utf-8"),
+                                                  (image_path, ib.getbuffer(), False),
+                                                  (thumb_path, tb.getbuffer(), False)], 
+                                                 f'{change_message} - online adding of an image')
+            except version_control.WriteAndCommitError as e:
+                return JsonResponse({"error": e.message})
+            linked_image_template = loader.get_template('linked_image_template.html')
+            html_snippet = linked_image_template.render({'thumbnail_url': f'/{thumb_rel_path}', 'page_url': f'/{desc_rel_path}'}, request)
+            return JsonResponse({"html": html_snippet})
+    else:
+        form = NewWebImageForm(directory = directory)
+    template = loader.get_template('new_image_form.html')
+    htmlform = template.render({'form': form, 'path': path}, request)
+    return JsonResponse({"form": htmlform})
+    
+class NewWebImageForm(forms.Form):
+    '''The form used by the editexpopage function
+    '''
+    header = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Enter title (displayed as a header and in the tab)"}))
+    file_ = forms.FileField()
+    description = forms.CharField(widget=forms.Textarea(attrs={"cols":80, "rows":20, 'placeholder': "Describe the photo (using HTML)"}))
+    photographer = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Photographers name"}), required = False)
+    year = forms.CharField(widget=forms.TextInput(attrs={'size':'60', 'placeholder': "Year photo was taken"}), required = False)
+    change_message = forms.CharField(widget=forms.Textarea(attrs={"cols":80, "rows":3, 'placeholder': "Descibe the change made (for git)"}))
+    
+    def __init__(self, *args, **kwargs):
+        self.directory = Path(kwargs.pop('directory'))
+        super(forms.Form, self).__init__(*args, **kwargs)
+
+    def get_rel_paths(self):
+        f = self.cleaned_data['file_']
+        return [self.directory / "i" / (f.name.rsplit('.', 1)[0] + ".png"),
+                self.directory / "t" / (f.name.rsplit('.', 1)[0] + ".png"),
+                self.directory / "l" / (f.name.rsplit('.', 1)[0] + ".html")]
+
+    def get_full_paths(self):
+        return [Path(settings.EXPOWEB) / x for x in self.get_rel_paths()]
+    
+    def clean_file_(self):
+        for rel_path, full_path in zip(self.get_rel_paths(), self.get_full_paths()):
+            if full_path.exists():
+                raise forms.ValidationError("File already exists in %s" % rel_path)
+        return self.cleaned_data['file_']
diff --git a/core/views/expo.py b/core/views/expo.py
index 00cb6dc..91919a5 100644
--- a/core/views/expo.py
+++ b/core/views/expo.py
@@ -359,7 +359,7 @@ def editexpopage(request, path):
             if result != html: # Check if content changed
                 try:
                     change_message = pageform.cleaned_data["change_message"]
-                    version_control.write_and_commit(filepath, result, f'{change_message} - online edit of {path}')
+                    version_control.write_and_commit([(filepath, result, "utf-8")], f'{change_message} - online edit of {path}')
                 except version_control.WriteAndCommitError as e:
                     return render(request,'errors/generic.html', {'message': e.message})
                 
diff --git a/lib/version_control.py b/lib/version_control.py
index 6343914..01bd8ba 100644
--- a/lib/version_control.py
+++ b/lib/version_control.py
@@ -1,28 +1,35 @@
 import troggle.settings as settings
 import subprocess
 
-def write_and_commit(filepath, content, message):
+def write_and_commit(files, message):
     """Writes the content to the filepath and adds and commits the file to git.  If this fails, a WriteAndCommitError is raised."""
-    cwd = filepath.parent
-    filename = filepath.name
     git = settings.GIT
-    # GIT see also core/models/cave.py writetrogglefile()
-    # GIT see also core/views/uploads.py dwgupload()
+    try:
+        for filepath, content, encoding in files:
+            cwd = filepath.parent
+            filename = filepath.name
+            # GIT see also core/models/cave.py writetrogglefile()
+            # GIT see also core/views/uploads.py dwgupload()
     
-    try:
-        with open(filepath, "w", encoding="utf8") as f:
-            print(f'WRITING{cwd}---{filename} ')
-            # as the wsgi process www-data, we have group write-access but are not owner, so cannot chmod.
-            # os.chmod(filepath, 0o664) # set file permissions to rw-rw-r--
-            f.write(content)
-    except PermissionError:
-        raise WriteAndCommitError(f'CANNOT  save this file.\nPERMISSIONS incorrectly set on server for this file {filename}. Ask a nerd to fix this.')
+            if encoding:
+                mode = "w"
+                kwargs = {"encoding": encoding}
+            else:
+                mode = "wb"
+                kwargs = {}
+            try:
+                with open(filepath, mode, **kwargs) as f:
+                    print(f'WRITING{cwd}---{filename} ')
+                    # as the wsgi process www-data, we have group write-access but are not owner, so cannot chmod.
+                    # os.chmod(filepath, 0o664) # set file permissions to rw-rw-r--
+                    f.write(content)
+            except PermissionError:
+                raise WriteAndCommitError(f'CANNOT  save this file.\nPERMISSIONS incorrectly set on server for this file {filename}. Ask a nerd to fix this.')
 
-    try:
-        cp_add = subprocess.run([git, "add", filename], cwd=cwd, capture_output=True, text=True)
-        if cp_add.returncode != 0:
-            msgdata = 'Ask a nerd to fix this.\n\n' + cp_add.stderr + '\n\n' + cp_add.stdout  + '\n\nreturn code: ' + str(cp_add.returncode)
-            raise WriteAndCommitError(f'CANNOT git on server for this file {filename}. Edits saved but not added to git.\n\n' + msgdata)
+            cp_add = subprocess.run([git, "add", filename], cwd=cwd, capture_output=True, text=True)
+            if cp_add.returncode != 0:
+                msgdata = 'Ask a nerd to fix this.\n\n' + cp_add.stderr + '\n\n' + cp_add.stdout  + '\n\nreturn code: ' + str(cp_add.returncode)
+                raise WriteAndCommitError(f'CANNOT git on server for this file {filename}. Edits saved but not added to git.\n\n' + msgdata)
        
         cp_commit = subprocess.run([git, "commit", "-m", message], cwd=cwd, capture_output=True, text=True)
         # This produces return code = 1 if it commits OK, but when the repo still needs to be pushed to origin/expoweb
diff --git a/templates/editexpopage.html b/templates/editexpopage.html
index ef3aa20..b71c91f 100644
--- a/templates/editexpopage.html
+++ b/templates/editexpopage.html
@@ -5,6 +5,8 @@
 <!--<script src="{{ settings.TINY_MCE_MEDIA_URL }}tiny_mce.js" type="text/javascript"></script>-->
 <!-- <script type="text/javascript"> tinyMCE.init({ mode : "textareas"  }); </script>-->
 
+<script src="{{ settings.MEDIA_URL }}admin/js/vendor/jquery/jquery.js" type="text/javascript"></script>
+
     <script src={{ settings.MEDIA_URL }}codemirror/codemirror.js></script>
     <script src={{ settings.MEDIA_URL }}codemirror/xml.js></script>
     <script src={{ settings.MEDIA_URL }}codemirror/javascript.js></script>
@@ -38,9 +40,82 @@
       height: 5%;
       }
     </style>
+    <style type=text/css>
+	html {
+	  font-family: "Helvetica Neue", sans-serif;
+	  width: 100%;
+	  color: #666666;
+	  text-align: center;
+	}
+
+	.popup-overlay {
+	  /*Hides pop-up when there is no "active" class*/
+	  visibility: hidden;
+	  position: absolute;
+	  background: #ffffff;
+	  border: 3px solid #666666;
+	  width: 90%;
+	  height: 80%;
+	  overflow: scroll; 
+	  left: 5%;
+	  z-index: 20;
+	}
+
+	.popup-overlay.active {
+	  /*displays pop-up when "active" class is present*/
+	  visibility: visible;
+	  text-align: center;
+	}
+
+	.popup-content {
+	  /*Hides pop-up content when there is no "active" class */
+	  visibility: hidden;
+	}
+
+	.popup-content.active {
+	  /*Shows pop-up content when "active" class is present */
+	  visibility: visible;
+	}
+
+	button {
+	  display: inline-block;
+	  vertical-align: middle;
+	  border-radius: 30px;
+	  margin: .20rem;
+	  font-size: 1rem;
+	  color: #666666;
+	  background: #ffffff;
+	  border: 1px solid #666666;
+	}
+
+	button:hover {
+	  border: 1px solid #666666;
+	  background: #666666;
+	  color: #ffffff;
+	}
+	</style>
 {% endblock %}
 {% block body %}
 <h1>Edit {{ path }}</h1>
+<!--Creates the add image popup-->
+<div class="add-image-popup popup-overlay">
+  <div class="add-image-popup popup-content">
+    <h2>Select Image</h2>
+    <p id="image_popup_content"> Loading ...</p>
+    <button onclick="new_image_popup()">Upload Image</button> 
+    <button class="close" onclick="$('.add-image-popup').removeClass('active');">Close</button> 
+  </div>
+</div>
+
+<!--Creates the new image popup-->
+<div class="new-image-popup popup-overlay">
+  <div class="new-image-popup popup-content">
+    <h2>New Image</h2>
+    <p id="new_image_popup_content"> Loading ...</p>
+    <button class="close" onclick="$('.new-image-popup').removeClass('active');">Close</button> 
+  </div>
+</div>
+
 <form action="" method="post">{% csrf_token %}
 {{ form.non_field_errors }}
 <div class="fieldWrapper">
@@ -62,6 +137,7 @@
 <button type="button" onclick="addTag('h4', '')">heading 4</button> 
 <button type="button" onclick="addTag('a', 'href=&quot;&quot;')">hyperlink</button> 
 <button type="button" onclick="addTag('p', '')">paragraph</button> 
+<button type="button" onclick="add_image_popup()">image</button> 
 <div class="fieldWrapper">
     {{ form.change_message.errors }}
     <label for="{{ form.title.id_for_label }}">Git change message:</label>
@@ -70,6 +146,60 @@
 {% include "menu.html" %}
 <p><input type="submit" value="Submit" /></p>
 </form>
+
+
+<script>
+
+
+function add_image_popup() {
+  $('.add-image-popup').addClass('active'); 
+  $('#image_popup_content').load("{% url 'image_selector' path %}", function() {
+      $('.thumbnail').click(function(){
+           $(".add-image-popup").removeClass("active");
+           addStr($( this ).attr("data-html"))
+           });
+    })
+  }
+function new_image_popup() {
+  $('.add-image-popup').removeClass('active');
+  $('.new-image-popup').addClass('active');
+  $.ajax({
+          type : "GET", 
+          dataType: "json",
+          url: "{% url 'new_image_form' path %}",
+          success: function(data){handle_new_image(data)}
+          });  
+  }
+  
+function handle_new_image(data) {
+  if (data.hasOwnProperty('form')) {
+      $('#new_image_popup_content').html(data.form);
+      $('#new_image_form').on('submit', function(e){
+         e.preventDefault();
+         data = $('#new_image_form').serialize();
+         
+         $.ajax({
+                type : "POST", 
+                dataType: "json",
+                url: "{% url 'new_image_form' path %}",
+                data: new FormData($('#new_image_form')[0]),     
+                processData: false,
+                contentType: false,
+                success: function(data){
+                   handle_new_image(data);
+                }
+            });
+         });
+    }
+  else if (data.hasOwnProperty('html')) {
+    $('.new-image-popup').removeClass('active');
+    addStr(data.html);
+    }
+  else {
+    alert(data.error);
+    }
+  }
+</script>
     <script>
       var delay;
       // Initialize CodeMirror editor with a nice html5 canvas demo.
@@ -103,5 +233,12 @@
           editor.focus();
           editor.setCursor({line: to.line , ch : to.ch + 2 + tag.length + attr.length });
       }
+      
+      function addStr(x){
+          var to = editor.getCursor(false);
+          editor.replaceRange(x, to);
+          editor.focus();
+          editor.setCursor({line: to.line , ch : to.ch + x.length });
+      }
     </script>
 {% endblock %}
diff --git a/templates/image_page_template.html b/templates/image_page_template.html
new file mode 100644
index 0000000..4a240e8
--- /dev/null
+++ b/templates/image_page_template.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
+<title>
+{{ header }}
+</title>
+<link rel="stylesheet" type="text/css" href="../../../css/main2.css" />
+</head>
+
+<body>
+<H1>{{ header }}</H1>
+<div class="centre"><img alt="" src="{{ filepath }}" />
+</div>
+
+<p>{{ description }}</p>
+
+{% if photographer %}
+<p class="caption">Photo &copy; {{ photographer }}{% if year %}, {{ year }}{% endif %}</p>
+{% endif %}
+
+<hr />
+</body>
+</html>
diff --git a/templates/image_selector.html b/templates/image_selector.html
new file mode 100644
index 0000000..5ac4e01
--- /dev/null
+++ b/templates/image_selector.html
@@ -0,0 +1,3 @@
+{% for thumbnail in thumbnails %}
+    <img class = "thumbnail" src = "{{ thumbnail.thumbnail_url }}" data-html = "{% include 'linked_image_template.html' with thumbnail_url=thumbnail.thumbnail_url page_url=thumbnail.page_url  %}"/>
+{% endfor %}
diff --git a/templates/linked_image_template.html b/templates/linked_image_template.html
new file mode 100644
index 0000000..336f1dd
--- /dev/null
+++ b/templates/linked_image_template.html
@@ -0,0 +1 @@
+<a href='{{ page_url }}'><img src='{{ thumbnail_url }}' /></a>
diff --git a/templates/new_image_form.html b/templates/new_image_form.html
new file mode 100644
index 0000000..1a2f636
--- /dev/null
+++ b/templates/new_image_form.html
@@ -0,0 +1,5 @@
+<form id="new_image_form" action="{% url 'new_image_form' path %}" method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {{ form.as_p }}
+    <input type="submit" value="Submit">
+</form>
diff --git a/urls.py b/urls.py
index da420a0..5f5a711 100644
--- a/urls.py
+++ b/urls.py
@@ -23,6 +23,7 @@ from troggle.core.views.statistics import pathsreport, stats, dataissues
 from troggle.core.views.expo import expofiles_redirect, expofilessingle, expopage, editexpopage, mediapage, map, mapfile
 from troggle.core.views.survex import survexcaveslist, survexcavesingle, svx
 from troggle.core.views.auth import expologin, expologout
+from troggle.core.views.editor_helpers import image_selector, new_image_form
 """This sets the actualurlpatterns[] and urlpatterns[] lists which django uses 
 to resolve urls - in both directions as these are declarative. 
 
@@ -190,6 +191,11 @@ trogglepatterns = [
     re_path(r'^/loser/(?P<subpath>.*)$',      mediapage, {'doc_root': settings.SURVEX_DATA},  name="mediapage"),  #  Oddly not working !?
     re_path(r'^map/map.html', map,     name="map"), # Redirects to OpenStreetMap JavaScript
     re_path(r'^map/(?P<path>.*)$',     mapfile, name="mapfile"),  # css, js, gpx
+    
+# Helpers to edit HTML
+    re_path(r'^image_selector/(?P<path>.*)', image_selector, name = 'image_selector'),
+    re_path(r'^new_image_form/(?P<path>.*)', new_image_form, name = 'new_image_form'),
+    
 
 # Final catchall which also serves expoweb handbook pages and images
     re_path(r'^(.*)$',              expopage,     name="expopage"),     # CATCHALL assumed relative to EXPOWEB