0

I am looking for support in uploading an image (as byte!) into a DB along with other properties. I have to admit, being a newbie to programming I was struggling quite a bit to get this done, however, I kind of got it to work. Now my issue is that whenever I try to leverage the model, it would stop working with an error message popping up saying the following:

The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.

ViewModel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.ComponentModel.DataAnnotations;

namespace errandomWeb.Models
{
    public class PhotoCompetition
    {
        public int ID { get; set; }

        public string UserID { get; set; }

        public string FirstName { get; set; }

        public string Email { get; set; }

        public byte[] CompetitionPicture { get; set; }

        //[Required]
        [Display(Name = "by checking this box I accept the Terms & Conditions")]
        //[CheckBoxRequired(ErrorMessage = "Please accept our Terms & Conditions to participate.")]
        public bool TermsAndConditionsAccepted { get; set; }

        public DateTime TimeStamp { get; set; }
    }
}

View:

@model errandomWeb.Models.PhotoCompetition
@{
    ViewBag.Title = "Become Our Model";
}
<div id="photoCompetitionContainer" class="manageContainer">
    <div id="photoCompetitionHeaderSection" class="manageHeaderSection">
        <h1 id="photoCompetitionHeaderTitle" class="manageHeaderTitle">
            @ViewBag.Title
        </h1>
        <img id="photoCompetitionHeaderProfilePicture" class="manageHeaderProfilePicture" src="@Url.Action("UserPicture", "Manage")" />
        <p id="photoCompetitionHeaderPersonalizationGeneric" class="manageHeaderPersonalization">
            Hello
        </p>
        <p id="photoCompetitionHeaderPersonalizationName" class="manageHeaderPersonalization">
            @Html.TextBoxFor(m => m.FirstName, new { @id = "photoCompetitionHeaderUserName", @class = "manageHeaderUserName", @placeholder = "Stranger", @disabled = true })
        </p>
    </div>
    @Html.Partial("_ProfileLogout")
    <div id="photoCompetitionContextSection" class="manageContextSection">
        <p id="photoCompetitionContext" class="manageContext">
            Want to become our model?
        </p>
    </div>
    <div id="photoCompetitionValidationSection" class="manageValidation">
        @Html.ValidationSummary("", new { @id = "photoCompetitionValidation", @class = "manageValidation" })
    </div>
    <section id="photoCompetition" class="manageForm">
        @using (Html.BeginForm("UploadCompetitionPicture", "errandom", FormMethod.Post, new { @id = "photoCompetitionForm", @class = "form-horizontal", @role = "form", @enctype = "multipart/form-data" }))
        {
            @Html.AntiForgeryToken()
            <div id="photoCompetitionSection" class="manageSection">
                <p id="photoCompetitionSectionTitle" class="manageSectionTitle">
                    Upload your picture and be selected as our model!
                </p>
                @Html.HiddenFor(m => m.UserID)
                @Html.HiddenFor(m => m.Email)
                @Html.HiddenFor(m => m.FirstName)
                @Html.HiddenFor(m => m.TimeStamp)
                <div id="photoCompetitionProfilePictureArea" class="manageArea row">
                    @Html.LabelFor(m => m.CompetitionPicture, new { @id = "photoCompetitionProfilePictureLabel", @class = "manageLabel col-xs-offset-1 col-xs-10 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-3 col-lg-offset-1 col-lg-4" })
                    <a id="photoCompetitionProfilePictureSelectionButton" class="manageField col-xs-offset-1 col-xs-10 col-sm-offset-1 col-sm-10 col-md-offset0 col-md-7 col-lg-offset-0 col-lg-6" href="#">
                        select a file...
                    </a>
                    @Html.TextBoxFor(m => m.CompetitionPicture, new { @id = "photoCompetitionProfilePictureField", @class = "manageField col-xs-offset-1 col-xs-10 col-sm-offset-1 col-sm-10 col-md-offset-0 col-md-7 col-lg-offset-0 col-lg-6", @type = "file", @style = "display: none" })
                </div>
                <div id="photoCompetitionTermsAndConditionsArea" class="manageArea row">
                    @Html.CheckBoxFor(m => m.TermsAndConditionsAccepted, new { @id = "photoCompetitionTermsAndConditionsField", @class = "photoCompetitionTermsAndConditionsField" })
                    @Html.LabelFor(m => m.TermsAndConditionsAccepted, new { @id = "photoCompetitionTermsAndConditionsLabel", @class = "photoCompetitionTermsAndConditionsLabel" })
                    @Html.ValidationMessageFor(m => m.TermsAndConditionsAccepted, "", new { @id = "photoCompetitionTermsAndConditionsValidation", @class = "manageValidation col-xs-offset-1 col-xs-10 col-sm-offset-1 col-sm-10 col-md-offset-4 col-md-7 col-lg-offset-5 col-lg-6" })
                </div>
                <script>
                    jQuery("#photoCompetitionProfilePictureSelectionButton").click(function () {
                        $("#photoCompetitionProfilePictureField").click();
                    });
                </script>
                <script>
                    $("#photoCompetitionProfilePictureField").change(function () {
                        var fullFileName = $("#photoCompetitionProfilePictureField").val()
                        $("#photoCompetitionProfilePictureSelectionButton").html(fullFileName.substr(fullFileName.lastIndexOf('\\') + 1));
                    });
                </script>
                <div id="photoCompetitionCroppingArea" class="manageArea row">
                    <img id="photoCompetitionOriginal" class="photoCompetitionImage" src="" alt="" style="display: none" />
                    <canvas id="photoCompetitionCropped" class="photoCompetitionImage" height="5" width="5"></canvas>
                </div>
                <div id="photoCompetitionButtonArea" class="manageArea row">
                    <input id="photoCompetitionButtonCrop" class="manageButton col-xs-offset-1 col-xs-10 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-10 col-lg-offset-1 col-lg-10" type="button" value="Crop" style="display: none" />
                    <input id="photoCompetitionButtonUpload" class="manageButton col-xs-offset-1 col-xs-10 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-10 col-lg-offset-1 col-lg-10" type="submit" value="Save" style="display: none" />
                    <input id="photoCompetitionCropX" class="photoCompetitionData" name="photoCompetitionCropX" type="hidden" />
                    <input id="photoCompetitionCropY" class="photoCompetitionData" name="photoCompetitionCropY" type="hidden" />
                    <input id="photoCompetitionCropW" class="photoCompetitionData" name="photoCompetitionCropW" type="hidden" />
                    <input id="photoCompetitionCropH" class="photoCompetitionData" name="photoCompetitionCropH" type="hidden" />
                    <input id="photoCompetitionCroppedPicture" class="photoCompetitionData" name="photoCompetitionCroppedPicture" type="hidden" />
                </div>
            </div>
        }
    </section>
    <div id="photoCompetitionReturnToMenuSection" class="manageReturnToMenuSection">
        @Html.ActionLink("Return to Menu", "Index", "", htmlAttributes: new { @id = "photoCompetitionReturnToMenuButton", @class = "manageReturnToMenuButton" })
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script type="text/javascript" src="https://cdn.rawgit.com/tapmodo/Jcrop/master/js/jquery.Jcrop.min.js"></script>
    <script type="text/javascript">
        $(function () {
            if ($('#photoCompetitionCroppingArea').width() > 700) {
                $('#photoCompetitionProfilePictureField').change(function () {
                    $('#photoCompetitionOriginal').hide();
                    var reader = new FileReader();
                    reader.onload = function (e) {
                        $('#photoCompetitionOriginal').show();
                        $('#photoCompetitionOriginal').attr("src", e.target.result);
                        $('#photoCompetitionOriginal').Jcrop({
                            onChange: SetCoordinates,
                            onSelect: SetCoordinates,
                            aspectRatio: 1,
                            boxWidth: 600,
                            addClass: 'photoCompetitionCropping'
                        });
                    }
                    reader.readAsDataURL($(this)[0].files[0]);
                });
            }
            else {
                $('#photoCompetitionProfilePictureField').change(function () {
                    $('#photoCompetitionOriginal').hide();
                    var reader = new FileReader();
                    reader.onload = function (e) {
                        $('#photoCompetitionOriginal').show();
                        $('#photoCompetitionOriginal').attr("src", e.target.result);
                        $('#photoCompetitionOriginal').Jcrop({
                            onChange: SetCoordinates,
                            onSelect: SetCoordinates,
                            aspectRatio: 1,
                            boxWidth: 250,
                            addClass: 'photoCompetitionCropping'
                        });
                    }
                    reader.readAsDataURL($(this)[0].files[0]);
                });
            }
            $('#photoCompetitionButtonCrop').click(function () {
                var x1 = $('#photoCompetitionCropX').val();
                var y1 = $('#photoCompetitionCropY').val();
                var height = $('#photoCompetitionCropH').val();
                var width = $('#photoCompetitionCropW').val();
                var canvas = $("#photoCompetitionCropped")[0];
                var context = canvas.getContext('2d');
                var img = new Image();
                img.onload = function () {
                    canvas.height = height;
                    canvas.width = width;
                    context.drawImage(img, x1, y1, width, height, 0, 0, width, height);
                    $('#photoCompetitionCroppedPicture').val(canvas.toDataURL());
                    $('#photoCompetitionButtonUpload').show();
                    $('#photoCompetitionCropped').hide();
                    $('#photoCompetitionButtonCrop').hide();
                };
                img.src = $('#photoCompetitionOriginal').attr("src");
            });
        });
        function SetCoordinates(c) {
            $('#photoCompetitionCropX').val(c.x);
            $('#photoCompetitionCropY').val(c.y);
            $('#photoCompetitionCropW').val(c.w);
            $('#photoCompetitionCropH').val(c.h);
            $('#photoCompetitionButtonCrop').show();
        };
    </script>
}

