Un contrôleur ASP.NET MVC peut-il renvoyer une image?

455

Puis-je créer un contrôleur qui renvoie simplement un élément d'image?

Je voudrais acheminer cette logique via un contrôleur, chaque fois qu'une URL telle que la suivante est demandée:

www.mywebsite.com/resource/image/topbanner

Le contrôleur recherchera topbanner.pnget renverra cette image directement au client.

J'ai vu des exemples de cela où vous devez créer une vue - je ne veux pas utiliser une vue. Je veux tout faire avec juste le contrôleur.

Est-ce possible?

Jonathan
la source
1
J'ai posé une question similaire ici /programming/155906/creating-a-private-photo-gallery-using-aspnet-mvc et j'ai fini par trouver un excellent guide pour le faire. J'ai créé une classe ImageResult en suivant ce guide. https://blog.maartenballiauw.be/post/2008/05/13/aspnet-mvc-custom-actionresult.html
Vyrotek
2
Si vous souhaitez modifier l'image, utilisez le ImageResizing.Net HttpModule pour les meilleures performances. Si vous ne le faites pas, un FilePathResult ajoute seulement quelques pour cent de la surcharge. La réécriture d'URL ajoute un peu moins.
Lilith River
1
Pourquoi ne pas utiliser WebApi Controller au lieu de MVC? ApiController class
A-Sharabiani

Réponses:

534

Utilisez la méthode File des contrôleurs de base.

public ActionResult Image(string id)
{
    var dir = Server.MapPath("/Images");
    var path = Path.Combine(dir, id + ".jpg"); //validate the path for security or use other means to generate the path.
    return base.File(path, "image/jpeg");
}

Comme note, cela semble être assez efficace. J'ai fait un test où j'ai demandé l'image via le contrôleur ( http://localhost/MyController/Image/MyImage) et via l'URL directe ( http://localhost/Images/MyImage.jpg) et les résultats étaient:

  • MVC: 7,6 millisecondes par photo
  • Direct: 6,7 millisecondes par photo

Remarque: il s'agit de la durée moyenne d'une demande. La moyenne a été calculée en effectuant des milliers de demandes sur la machine locale, de sorte que les totaux ne doivent pas inclure les problèmes de latence du réseau ou de bande passante.

Brian
la source
10
Pour ceux qui abordent cette question maintenant, c'est la solution qui a le mieux fonctionné pour moi.
Clarence Klopfstein
177
Ce n'est pas un code sûr. Laisser l'utilisateur passer un nom de fichier (chemin) comme celui-ci signifie qu'il pourrait potentiellement accéder aux fichiers de n'importe où sur le serveur. Pourrait vouloir avertir les gens de ne pas l'utiliser tel quel.
Ian Mercer
7
Sauf si vous construisez les fichiers à la volée car ils sont nécessaires et les mettez en cache une fois qu'ils sont créés (c'est ce que nous faisons).
Brian
15
@ mare - vous pouvez également le faire si vous servez des fichiers à partir d'un emplacement restreint, par exemple, vous pouvez avoir des images App_Dataqui devraient être signées par certains utilisateurs de votre application mais pas par d'autres. L'utilisation d'une action de contrôleur pour les servir vous permet de restreindre l'accès.
Russ Cam
8
Comme d'autres l'ont mentionné, soyez prudent dans la création de votre chemin, car j'ai vu un code de production réel qui permettait à l'utilisateur de naviguer dans un répertoire avec une chaîne de requête ou de requête soigneusement construite: /../../../danger/someFileTheyTHoughtWasInaccessible
AaronLS
128

En utilisant la version finale de MVC, voici ce que je fais:

[AcceptVerbs(HttpVerbs.Get)]
[OutputCache(CacheProfile = "CustomerImages")]
public FileResult Show(int customerId, string imageName)
{
    var path = string.Concat(ConfigData.ImagesDirectory, customerId, "\\", imageName);
    return new FileStreamResult(new FileStream(path, FileMode.Open), "image/jpeg");
}

J'ai évidemment des informations spécifiques à l'application ici concernant la construction du chemin, mais le retour de FileStreamResult est agréable et simple.

J'ai fait des tests de performances en ce qui concerne cette action contre votre appel quotidien à l'image (en contournant le contrôleur) et la différence entre les moyennes n'était que d'environ 3 millisecondes (la moyenne du contrôleur était de 68 ms, celle du non-contrôleur était de 65 ms).

J'avais essayé certaines des autres méthodes mentionnées dans les réponses ici et les performances ont été beaucoup plus spectaculaires ... plusieurs des réponses des solutions étaient jusqu'à 6 fois supérieures au non-contrôleur (autres contrôleurs moy. 340 ms, non-contrôleur 65 ms).

