Blazor에서 이미지 파일 다루기

Blazor에서 이미지파일 다루기.md

Updated at 2021.6.10

Blazor and Image File

Web App에서 사용자 컴퓨터에 있는 이미지 파일을 선택하여 웹에 보여주고, 필요시 서버에 업로드할 필요가 있다. 여기서는 Blazor에서 파일을 선택하고 Data Uri를 이용하여 이미지를 보여주는 기능을 구현해 볼 것이다.

다음 3가지에 대한 지식이 필요하다.

파일 선택 및 로딩 하기

InputFile Component

파일 선택 창을 보여주고 로컬 위치에서 파일을 선택할 때 그 파일에 대한 정보로 반환하는 컴포넌트이다. 기본적으로 HTML에서의 <input> 요소를 렌더링한 것이다.

<input type="file" multiple>

일반적인 사용법은 다음과 같다.

<InputFile OnChange="@LoadFiles" multiple />

@code {
    private void LoadFiles(InputFileChangeEventArgs e)
    {
        ...
    }
}

OnChange 이벤트에서 사용되는 메서드는 InputFileChangeEventArgs 파라미터를 포함하고 있는데, 이 클래스는 다음과 같은 프로퍼티와 메서드를 가지고 있다.

  • Properties
    • File: IBrowserFile 인테페이스 타입이다.
  • Methods
    • IReadOnlyList GetMultipleFiles(int maximumFileCount): multiple 옵션 선택시 IBrowserFile 리스트를 반환하는 함수이다.

위 둘을 활용하여 파일 브라우저에서 선택된 파일의 정보를 가지고 있는 IBrowserFile를 얻을 수 있다.

IBrowserFile Interface

InputFile 컴포넌트에서 선택된 파일에 대한 정보를 가지고 있는 인터페이스로서 다음과 같은 속성과 메소드를 통해 파일 정보를 얻거나 스트림으로 읽어 오고, 이미지 파일의 형식 및 사이즈를 변경할 수 있다.

  • Properties
    • ContentType: MIME 타입
    • LastModified
    • Name
    • Size: 몇 바이트인가?
  • Methods
    • Stream OpenReadStream(long maxAllowedSize, CancellationToken cancellationToken)
  • Extension Methods
    • ValueTask RequestImageFileAsync(this IBrowserFile browserFile, string format, int maxWidth, int maxHeight)

위의 두 메서드에 대한 사용 예는 다음과 같다.

private void LoadFiles(InputFileChangeEventArgs e)
{
  ...
  foreach (var file in e.GetMultipleFiles(MaxAllowedFiles))
  {
    string format = "image/jpeg";
    IBrowserFile imageFile = await file.RequestImageFileAsync(format, 980, 980);
    Stream fileStream = imageFile.OpenReadStream(maxFileSize);
  }
  ...
}

이미지 데이터를 Uri로

우선 Uri과 Url의 차이에 대해 알아보자.

  • Uri: Uniform Resource Identifier (식별자)
  • Url: Uniform Resource Locator (위치), 인터넷에서 웹페이지, 이미지, 비디오 등 리스스의 위치를 나타내는 문자열이다.
graph LR subgraph Uri Url end

Uri

Uri의 일반적인 구조는 다음과 같다.

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
  • scheme: 프로토콜, 웹에서는 일반적으로 http 또는 https 이다.
  • user, passwd: 인증된 사용자만 접속할 경우의 이름과 비밀번호이다.
  • host, port: 접속할 서버의 호스트 이름과 포트 번호이다.
  • path: 접속한 서버 내에서의 상세 경로이다.
  • query: 요청할 정보에 대한 추가적인 파라미터로 사용된다
  • fragment: 요청한 정보 내에서의 추가적인 위치를 나타낸다.

Data Uri

data: 스킴을 활용하여 컨텐츠를 문서 내에 인라인으로 포함시킬 수 있는 방법이다. 기본 구문은 아래와 같다.

data:[<mediatype>][;base64],<data>
  • mediatype은 MIME 타입으로 JPEG 이미지는 image/jpeg이다. 생략도 가능한데, 생략하면 기본값은 text/plain;charset=US-ASCII이다.
  • 텍스트가 아니면 base64로 인코딩된 이진데이터를 덧붙이면 된다.
  • 몇가지 예제
    • data:,Hello%2C%20World! (단순 텍스트 포함시, 컴마를 빼먹으면 안됨)
    • data:text/plain;base64,xxx (MIME 타입 명시하고, 데이터 포멧이 이진화 된 것일 때)

적용하기

우리가 구현하고자 하는 것은 이미지 파일을 이진화 시켜서 웹앱에 인라인으로 포함시키고자 하는 것이어서 다음과 같이 MemoryStream으로 읽어 들인 데이터를 Uri로 만든다.

...
string format = "image/jpeg";
MemoryStream memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream);
string url = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";

Full Source Code

실제 실행가능한 소스 코드 및 실행 화면은 다음과 같다.

ImagePreview.razor

<p>
    <InputFile OnChange="LoadImages" id="input-image" accept="image/png,image/gif,image/jpeg" multiple />
    <span id="exception-message">@exceptionMessage</span>
</p>

@if (isLoading)
{
    <p>Loading...</p>
}

@foreach (var (file, content) in loadedFiles)
{
    <p id="file-@(file.Name)">
        <strong>Name:</strong> <span id="file-name">@(file.Name)</span><br />
        <strong>Last modified:</strong> <span id="file-last-modified">@(file.LastModified.ToString())</span><br />
        <strong>Size (bytes):</strong> <span id="file-size">@(file.Size)</span><br />
        <strong>Content type:</strong> <span id="file-content-type">@(file.ContentType)</span><br />
        <img id="image-uploaded" src="@content" /><br />
    </p>
}

@using System.IO;
@code {
    [Parameter]
    public int MaxAllowedFiles { get; set; } = 5;

    Dictionary<IBrowserFile, string> loadedFiles = new Dictionary<IBrowserFile, string>();

    long maxFileSize = 1024 * 1024 * 15;

    bool isLoading;

    string exceptionMessage;

    async Task LoadImages(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();
        exceptionMessage = string.Empty;

        try
        {
            foreach (var file in e.GetMultipleFiles(MaxAllowedFiles))
            {
                StateHasChanged();
                loadedFiles.Add(file, await LoadImage(file));
            }
        }
        catch (Exception ex)
        {
            exceptionMessage = ex.Message;
        }

        isLoading = false;
    }

    async Task<string> LoadImage(IBrowserFile file)
    {
        var format = "image/jpeg";
        var imageFile = await file.RequestImageFileAsync(format, 980, 980);

        using var fileStream = imageFile.OpenReadStream(maxFileSize);
        using var memoryStream = new MemoryStream();
        await fileStream.CopyToAsync(memoryStream);

        return $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";
    }
}

댓글