30 November 2009 ASP.NET, ASP.NET MVC, Crop, jCrop, jQuery, MVC Robert Muehsig

image Auf vielen Seiten kann man Profilbilder hinterlegen. Meinstens müssen diese eine bestimmte Größe haben, ansonsten wird das Bild gestaucht oder gezerrt. Beides eher suboptimal. Mit jCrop gibt es ein kleines, nützliches jQuery Plugin, welches man benutzen kann um bestimmte Ausschnitte aus einem Bild auszuschneiden. jCrop macht dies Client-Seitig und ich möchte das "ausgeschnittene" Bild nun auch in ASP.NET MVC verarbeiten...

ASP.NET jCrop Control

Auf Codeplex gibt es bereits ein vorgefertiges ASP.NET Control. Evtl. funktioniert das Control auch in ASP.NET MVC, dies habe ich nicht getestet, da ich den "Cropping" Code bei mir in ein Controller-Action getan habe. Daher schonmal großen Dank an Cem Sisman.

Schritt für Schritt:

1. Grundlagen

- ASP.NET MVC Projekt anlegen
- jQuery in der Site.Master hinzufügen
- in der Site.Master noch ein ContentPlaceHolder für Javascript hinzufügen (dort kommt der Javascript Teil von jCrop hin).


2. jCrop runterladen

image

Nun benötigt man ein Bild (ich hab ein Beispielbild von jCrop genommen), die jquery.Jcrop.css Datei und die jquery.JCrop.min.js Javascript Datei.

Die Javascript und CSS Datei muss natürlich in der Site.Master noch mit angegeben werden.

 

 

 

 

 

 

5. Der "View" - HTML

Wir stellen uns vor, dass Bild wäre schon hochgeladen und wir möchten jetzt ein Ausschnitt auswählen. Dem Benutzer zeigen wir direkt eine kleine Preview an, so wie hier.

Im ersten "p" ist das Originalbild. Im zweiten "p" kommt die Preview hin. Die Styleattribute beim p werden benötigt, da ansonsten das Bild "rausragen würde. So beschränken wir es auf 100px.

Was macht jCrop eigentlich?

jCrop zeigt in der Preview nur den ausgewählten Bereich an. Das Ursprungsbild wird hierbei nicht verändern. Stattdessen wir nur der Sichtbereich geändert.

Unter "Form Data" kommt für die Verarbeitung dann die wichtigen Daten: X/Y als Startkoordinaten und eine Höhe und Breite. Diese Daten schicken wir per POST an die PostPicture Methode.

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Crop</h2>
    
    <p>
        <!-- "Original" -->
        <img id="cropbox" src="<%=Url.Content("~/Content/flowers.jpg") %>" />
    </p>
    <p style="width:100px;height:100px;overflow:hidden;">
        <!-- Crop Preview -->
        <img id="preview" src="<%=Url.Content("~/Content/flowers.jpg") %>" />
    </p>
    <p>
        <!-- Form Data -->
        <form action="<%=Url.Action("PostPicture") %>" method="post">
            <input type="hidden" id="x" name="x" />
            <input type="hidden" id="y" name="y" />
            <input type="hidden" id="w" name="w" />
            <input type="hidden" id="h" name="h" />
        <button type="submit">Send</button>
        </form>
    </p>
</asp:Content>

5. Der View - der Javascript Teil

Sobald die Seite geladen wird, werden dem original Bild "Eventhandler" angehangen. Hier kann man auch alle Optionen die man möchte mit angeben.

In der "showPreview" Funktion wird als Parameter die Koordinaten des gezeichneten Rahmens übergeben. In der if-Abfrage wird das Preview Bild anhand von CSS Eigenschaften "erzeugt" und im letzten Abschnitt werden die 4 Werte in die Inputfelder geschrieben: X,Y,W,H.

		<script language="Javascript">

			// Remember to invoke within jQuery(window).load(...)
			// If you don't, Jcrop may not initialize properly
			jQuery(window).load(function(){

				jQuery('#cropbox').Jcrop({
					onChange: showPreview,
					onSelect: showPreview,
					aspectRatio: 1
				});

			});

			// Our simple event handler, called from onChange and onSelect
			// event handlers, as per the Jcrop invocation above
			function showPreview(coords)
			{
				if (parseInt(coords.w) > 0)
				{
					var rx = 100 / coords.w;
					var ry = 100 / coords.h;

					jQuery('#preview').css({
						width: Math.round(rx * 500) + 'px',
						height: Math.round(ry * 370) + 'px',
						marginLeft: '-' + Math.round(rx * coords.x) + 'px',
						marginTop: '-' + Math.round(ry * coords.y) + 'px'
					});
	            }

	            $('#x').val(coords.x);
	            $('#y').val(coords.y);
	            $('#w').val(coords.w);
	            $('#h').val(coords.h);
			}

		</script>