Judo à voile
la source
12
Qu'en est-il de l'image n'est pas modifiée? FileStreamResult doit envoyer 304 lorsque l'image n'est pas modifiée depuis la dernière demande.
dariol
Vous pouvez utiliser à la Path.Combineplace de concat pour un code plus sûr et plus lisible.
Marcell Toth
101

Pour expliquer légèrement la réponse de Dyland:

Trois classes implémentent la classe FileResult :

System.Web.Mvc.FileResult
      System.Web.Mvc.FileContentResult
      System.Web.Mvc.FilePathResult
      System.Web.Mvc.FileStreamResult

Ils sont tous assez explicites:

  • Pour les téléchargements de chemin de fichier où le fichier existe sur le disque, utilisez FilePathResult- c'est le moyen le plus simple et vous évite d'avoir à utiliser Streams.
  • Pour les tableaux d'octets [] (semblables à Response.BinaryWrite), utilisez FileContentResult.
  • Pour les tableaux d'octets [] où vous souhaitez télécharger le fichier (content-disposition: attachment), utilisez FileStreamResultde la même manière que ci-dessous, mais avec un MemoryStreamet en utilisant GetBuffer().
  • Pour Streamsutilisation FileStreamResult. Cela s'appelle un FileStreamResult mais cela prend un Streamdonc je suppose que cela fonctionne avec un MemoryStream.

Voici un exemple d'utilisation de la technique de disposition de contenu (non testée):

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult GetFile()
    {
        // No need to dispose the stream, MVC does it for you
        string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", "myimage.png");
        FileStream stream = new FileStream(path, FileMode.Open);
        FileStreamResult result = new FileStreamResult(stream, "image/png");
        result.FileDownloadName = "image.png";
        return result;
    }
Chris S
la source
2
La partie de disposition de contenu de ce message a été extrêmement utile
Diego
VS me dit que cette surcharge de FileStream () est obsolète.
MrBoJangles
1
Quelque chose à noter: si vous avez une virgule dans votre nom de fichier, Chrome la rejettera avec une erreur "trop ​​d'en-têtes reçus". Remplacez donc toutes les virgules par un "-" ou "".
Chris S
Comment pourrait-on faire cela en utilisant uniquement des contrôleurs Web API?
Zapnologica
74

Cela peut être utile si vous souhaitez modifier l'image avant de la renvoyer:

public ActionResult GetModifiedImage()
{
    Image image = Image.FromFile(Path.Combine(Server.MapPath("/Content/images"), "image.png"));

    using (Graphics g = Graphics.FromImage(image))
    {
        // do something with the Graphics (eg. write "Hello World!")
        string text = "Hello World!";

        // Create font and brush.
        Font drawFont = new Font("Arial", 10);
        SolidBrush drawBrush = new SolidBrush(Color.Black);

        // Create point for upper-left corner of drawing.
        PointF stringPoint = new PointF(0, 0);

        g.DrawString(text, drawFont, drawBrush, stringPoint);
    }

    MemoryStream ms = new MemoryStream();

    image.Save(ms, System.Drawing.Imaging.ImageFormat.Png);

    return File(ms.ToArray(), "image/png");
}
staromeste
la source
1
Je vous remercie. C'est parfait pour le scénario où un proxy est nécessaire pour télécharger une image nécessitant une authentification qui ne peut pas être effectuée côté client.
Hong
1
Vous oubliez de disposer d'un énorme 3 objets natifs: Font, SolidBrush et Image.
Wout
3
Amélioration suggérée ici: vous créez un flux de mémoire, écrivez les données, puis créez un résultat File avec les données à l'aide de .ToArray () Vous pouvez également simplement appeler ms.Seek (0, SeekOrigin.Begin), puis renvoyer File (ms, " image / png ") // retourne le flux lui
Quango
12

Vous pouvez créer votre propre extension et procéder de cette façon.

public static class ImageResultHelper
{
    public static string Image<T>(this HtmlHelper helper, Expression<Action<T>> action, int width, int height)
            where T : Controller
    {
        return ImageResultHelper.Image<T>(helper, action, width, height, "");
    }

    public static string Image<T>(this HtmlHelper helper, Expression<Action<T>> action, int width, int height, string alt)
            where T : Controller
    {
        var expression = action.Body as MethodCallExpression;
        string actionMethodName = string.Empty;
        if (expression != null)
        {
            actionMethodName = expression.Method.Name;
        }
        string url = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection).Action(actionMethodName, typeof(T).Name.Remove(typeof(T).Name.IndexOf("Controller"))).ToString();         
        //string url = LinkBuilder.BuildUrlFromExpression<T>(helper.ViewContext.RequestContext, helper.RouteCollection, action);
        return string.Format("<img src=\"{0}\" width=\"{1}\" height=\"{2}\" alt=\"{3}\" />", url, width, height, alt);
    }
}

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();
        context.HttpContext.Response.ContentType = GetMimeType(ImageFormat);
        Image.Save(context.HttpContext.Response.OutputStream, ImageFormat);
    }

    private static string GetMimeType(ImageFormat imageFormat)
    {
        ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
        return codecs.First(codec => codec.FormatID == imageFormat.Guid).MimeType;
    }
}
public ActionResult Index()
    {
  return new ImageResult { Image = image, ImageFormat = ImageFormat.Jpeg };
    }
    <%=Html.Image<CapchaController>(c => c.Index(), 120, 30, "Current time")%>