Controller:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using errandomWeb.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;

namespace errandomWeb.Controllers
{
    [Authorize]
    public class errandomController : Controller
    {
        private ApplicationDbContext DB = new ApplicationDbContext();

        // GET: /errandom/PhotoCompetition
        public ActionResult PhotoCompetition()
        {
            var model = new PhotoCompetition
            {
                UserID = User.Identity.GetUserId(),
                Email = User.Identity.GetUserName(),
                FirstName = User.Identity.Name,
                TimeStamp = DateTime.UtcNow.ToUniversalTime()
            };

            return View(model);
        }

        // POST: /errandom/PhotoCompetition
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see https://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult UploadCompetitionPicture()
        {
            string croppedImage = Request.Form["photoCompetitionCroppedPicture"];
            byte[] imageBytes = Convert.FromBase64String(croppedImage.Split(',')[1]);
            var userId = User.Identity.GetUserId();
            var participation = new PhotoCompetition
            {
                CompetitionPicture = imageBytes,
                UserID = User.Identity.GetUserId(),
                FirstName = "testcase",
                Email = User.Identity.GetUserName(),
                TermsAndConditionsAccepted = false,
                TimeStamp = DateTime.UtcNow.ToUniversalTime(),
            };
            DB.PhotoCompetition.Add(participation);
            DB.SaveChanges();
            return View("Edit");
        }
    }
}

