1

I am making an API call that should return a list of PNGs . I'm storing the PNG in blob storage and I'm able to successfully grab them. In the past when I've only had to return one image, I'd convert the blob to a memory stream and just handle the memory stream on the client side. That does not seem to work for multiple files though.

[HttpGet("getThumbnails/{seriesId}")]
public async Task<ActionResult<List<MemoryStream>>> GetThumbnails(string seriesId)
{          
    var pngs = await _service.LoadPNGsMs(seriesId);

    Response.ContentType = "image/png ";
    return pngs;
}

public async Task<List<MemoryStream>> LoadPNGsMs(string seriesID)
{
    var returnList = new List<MemoryStream>();
    var blobls = await _azureBlobStorageService.GetBlockBlob(seriesID);

    foreach (var blob in blobls)
    {
            var stream = new MemoryStream();
            await blob.DownloadToStreamAsync(stream);
            stream.Position = 0;
            returnList.Add(stream);
    }
    return returnList;
}

public async Task<List<CloudBlockBlob>> GetBlockBlob(string seriesID)
{
    var blobFiles = containerClient.GetBlobs(prefix: seriesID);
    var blobFilePaths = blobFiles.Select(x => x.Name).ToList();
    List<CloudBlockBlob> cloudBlockBlobs = new List<CloudBlockBlob>();

    foreach (var blob in blobFilePaths)
    {
        CloudBlockBlob blockBlob = blobContainer.GetBlockBlobReference(blob);
        bool isExists = await blockBlob.ExistsAsync();
        if (isExists)
            cloudBlockBlobs.Add(blockBlob);
    }
    return cloudBlockBlobs;
}

I am getting a 406 for running this code. Am I doing too much setting the response type to be image/png? Should I just work with the blobs and forget about the memory streams? I'll keep playing around with this here and post results if I find anything.

  • 1
    Who's making the call? The `ContentType` specifies only a single image is use. Browsers won't understand responses that contain multiple "images". You can return a multipart response but [it's unlikely browsers will support it](https://stackoverflow.com/questions/1806228/browser-support-of-multipart-responses). If the caller is an API client it may be able to handle the response. You could also zip the images into a single file (even without compression) and return them – Panagiotis Kanavos Feb 21 '23 at 15:20
  • 1
    On the other hand, you're returning images one by one when the client could request many of them concurrently, especially if HTTP/2 is enabled – Panagiotis Kanavos Feb 21 '23 at 15:21
  • @PanagiotisKanavos The client side (browser) is making the call. I guess you bringing up the fact that browser can not support multiple images makes this a tougher problem than I originally anticipated. I was going to go the route of returning returning images one by one but this would require a extra data on objects I don't really have control over. Your comments were helpful, thank you. – Brendan Murphy Feb 21 '23 at 16:09
  • I think I can leverage what @PanagiotisKanavos recommended by making one call ahead of time to get the blob references (the stuff before the for in GetBlockBlob) and then make concurrent calls using the blob references. – Brendan Murphy Feb 21 '23 at 16:17

2 Answers2

0

Well, you could say use a repeater, and simple then push out the image to the repeater for the number of pictures.

So if I have a table of pictures, then I want the picture name, and then the byte-stream of each picture.

So, this markup:

<asp:Repeater ID="Repeater1" runat="server" OnItemDataBound="Repeater1_ItemDataBound">
    <ItemTemplate>
        <div class="mybox">
            <div style="text-align: center; padding: 2px 10px 12px 10px">
                <asp:Button ID="cmdMyView" runat="server" Text="View"
                    CssClass="btn-info" Style="float: right"
                    OnClick="cmdMyView_Click"
                    CommandArgument='<%# Eval("ID") %>' />
                <br />
                <h3><%# Eval("Fighter") %></h3>
                <asp:Image ID="Image2" runat="server" Width="180" Height="120" />
                <h4>Engine</h4>
                <asp:Label ID="EngineLabel2" runat="server" Text='<%# Eval("Engine") %>' />
                <h4>Description</h4>
                <asp:Label ID="DescLabel" runat="server" Text='<%# Eval("Description") %>' />
            </div>
        </div>
    </ItemTemplate>
</asp:Repeater>

(nothing special).

but, in above is a plain jane image control.

So, code to load is this:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
        LoadGrid();
}

void LoadGrid()
{
    using (SqlConnection conn = new SqlConnection(Properties.Settings.Default.TEST4))
    {
        string strSQL = "SELECT * FROM Fighters";
        using (SqlCommand cmdSQL = new SqlCommand(strSQL, conn))
        {
            conn.Open();
            DataTable rstData = new DataTable();
            rstData.Load(cmdSQL.ExecuteReader());
            Repeater1.DataSource = rstData;
            Repeater1.DataBind();
        }
    }
}

And row item data bound is where we push out the byte array to each image.

protected void Repeater1_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if ( (e.Item.ItemType == ListItemType.Item) ||  
        (e.Item.ItemType == ListItemType.AlternatingItem) )
    {
        Image ctlImage = (Image)e.Item.FindControl("Image2");
        DataRowView rData = (DataRowView)e.Item.DataItem;
        byte[] MyBytes = (byte[])rData["MyImage"];

        ctlImage.ImageUrl =
            $@"data:image/jpg;base64,{Convert.ToBase64String(MyBytes)}";
    }
}

And the result is this:

enter image description here

So, if you have some type of repeater, listview, grid view, datalist view or repeater?

Then just push out the byte array to the image control as a base64 string.

Above is not only simple, but requires no special image handler's and all that jazz.

You do however, need the mine type.

Albert D. Kallal
  • 42,205
  • 3
  • 34
  • 51
-1

Instead of using List<MemoryStream> use a IEnumerable<byte[]> and write each blob to byte arrays. Your client won't be able to use the memory stream for multiple blobs in the same way you probably wrote it out to the response for a single item. I would also change it to

public async Task<ActionResult<IEnumerable<byte[]>>> GetThumbnails(string seriesId)

so you can yield return byte[] and improve the throughput to the client with less memory consumption on the server overall.

Bron Davies
  • 5,930
  • 3
  • 30
  • 41
  • 1
    You can only use `yield return` in an iterator. As for the types, it doesn't matter what type is used, it's not possible to return multiple images in a single response, either as byte arrays or memory streams. – Panagiotis Kanavos Feb 21 '23 at 15:14
  • 1
    @BrendanMurphy this won't even compile. You can't return multiple images in a single response. That's not how HTTP works. Your own code specifies there's only one image when it sets the content type to `image/png` – Panagiotis Kanavos Feb 21 '23 at 15:15
  • @PanagiotisKanavos you can use the `yield return` syntax if a method uses an `IEnumerable` return type. If the OP is implementing this as an API then that's different from just a simple image HTTP response and theoretically could return multiple byte arrays (although the merits of this approach could be flawed) – Bron Davies Feb 22 '23 at 00:16
  • Your code doesn't return an `IEnumerable` though, it returns a `Task`, so `yield return` will throw a compilation error. And no, theoretically it can't just return multiple byte arrays. It can only return what the `Content-Type` header says is contained in the response. Clients use that header to determine how to treat the response's byte stream. There are multipart content types but browsers can't handle them. At the very least the client needs to have a way to determine how many blobs there are and how large each one is – Panagiotis Kanavos Feb 22 '23 at 07:52