Add BackgroundImage with EPPlus only allows path but cannot get path in Blazor WASM

Viewed 22

This may not be 100% an EPPlus issue, but since it is Blazor WASM it appears I cannot get the file path to a static image in the wwwroot/images folder. I can get the url and paste it into a browser and that works, even adding that same path to the src attribute of an img works, neither of those helps me.

FYI "background" in this context means a watermark.

It appears that the EPPlus dev team only wants a drive path the file (ex. C:\SomeFolder\SomeFile.png), and I am not seeing how to get that within Blazor WASM. I can get the bytes of the file in c# and even a stream, but no direct path.

My code is the following:

using (var package = new ExcelPackage(fileName))
{
  var sheet = package.Workbook.Worksheets.Add(exportModel.OSCode);

  sheet.BackgroundImage.SetFromFile("https://localhost:44303/images/Draft.png");
  ...
}

This returns an exception:

Unhandled exception rendering component: Can't find file /https:/localhost:44303/images/Draft.png

Noticing that leading / I even tried:

sheet.BackgroundImage.SetFromFile("images/Draft.png");

Which returned the same error:

Unhandled exception rendering component: Can't find file /images/Draft.png

So, I am perhaps needing 1 of 2 possible answers:

  1. A way to get a local drive path to the file so the .SetFromFile method is not going to error.
  2. To have a way to set that BackgroundImage property with a byte array or stream of the image. There is this property BackgroundImage.Image but it is readonly.
1 Answers

Thanks to a slap in the face from @Panagiotis-Kanavos I wound up taking the processing out of the client and moving it to the server. With that, I was able to use Static Files to add the watermark with relatively little pain.

In case anyone may need the full solution (which I always find helpful) here it is:

Here is the code within the button click on the Blazor component or page:

private async Task GenerateFile(bool isFinal)
{
  ...
  var fileStream = await excelExportService.ProgramMap(exportModel);
  var fileName = "SomeFileName.xlsx";

  using var streamRef = new DotNetStreamReference(stream: fileStream);

  await jsRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
}

That calls a client-side service that really just passes control over to the server:

public class ExcelExportService : IExcelExportService
{
  private const string baseUri = "api/excel-export";
  private readonly IHttpService httpService;

  public ExcelExportService(IHttpService httpService)
  {
    this.httpService = httpService;
  }

  public async Task<Stream> ProgramMap(ProgramMapExportModel exportModel)
  {
    return await httpService.PostAsJsonForStreamAsync<ProgramMapExportModel>($"{baseUri}/program-map", exportModel);
  }
}

Here is the server-side controller that catches the call from the client:

[Route("api/excel-export")]
[ApiController]
public class ExcelExportController : ControllerBase
{
  private readonly ExcelExportService excelExportService;

  public ExcelExportController(ExcelExportService excelExportService)
  {
    this.excelExportService = excelExportService;
  }

  [HttpPost]
  [Route("program-map")]
  public async Task<Stream> ProgramMap([FromBody] ProgramMapExportModel exportModel)
  {
    return await excelExportService.ProgramMap(exportModel);
  }
}

And that in-turn calls the server-side service where the magic happens:

public async Task<Stream> ProgramMap(ProgramMapExportModel exportModel)
{
  var result = new MemoryStream();

  ExcelPackage.LicenseContext = LicenseContext.Commercial;
  var fileName = @$"Gets Overwritten";

  using (var package = new ExcelPackage(fileName))
  {
    var sheet = package.Workbook.Worksheets.Add(exportModel.OSCode);

    if (!exportModel.IsFinal)
    {
      var pathToDraftImage = @$"{Directory.GetCurrentDirectory()}\StaticFiles\Images\Draft.png";

      sheet.BackgroundImage.SetFromFile(pathToDraftImage);
    }

    ...
    sheet.Cells.AutoFitColumns();

    package.SaveAs(result);
  }

  result.Position = 0; // Without this, data does not get written
  return result;
}

For some reason, this next method was not needed when doing this on the client-side but now that it is back here, I had to add a method that returned a stream specifically and used the ReadAsStreamAsync instead of ReadAsJsonAsync:

public async Task<Stream> PostAsJsonForStreamAsync<TValue>(string requestUri, TValue value, CancellationToken cancellationToken = default)
{
  Stream result = default;
  var responseMessage = await httpClient.PostAsJsonAsync(requestUri, value, cancellationToken);

  try
  {
    result = await responseMessage.Content.ReadAsStreamAsync(cancellationToken: cancellationToken);
  }
  catch (HttpRequestException e)
  {
    ...
  }

  return result;
}

Lastly, in order for it to give the end-user a download link, this was used (taken from the Microsoft Docs):

window.downloadFileFromStream = async (fileName, contentStreamReference) => {
  const arrayBuffer = await contentStreamReference.arrayBuffer();
  const blob = new Blob([arrayBuffer]);
  const url = URL.createObjectURL(blob);
  const anchorElement = document.createElement("a");

  anchorElement.href = url;
  anchorElement.download = fileName ?? "";
  anchorElement.click();
  anchorElement.remove();
  URL.revokeObjectURL(url);
}
Related