I would like to include

TermsAndConditionsAccepted = model.TermsAndConditionsAccepted

so the value is actually being taken from the form a user would fill out, but for aforementioned reasons I am encountering issues.

Appreciate any help I am getting here, thx!

Ken-F
  • 152
  • 1
  • 12

2 Answers2

1

The error has nothing to do with EF, it is in this line, in the call to Convert method in the controller:

byte[] imageBytes = Convert.FromBase64String(croppedImage.Split(',')[1]);

There is something wrong in the results of the Split. Try to inspect that value and see how it looks like.

What can be happening? The value you are trying to decode as base64 is generated in the client javascript in this line, by the function toDataURL():

$('#photoCompetitionCroppedPicture').val(canvas.toDataURL());

The returned value includes a prefix depending on the image format. The default format used by this function is png, so it returns a value like this:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby...

The real value is after the prefix, separated by a comma, that is why you use the split when reading it in your controller.

But perhaps the format prefix you receive is different and your split is not getting the right value. Try to debug and see the real value, by doing something like this:

console.log(canvas.toDataURL()); 

Perhaps the value you get in the server is not the same that you generated in the client (it may be truncated, for instance). Compare a console trace of the value in the client (generated with toDataURL) and the value in the server (the contents of your variable croppedImage, or the values after doing the Split).

