Czy kontroler ASP.NET MVC może zwrócić obraz?

455

Czy mogę utworzyć kontroler, który po prostu zwraca zasób obrazu?

Chciałbym przekierować tę logikę przez kontroler, ilekroć wymagany jest adres URL taki jak poniżej:

www.mywebsite.com/resource/image/topbanner

Kontroler wyszuka topbanner.pngi wyśle ​​ten obraz bezpośrednio z powrotem do klienta.

Widziałem przykłady tego, w których musisz utworzyć widok - nie chcę używać widoku. Chcę to wszystko zrobić tylko z kontrolerem.

czy to możliwe?

Jonathan
źródło
1
Zadałem podobne pytanie tutaj /programming/155906/creating-a-private-photo-gallery-using-aspnet-mvc i znalazłem świetny przewodnik, aby to zrobić. Utworzyłem klasę ImageResult zgodnie z tym przewodnikiem. https://blog.maartenballiauw.be/post/2008/05/13/aspnet-mvc-custom-actionresult.html
Vyrotek
2
Jeśli chcesz zmodyfikować obraz, użyj ImageResizing.Net HttpModule, aby uzyskać najlepszą wydajność. Jeśli nie, FilePathResult dodaje tylko kilka procent kosztów ogólnych. Przepisywanie adresów URL dodaje nieco mniej.
Lilith River,
1
Dlaczego nie użyć kontrolera WebApi zamiast MVC? ApiController class
A-Sharabiani

Odpowiedzi:

534

Użyj metody pliku kontrolerów podstawowych.

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");
}