Oleksandr Fentsyk
la source
10

Vous pouvez écrire directement dans la réponse mais elle n'est pas testable. Il est préférable de renvoyer un ActionResult dont l'exécution a été différée. Voici mon StreamResult réutilisable:

public class StreamResult : ViewResult
{
    public Stream Stream { get; set; }
    public string ContentType { get; set; }
    public string ETag { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.ContentType = ContentType;
        if (ETag != null) context.HttpContext.Response.AddHeader("ETag", ETag);
        const int size = 4096;
        byte[] bytes = new byte[size];
        int numBytes;
        while ((numBytes = Stream.Read(bytes, 0, size)) > 0)
            context.HttpContext.Response.OutputStream.Write(bytes, 0, numBytes);
    }
}
JarrettV
la source
9

Pourquoi ne pas aller simple et utiliser l' ~opérateur tilde ?

public FileResult TopBanner() {
  return File("~/Content/images/topbanner.png", "image/png");
}
JustinStolle
la source
6

MISE À JOUR: Il y a de meilleures options que ma réponse d'origine. Cela fonctionne assez bien en dehors de MVC mais il est préférable de s'en tenir aux méthodes intégrées de retour du contenu de l'image. Voir les réponses votées.

Vous le pouvez certainement. Essayez ces étapes:

  1. Charger l'image du disque dans un tableau d'octets
  2. mettre l'image en cache dans le cas où vous attendez plus de demandes pour l'image et ne voulez pas les E / S du disque (mon échantillon ne la cache pas ci-dessous)
  3. Modifiez le type MIME via Response.ContentType
  4. Response.BinaryWrite le tableau d'octets d'image

Voici un exemple de code:

string pathToFile = @"C:\Documents and Settings\some_path.jpg";
byte[] imageData = File.ReadAllBytes(pathToFile);
Response.ContentType = "image/jpg";
Response.BinaryWrite(imageData);

J'espère que cela pourra aider!

Ian Suttle
la source
4
et à quoi cela ressemblerait-il dans l'action du contrôleur?
CRice
5

Solution 1: pour rendre une image dans une vue à partir d'une URL d'image

Vous pouvez créer votre propre méthode d'extension:

public static MvcHtmlString Image(this HtmlHelper helper,string imageUrl)
{
   string tag = "<img src='{0}'/>";
   tag = string.Format(tag,imageUrl);
   return MvcHtmlString.Create(tag);
}

Ensuite, utilisez-le comme:

@Html.Image(@Model.ImagePath);

Solution 2: pour rendre l'image à partir de la base de données

Créer une méthode de contrôleur qui renvoie les données d'image comme ci-dessous

public sealed class ImageController : Controller
{
  public ActionResult View(string id)
  {
    var image = _images.LoadImage(id); //Pull image from the database.
    if (image == null) 
      return HttpNotFound();
    return File(image.Data, image.Mime);
  }
}

Et utilisez-le dans une vue comme:

@ { Html.RenderAction("View","Image",new {id=@Model.ImageId})}

Pour utiliser une image rendue à partir de ce résultat d'action dans n'importe quel code HTML, utilisez

<img src="http://something.com/image/view?id={imageid}>
Ajay Kelkar
la source
5

Cela a fonctionné pour moi. Depuis que je stocke des images sur une base de données SQL Server.

    [HttpGet("/image/{uuid}")]
    public IActionResult GetImageFile(string uuid) {
        ActionResult actionResult = new NotFoundResult();
        var fileImage = _db.ImageFiles.Find(uuid);
        if (fileImage != null) {
            actionResult = new FileContentResult(fileImage.Data,
                fileImage.ContentType);
        }
        return actionResult;
    }

Dans l'extrait ci _db.ImageFiles.Find(uuid)- dessus, vous recherchez l'enregistrement du fichier image dans la base de données (contexte EF). Il renvoie un objet FileImage qui n'est qu'une classe personnalisée que j'ai créée pour le modèle, puis l'utilise comme FileContentResult.

public class FileImage {
   public string Uuid { get; set; }
   public byte[] Data { get; set; }
   public string ContentType { get; set; }
}
hmojica
la source
4