As a final note, try to use a regular expression instead of a Split by comma separator to remove the unwanted prefix from your base64 value. It will be safer. Here you have some examples of expressions to do this.

More info on toDataURL function here.

Diana
  • 2,186
  • 1
  • 20
  • 31
  • Thx Diana, appreciate your support and guidance! What seems weird is the fact that whenever I am not using a model, saving the image would work seamlessly... so can't really follow why there is an issue with the split in the scenario with the model and no issue when not using the model? – Ken-F Feb 05 '18 at 12:09
  • I have followed your recommendation of using a regular expression in my script, however, it does not change a thing; as soon as I change the controller to use the model, I get the very same error saying that the input is not a valid base64 string. Any suggestions?? – Ken-F Feb 05 '18 at 14:42
  • I have worked a bit more on the issue, finding that the picture that is uploaded without using the model `public ActionResult UploadCompetitionPicture(PhotoCompetition model) ` works fine as is in my above snippet.. so the issue is not with that but rather when i try to use the model to write other stuff to the DB... any suggestions? – Ken-F Feb 05 '18 at 15:52
  • 1
    If you inspect the value received in the controller in the variable `croppedImage`, do you see any difference when changing the model and when not changing it? Perhaps you could paste here the value of `croppedImage` (or at least the beginning of the string) to try to see what is happening. I can't think of any EF related thing that would cause that exception, that is related to `Convert.FromBase64String`. – Diana Feb 05 '18 at 16:18
0

I finally got it done. Here's the updated controller code:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult UploadCompetitionPicture([Bind(Exclude = "CompetitionPicture")]PhotoCompetition model)
{
    string croppedImage = Request.Form["photoCompetitionCroppedPicture"];
    byte[] imageBytes = Convert.FromBase64String(croppedImage);
    var userId = User.Identity.GetUserId();
    var participation = new PhotoCompetition
    {
        UserID = User.Identity.GetUserId(),
        FirstName = "fuckingtest",
        Email = User.Identity.GetUserName(),
        TermsAndConditionsAccepted = true,
        TimeStamp = DateTime.UtcNow.ToUniversalTime(),
    };
    participation.CompetitionPicture = imageBytes;
    DB.PhotoCompetition.Add(participation);
    DB.SaveChanges();
    return View("Edit");
}

I guess, what made the difference is excluding the picture in the first place, then submitting all other fields and after that adding the picture separately. Not sure why, but glad I figured it out. Thx anyone who contributed!

Ken-F
  • 152
  • 1
  • 12
  • 1
    Glad you managed. Usually it is a good practice not using the same class for your view model and your EF data model (that is, having something like PhotoCompetitionDto and PhotoCompetition). With this you wouldn't need the exclude, just don't define the CompetitionPicture property in your dto as byte array, but as a string, and then do the Convert when setting the value in your model `PhotoCompetition.CompetitionPicture`. It is a better design too, as you don't need to expose your internal model details to the view. – Diana Feb 05 '18 at 17:14
  • 1
    Some arguments in favor of using DTOs: https://learn.microsoft.com/en-us/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5 – Diana Feb 05 '18 at 17:17