Uwaga: wydaje się to dość wydajne. Zrobiłem test, w którym poprosiłem o obraz za pośrednictwem kontrolera ( http://localhost/MyController/Image/MyImage) i bezpośredniego adresu URL ( http://localhost/Images/MyImage.jpg), a wyniki były następujące:

  • MVC: 7,6 milisekund na zdjęcie
  • Bezpośrednio: 6,7 milisekund na zdjęcie

Uwaga: jest to średni czas żądania. Średnia została obliczona na podstawie tysięcy żądań na komputerze lokalnym, więc sumy nie powinny obejmować opóźnień sieci ani problemów z przepustowością.

Brian
źródło
10
Dla tych, którzy zastanawiają się teraz nad tym pytaniem, było to dla mnie najlepsze rozwiązanie.
Clarence Klopfstein,
177
To nie jest bezpieczny kod. Pozwolenie użytkownikowi na przekazanie takiej nazwy pliku (ścieżki) oznacza, że ​​może on potencjalnie uzyskać dostęp do plików z dowolnego miejsca na serwerze. Może chcę ostrzec ludzi, aby nie używali go takim, jakim jest.
Ian Mercer
7
Chyba że konstruujesz pliki w locie, tak jak są potrzebne, i buforujesz je po ich utworzeniu (to właśnie robimy).
Brian
15
@ mare - możesz to również zrobić, jeśli podajesz pliki z ograniczonej lokalizacji, np. możesz mieć obrazy, App_Dataktóre powinny być podpisane przez niektórych użytkowników aplikacji, ale nie przez innych. Użycie akcji kontrolera do ich obsługi pozwala ograniczyć dostęp.
Russ Cam
8
Jak inni wspominali, bądź ostrożny w budowaniu ścieżki, ponieważ widziałem rzeczywisty kod produkcyjny, który pozwalał użytkownikowi poruszać się po katalogu ze starannie skonstruowanym ciągiem POST lub zapytaniem: /../../../danger/someFileTheyTHoughtWasInaccessible
AaronLS
128

Używając wydanej wersji MVC, oto co robię:

[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");
}

Oczywiście mam tutaj pewne specyficzne dla aplikacji rzeczy dotyczące budowy ścieżki, ale zwracanie FileStreamResult jest przyjemne i proste.

Przeprowadziłem pewne testy wydajności w odniesieniu do tej akcji przeciwko codziennemu wywołaniu obrazu (z pominięciem kontrolera), a różnica między średnimi wynosiła tylko około 3 milisekund (średnia kontrolera wynosiła 68 ms, a kontroler inna niż 65 ms).

Wypróbowałem kilka innych metod wymienionych w odpowiedziach tutaj, a wydajność była znacznie bardziej dramatyczna ... kilka odpowiedzi na rozwiązania było aż 6 razy większa niż kontroler inny (inne kontrolery średnio 340 ms, a nie kontroler 65 ms).

Żeglarskie judo
źródło
12
Co z obrazem nie jest modyfikowany? FileStreamResult powinien wysłać 304, gdy obraz nie jest modyfikowany od ostatniego żądania.
dariol
Możesz użyć Path.Combinezamiast concat dla bezpieczniejszego i bardziej czytelnego kodu.
Marcell Toth,
101

Aby nieco rozwinąć odpowiedź Dyland:

Trzy klasy implementują klasę FileResult :

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

Wszystkie są dość zrozumiałe:

  • W przypadku pobierania ścieżek plików tam, gdzie plik istnieje na dysku, użyj FilePathResult- jest to najłatwiejszy sposób i pozwala uniknąć konieczności korzystania ze strumieni.
  • W przypadku tablic byte [] (podobnie jak Response.BinaryWrite) użyj FileContentResult.
  • W przypadku tablic byte [], do których chcesz pobrać plik (content-disposition: załącznik), użyj FileStreamResultw podobny sposób jak poniżej, ale za MemoryStreampomocąGetBuffer() .
  • Do Streamsużytku FileStreamResult. To się nazywa FileStreamResult, ale zajmuje Streamtak, więc zgaduję, że działa z MemoryStream.

Poniżej znajduje się przykład zastosowania techniki rozmieszczania treści (nie testowane):

    [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.
źródło
2
Treść tego postu była bardzo pomocna
Diego
VS mówi mi, że to przeciążenie FileStream () jest przestarzałe.
MrBoJangles
1
Uwaga: jeśli w nazwie pliku znajduje się przecinek, Chrome odrzuci go z błędem „otrzymano zbyt wiele nagłówków”. Dlatego zamień wszystkie przecinki na „-” lub „”.
Chris S,
Jak to zrobić, używając tylko kontrolerów interfejsu API?
Zapnologica
74

Może to być pomocne, jeśli chcesz zmodyfikować obraz przed jego zwróceniem:

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
źródło
1
Dziękuję Ci. Jest to idealne rozwiązanie w przypadku, gdy do pobrania obrazu wymagającego uwierzytelnienia potrzebny jest serwer proxy, którego nie można wykonać po stronie klienta.
Hong
1
Zapomniałeś pozbyć się aż 3 natywnych obiektów: Font, SolidBrush i Image.
Wout
3
Sugerowane ulepszenie tutaj: tworzysz strumień pamięci, zapisujesz dane, a następnie tworzysz wynik pliku z danymi za pomocą .ToArray () Możesz również wywołać ms.Seek (0, SeekOrigin.Begin), a następnie zwrócić plik (ms, „ image / png ") // zwróć sam strumień
Quango,
12

Możesz utworzyć własne rozszerzenie i zrobić to w ten sposób.

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
źródło
10

Możesz napisać bezpośrednio do odpowiedzi, ale wtedy nie można jej przetestować. Preferowane jest zwrócenie ActionResult, który odroczył wykonanie. Oto mój StreamResult wielokrotnego użytku:

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
źródło
9

Dlaczego nie pójść prosto i użyć ~operatora tyldy ?

public FileResult TopBanner() {
  return File("~/Content/images/topbanner.png", "image/png");
}
JustinStolle
źródło
6

AKTUALIZACJA: Są lepsze opcje niż moja pierwotna odpowiedź. Działa to całkiem dobrze poza MVC, ale lepiej trzymać się wbudowanych metod zwracania treści obrazu. Zobacz głosowane odpowiedzi.

Z pewnością możesz. Wypróbuj następujące kroki:

  1. Załaduj obraz z dysku do tablicy bajtów
  2. buforuj obraz w przypadku, gdy oczekujesz więcej żądań obrazu i nie chcesz dysku I / O (moja próbka nie buforuje go poniżej)
  3. Zmień typ MIME za pomocą Response.ContentType
  4. Response.BinaryZapisz tablicę bajtów obrazu

Oto przykładowy kod:

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

Mam nadzieję, że to pomaga!

Ian Suttle
źródło
4
i jak wyglądałoby to w działaniu kontrolera?
CRice
5

Rozwiązanie 1: Aby renderować obraz w widoku z adresu URL obrazu

Możesz stworzyć własną metodę rozszerzenia:

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

Następnie użyj go w następujący sposób:

@Html.Image(@Model.ImagePath);

Rozwiązanie 2: Aby renderować obraz z bazy danych

Utwórz metodę kontrolera, która zwraca dane obrazu, jak poniżej

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);
  }
}