vous pouvez utiliser File pour renvoyer un fichier comme View, Content etc.

 public ActionResult PrintDocInfo(string Attachment)
            {
                string test = Attachment;
                if (test != string.Empty || test != "" || test != null)
                {
                    string filename = Attachment.Split('\\').Last();
                    string filepath = Attachment;
                    byte[] filedata = System.IO.File.ReadAllBytes(Attachment);
                    string contentType = MimeMapping.GetMimeMapping(Attachment);

                    System.Net.Mime.ContentDisposition cd = new System.Net.Mime.ContentDisposition
                    {
                        FileName = filename,
                        Inline = true,
                    };

                    Response.AppendHeader("Content-Disposition", cd.ToString());

                    return File(filedata, contentType);          
                }
                else { return Content("<h3> Patient Clinical Document Not Uploaded</h3>"); }

            }
Avinash Urs
la source
3

Regardez ContentResult. Cela renvoie une chaîne, mais peut être utilisé pour créer votre propre classe de type BinaryResult.

leppie
la source
2
if (!System.IO.File.Exists(filePath))
    return SomeHelper.EmptyImageResult(); // preventing JSON GET/POST exception
else
    return new FilePathResult(filePath, contentType);

SomeHelper.EmptyImageResult()devrait revenir FileResultavec l'image existante (1x1 transparent, par exemple).

C'est le moyen le plus simple si vous avez des fichiers stockés sur le disque local. Si les fichiers sont byte[]ou stream- alors utilisez FileContentResultou FileStreamResultcomme Dylan l'a suggéré.

Victor Gelmutdinov
la source
1

Je vois deux options:

1) Implémentez votre propre IViewEngine et définissez la propriété ViewEngine du contrôleur que vous utilisez sur votre ImageViewEngine dans la méthode "image" souhaitée.

2) Utilisez une vue :-). Modifiez simplement le type de contenu, etc.

Matt Mitchell
la source
1
Cela pourrait être un problème en raison d'espaces supplémentaires ou de CRLF dans la vue.
Elan Hasson
2
J'avais tort dans mon dernier message ... msdn.microsoft.com/en-us/library/… Vous pouvez utiliser la classe WebImage et WebImage.Write dans une vue :)
Elan Hasson
1

Vous pouvez utiliser HttpContext.Response et y écrire directement le contenu (WriteFile () peut fonctionner pour vous), puis renvoyer ContentResult de votre action au lieu d'ActionResult.

Avis de non-responsabilité: je n'ai pas essayé cela, il est basé sur la recherche des API disponibles. :-)

Franci Penov
la source
1
Oui, je viens de remarquer que ContentResult ne prend en charge que les chaînes, mais il est assez facile de créer votre propre classe basée sur ActionResult.
leppie
1

Le code ci-dessous utilise System.Drawing.Bitmappour charger l'image.

using System.Drawing;
using System.Drawing.Imaging;

public IActionResult Get()
{
    string filename = "Image/test.jpg";
    var bitmap = new Bitmap(filename);

    var ms = new System.IO.MemoryStream();
    result.Save(ms, ImageFormat.Jpeg);
    ms.Position = 0;
    return new FileStreamResult(ms, "image/jpeg");
}
Youngjae
la source
0

J'ai également rencontré des exigences similaires,

Donc, dans mon cas, je fais une demande au contrôleur avec le chemin du dossier d'image, qui en retour renvoie un objet ImageResult.

L'extrait de code suivant illustre le travail:

var src = string.Format("/GenericGrid.mvc/DocumentPreviewImageLink?fullpath={0}&routingId={1}&siteCode={2}", fullFilePath, metaInfo.RoutingId, da.SiteCode);

                if (enlarged)
                    result = "<a class='thumbnail' href='#thumb'>" +
                        "<img src='" + src + "' height='66px' border='0' />" +
                        "<span><img src='" + src + "' /></span>" +
                        "</a>";
                else
                    result = "<span><img src='" + src + "' height='150px' border='0' /></span>";

Et dans le contrôleur à partir du chemin de l'image, je produis l'image et la renvoie à l'appelant

try
{
  var file = new FileInfo(fullpath);
  if (!file.Exists)
     return string.Empty;


  var image = new WebImage(fullpath);
  return new ImageResult(new MemoryStream(image.GetBytes()), "image/jpg");


}
catch(Exception ex)
{
  return "File Error : "+ex.ToString();
}
Shriram Navaratnalingam
la source
0

Lisez l'image, convertissez-la en byte[], puis retournez un File()avec un type de contenu.

public ActionResult ImageResult(Image image, ImageFormat format, string contentType) {
  using (var stream = new MemoryStream())
    {
      image.Save(stream, format);
      return File(stream.ToArray(), contentType);
    }
  }
}

Voici les utilisations:

using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using Microsoft.AspNetCore.Mvc;
facepalm42
la source