6. Der Controller - Crop

Die 4 Werte kommen in die Methode rein. Dann hole ich mir mein Bild, welches in diesem Fall statisch im Code drin steht. Dann erzeuge ich mir ein Rechteck der neuen größe und male das Ursprungsbild mit DrawImage auf die neue Fläche.

        public ImageResult PostPicture(int x, int y, int h, int w)
        {
            Image image = Image.FromFile(Path.Combine(this.Request.PhysicalApplicationPath,"Content/flowers.jpg"));
            Bitmap cropedImage = new Bitmap(w, h, image.PixelFormat);
            Graphics g = Graphics.FromImage(cropedImage);

            Rectangle rec = new Rectangle(0, 0,
                                w,
                                h);

            g.DrawImage(image, rec, x, y, w, h, GraphicsUnit.Pixel);
            image.Dispose();
            g.Dispose();

            return new ImageResult { Image = cropedImage, ImageFormat = ImageFormat.Jpeg }; 
        }

ImageResult? Was ist das denn?

Ich möchte am Ende einfach nur das Bild anzeigen, dabei soll das Bild nicht auf der Platte gespeichert sein. Man könnte es nun direkt in den Stream schreiben oder man nutzt eine kleine Hilfsklasse von Maarten Balliauw: Das ImageResult. Das macht die ganze Sache etwas sauberer IMHO. Natürlich kann man auch einen View oder etwas anderes zurückgeben.

ImageResult:

public class ImageResult : ActionResult
{
    public ImageResult() { }

    public Image Image { get; set; }
    public ImageFormat ImageFormat { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        // verify properties
        if (Image == null)
        {
            throw new ArgumentNullException("Image");
        }
        if (ImageFormat == null)
        {
            throw new ArgumentNullException("ImageFormat");
        }

        // output
        context.HttpContext.Response.Clear();

        if (ImageFormat.Equals(ImageFormat.Bmp)) context.HttpContext.Response.ContentType = "image/bmp";
        if (ImageFormat.Equals(ImageFormat.Gif)) context.HttpContext.Response.ContentType = "image/gif";
        if (ImageFormat.Equals(ImageFormat.Icon)) context.HttpContext.Response.ContentType = "image/vnd.microsoft.icon";
        if (ImageFormat.Equals(ImageFormat.Jpeg)) context.HttpContext.Response.ContentType = "image/jpeg";
        if (ImageFormat.Equals(ImageFormat.Png)) context.HttpContext.Response.ContentType = "image/png";
        if (ImageFormat.Equals(ImageFormat.Tiff)) context.HttpContext.Response.ContentType = "image/tiff";
        if (ImageFormat.Equals(ImageFormat.Wmf)) context.HttpContext.Response.ContentType = "image/wmf";

        Image.Save(context.HttpContext.Response.OutputStream, ImageFormat);
    }
}

Ergebnis:

Startansicht

image

Einen Teil ausschneiden:

image

Nach dem Drücken auf "Send":

image

Wenn man das nun noch mit einen fetzigen Uploader verbindet, wird das sogar richtig praktisch ;)

[ Download Democode ]

Update: Für VB.NET Entwickler

Daniel Klein hat den Code im Controller für VB.NET Entwickler umgeschrieben - funktionieren müsste er noch gleich ;)

Dim x As Integer
Dim y As Integer
Dim w As Integer
Dim h As Integer

x = Request.Form("x")
y = Request.Form("y")
w = Request.Form("w")
h = Request.Form("h")

Dim image As Image = System.Drawing.Image.FromFile(Path.Combine(Me.Request.PhysicalApplicationPath, "upload/fotoalbum/gross/" + Labelbild.Text))
Dim cropedImage As Bitmap = New Bitmap(w, h, image.PixelFormat)
Dim g As Graphics = Graphics.FromImage(cropedImage)
Dim rec As Rectangle = New Rectangle(0, 0, w, h)
g.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic
g.DrawImage(image, rec, x, y, w, h, GraphicsUnit.Pixel)

cropedImage.Save(Server.MapPath("~/upload/fotoalbum/klein/" + Labelbild.Text), ImageFormat.Jpeg)


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!