I użyj go w widoku takim jak:

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

Aby użyć obrazu renderowanego z tej akcji w dowolnym HTML, użyj

<img src="http://something.com/image/view?id={imageid}>
Ajay Kelkar
źródło
5

To zadziałało dla mnie. Ponieważ przechowuję obrazy w bazie danych 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;
    }

W powyższym fragmencie _db.ImageFiles.Find(uuid)jest wyszukiwanie rekordu pliku obrazu w db (kontekst EF). Zwraca obiekt FileImage, który jest po prostu niestandardową klasą, którą stworzyłem dla modelu, a następnie używa go jako FileContentResult.

public class FileImage {
   public string Uuid { get; set; }
   public byte[] Data { get; set; }
   public string ContentType { get; set; }
}
hmojica
źródło
4

możesz użyć Plik do zwrócenia pliku, takiego jak Widok, Treść itp

 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
źródło
3

Spójrz na ContentResult. Zwraca ciąg, ale można go użyć do stworzenia własnej klasy podobnej do BinaryResult.

leppie
źródło
2
if (!System.IO.File.Exists(filePath))
    return SomeHelper.EmptyImageResult(); // preventing JSON GET/POST exception
else
    return new FilePathResult(filePath, contentType);

SomeHelper.EmptyImageResult()powinien powrócić FileResultz istniejącym obrazem (na przykład przezroczysty 1x1).

Jest to najprostszy sposób, jeśli pliki są przechowywane na dysku lokalnym. Jeśli pliki są byte[]lub stream- to użyj FileContentResultlub, FileStreamResultjak sugerował Dylan.

Victor Gelmutdinov
źródło
1

Widzę dwie opcje:

1) Zaimplementuj własny IViewEngine i ustaw właściwość ViewEngine kontrolera, którego używasz, na ImageViewEngine w żądanej metodzie „image”.

2) Użyj widoku :-). Wystarczy zmienić typ zawartości itp.

Matt Mitchell
źródło
1
Może to być problem z powodu dodatkowych spacji lub list CRLF w widoku.
Elan Hasson
2
Myliłem się w moim ostatnim poście ... msdn.microsoft.com/en-us/library/ ... Możesz użyć klasy WebImage i WebImage.Write w widoku :)
Elan Hasson
1

Możesz użyć HttpContext.Response i bezpośrednio napisać do niego treść (WriteFile () może działać dla Ciebie), a następnie zwrócić ContentResult z akcji zamiast ActionResult.

Oświadczenie: Nie próbowałem tego, opiera się na sprawdzeniu dostępnych interfejsów API. :-)

Franci Penov
źródło
1
Tak, właśnie zauważyłem, że ContentResult obsługuje tylko łańcuchy, ale łatwo jest stworzyć własną klasę opartą na ActionResult.
leppie
1

Poniższy kod wykorzystuje System.Drawing.Bitmapdo załadowania obrazu.

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
źródło
0

Spotykałem również podobny wymóg,

Tak więc w moim przypadku przesyłam zapytanie do kontrolera ze ścieżką do folderu obrazów, która w zamian odsyła obiekt ImageResult.

Poniższy fragment kodu ilustruje pracę:

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>";

I w sterowniku ze ścieżki obrazu tworzę obraz i zwracam go z powrotem dzwoniącemu

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
źródło
0

Przeczytaj obraz, przekonwertuj go byte[], a następnie zwróć File()z typem zawartości.

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

Oto zastosowania:

using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using Microsoft.AspNetCore.Mvc;
facepalm42
źródło