🖼️

画像アップロード機能を調査して気になった基礎知識

に公開

こんにちは、PortalKeyの しゃり です。

画像アップロードの実装方法を調べる中で気になったことをまとめました。
体系的な解説ではなく、画像アップロード機能を初めて実装する人が全体像をふわっと把握したり、それらに関する記事を読みやすくするための記事です。

通信プロトコル

API

APIは文脈によって意味が変わる抽象的な概念です。
この記事では、Web APIの文脈で使われる意味で説明します。

以下のAWSの説明によると、/avatar+ POST/avatar+ GETは同じエンドポイントであり別APIという表現ができそうです。
AWS の API を理解しよう ! 初級編 ~ API の仕組みと利用方法を理解しよう

API は、2 つのソフトウェアコンポーネントが一連の定義とプロトコルを使用して相互に通信できるようにするメカニズムです。

私はこの言葉を少し言い換えて、API は「サービスを提供している相手に、決められた形式で依頼を送ると、決められた形式の結果が返ってくる仕組み」と表現することがあります。

APIとエンドポイントとは何ですか?

APIは、さまざまなアプリケーション間の関係を規定する契約として定義できます。エンドポイント、データ形式、リクエストの作成方法を記述します。エンドポイントは、特定のアプリケーションを配置できるAPI内の特定のポイントまたはアドレスです。

HTTPリクエスト メソッド

GET

サーバーのリソースを取得する際に使うリクエストです。

このリクエストに情報を載せる際にはURLに載せるクエリパラメーターか、認証情報等の秘匿情報にはAuthorizationヘッダーやCookieを使うことが普通だと思います。

なぜなら、GETのbodyにデータを入れることは「すべきでない(shouldn't)」からです。
https://developer.mozilla.org/en/docs/Web/HTTP/Reference/Methods/GET

The GET HTTP method requests a representation of the specified resource. Requests using GET should only be used to request data and shouldn't contain a body.

Slackのドキュメントではアバター削除apiはGETのbodyにauthorize tokenを乗せていたので、あれ🤔?と思って調べてみました。
やはり、それは非推奨のようですね。Slackはどういう意図で実装したんでしょうか。

PATCH

https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Methods/PATCH

リソースをどのように修正するかの指示セットです。
特徴は以下の通り。

  • CRUD に見られる "update" の概念にやや類似している
  • 冪等になることもあるが、保証されない。
  • PUTとの違い
    PUTは完全なリソースで全てを上書きする。故に冪等でなければいけない。
  • POSTとの違い
    POSTはコレクションに対して、単一のリソースを作成するのに使う

HTTP Content-type ヘッダー

https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Headers/Content-Type

HTTP の Content-Type は表現ヘッダーで、コンテンツへのエンコードが適用される前の、リソースの元のメディア種別を示すために使用します。

レスポンスにおいては、 Content-Type ヘッダーはクライアントに返されたコンテンツの実際の種類を伝えます。

Content-Typeヘッダーの値はMIME Type(メディアタイプ)形式で指定します。
画像送信ではapplication/jsonimage/[拡張子]といったMIME Typeが使われているようです。
それらを複数取り扱うためのmultipart/form-dataもあります。

application/json

https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Headers/Content-Type#rest_api_で_json_を使用した場合の_content-type

多くの REST API は、コンテンツタイプとして application/json を使用しており、これはマシン間の通信やプログラムによる操作に便利です。

JSONは人にも機械にも分かり易いデータ交換フォーマットで、1つのJSONに複数のkey: valueを持たせられるのでデータ転送に便利です。
しかし、JSONにバイナリをそのまま持たせることはできません。
https://www.rfc-editor.org/rfc/rfc8259#section-3

  1. Values
    A JSON value MUST be an object, array, number, or string, or one of
    the following three literal names:
    false
    null
    true

image

application/jsonでバイナリを扱えないのであれば、バイナリを送るためのMIME Typeがあります。
画像の場合はimage/jpeg, image/png, image/svg+xmlなどを使います
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/MIME_types#image

ただし、これではバイナリ1つしか送れないので、複数の画像や、画像の名前等のメタデータを持たせることができません。

multipart/form-data

https://tex2e.github.io/rfc-translater/html/rfc7578.html#4-1--Boundary-Parameter-of-multipartform-data

jsonでkey:valueも送りたいし、同時にバイナリも送りたいなんて時に使えるMIME Typeです。
両方のデータを許容する、というよりは、MIME Typeとデータのセットを複数持たせることができるという感じです。

HTMLのform要素で送信する場合、各input要素の値がboundary(境界線)で区切られてまとめられます。
RFC 7578によって、各パートにはinput要素のname属性の値を含めることが定められています。
https://tex2e.github.io/rfc-translater/html/rfc7578.html#4-1--Boundary-Parameter-of-multipartform-data

Content-Dispositionヘッダーフィールドには、「name」の追加パラメーターも含める必要があります。 「name」パラメータの値は、フォームの元のフィールド名です

そして、1つ1つのデータをboundaryで区切っています。
下の例ではWebKitFormBoundaryO5quBRiT4G7Vm3R7がboundaryです。

-----WebKitFormBoundaryO5quBRiT4G7Vm3R7
Content-Disposition: form-data; name="payload_json"
Content-Type: application/json

{ "size": 21349, "width": 399, "height": 399, "description": "" }

-----WebKitFormBoundaryO5quBRiT4G7Vm3R7
Content-Disposition: form-data; name="files[0]"; filename="myfilename.png"
Content-Type: image/png

[image bytes]
-----WebKitFormBoundaryO5quBRiT4G7Vm3R7
Content-Disposition: form-data; name="files[1]"; filename="mygif.gif"
Content-Type: image/gif

[image bytes]

-----WebKitFormBoundaryO5quBRiT4G7Vm3R7--

画像データ

画像をはじめとするファイルはバイナリであり、そのままでは使いずらい場面があります。
それを文字列で扱うことで、HTMLのimg要素のsrc属性に渡してブラウザで表示したり、JSOEに持たせることができるようになります。

先にざっくりまとめるとこんな感じです。

名前 スキーム 特徴
Data URL data: データ自体をURL内に埋め込む。Base64やパーセントエンコードで表現
Blob URL blob: メモリ上のデータへの参照。データ本体はURLに含まれない

では、個別に解説していきます。

data: URL

https://ja.wikipedia.org/wiki/Data_URI_scheme

データURIスキーム(英語: data URI scheme)とは、あたかも外部リソースを読み込むのと同じように、ウェブページにインラインにデータを埋めこむ手段を提供するURIスキームである。

https://developer.mozilla.org/ja/docs/Web/URI/Reference/Schemes/data

  • data URL は data: スキームが先頭についた URL
    data:[<media-type>][;base64],<data>
    data:image/png;base64,iVBORw0...(略)...=
    最後の=はパディングであり、文字数の不足分だけ穴埋めに使われるため無いこともある。

長さの制限

ブラウザーは特定の最大長のデータに対応する必要はありません。 Chromium と Firefox では data URL は 512MB に制限されており、 Safari (WebKit) は 2048MB に制限されています。 なお、 Firefox 97 では制限が 256KB から 32MB へ拡大され、 Firefox 136 ではさらに 512MB に拡大されました。

blob: URL

blob: URLはリソースの場所です。
(正確には、ブラウザのメモリ内にあるデータへの参照)
そのため、blobの文字列をそのまま転送してもデータ転送にはなりません。

https://developer.mozilla.org/ja/docs/Web/URI/Reference/Schemes/blob

blob URL は data URL とよく似ています。どちらも、メモリー内のリソースを URL として表すことができます。違いは、データ URL はリソースを自身に埋め込み、サイズに厳しい制限があるのに対し、 blob URL はバックエンドの Blob または MediaSource を必要とし、より大きなリソースを表すことができる点です。

blob:<origin>/<uuid>

base64

https://developer.mozilla.org/ja/docs/Glossary/Base64
base64はバイナリをASCII文字列形式へ変換したものです。

base64で扱うことのメリデメはこの記事が詳細に取り上げています。
当記事では省略しますので、気になる方は読んでみてください。
https://watashi.xyz/base64/

大きな特徴は2つです

  • stringとして扱えること
    JSONの値にできたり、Data URI形式でimg要素に渡せる
  • データサイズがバイナリに対して約33%増加してしまう
    24bitのバイナリから32bitのA-Z, a-z, 0-9, +, /(バイナリのインデックス)になるため

画像の表示: img要素

https://developer.mozilla.org/ja/docs/Web/HTML/Reference/Elements/img

言わずと知れた、画像を表示するための要素。src属性に渡すと表示してくれる。
実はsrcに渡す画像はWebPP, AVIFが推奨されている。

WebP や AVIF などは、PNG、JPEG、GIF よりはるかに性能が良く、静止画と動画の両方で使えるのでおすすめします。

実際、Discordの画像はアプリ内ではwebPになっている。リンクをブラウザで個別ページとして開くと元のファイル形式になる。
(WebPは圧縮能力が高く、その割に画質劣化が少ないらしいです。)

画像読み込みエラー

discordではエラー用の画像が用意してますね。この機能でしょうか。

画像の読み込みまたは描画の間にエラーが発生した場合で、かつ onerror イベントハンドラーが error イベントを扱うよう設定されていた場合は、イベントハンドラーが呼び出されます。

loading="lazy"

Data: URIで画像を表示できるようにしたとしても、その画像の読み込みが終わるまでページを表示できないでは動作がもっさりしてしまいます。
対策として、画面外なら読み込みを遅延させるという設定ができます。

https://developer.mozilla.org/ja/docs/Web/HTML/Reference/Elements/img#loading

画像がブラウザーで定義されたビューポートからの距離に達するまで、画像の読み込みを遅延させます。これは、画像が必要とされるのが合理的に確実になるまで、処理に必要なネットワークやストレージの帯域幅を使用しないようにするためです。これは一般的に、ほとんどの典型的な使用法において、コンテンツの性能を向上させることができます。

ただし、読み込みまでwidth, heightが分からない/指定できないものはlazy非推奨なようです。

メモ: 画像の loading が lazy に設定されていると、たとえ読み込みによって表示が変更されるとしても、可視要素と交差しない場合は決して読み込まれません。未読み込みの画像は width および height が 0 であるためです。遅延読み込みの画像に width および height を設定すると、この問題が解決され、仕様で推奨されているベストプラクティスとなります。また、レイアウトのずれを防ぐことにも役立ちます。



インフラ関連

CDN

https://ja.wikipedia.org/wiki/コンテンツデリバリネットワーク
コンテンツデリバリネットワーク = キャッシュサーバー
自サーバーへの負荷を分散させたり、各地域に配置することでレイテンシを下げることができる。

0秒キャッシュ はキャッシュされないのではないので注意。
https://blog.redbox.ne.jp/cdn-cache-control.html

キャッシュをしてはいけない会員情報のページなどは、確実にキャッシュを行わない命令ヘッダで一番強いものを必ず付与させておくのが安全でしょう。さらに万一の事態に備えCookie内の認証情報別にキャッシュするルールも追加しておくとベストです

画像のような静的で大きいデータでは特に使いたいもの。ただ、CDNのキャッシュが効いていると、自社サーバーではデータが切り替わっていても、ユーザーが受け取るデータが古いことになるので注意。

※個人的によく間違えるDNSはDomain Name Systemであり全然違う。

Protocol Bufferと大容量データ

弊社ではProtocol Buffers(通称proto)とRPCを使用しているので、ついでに補足情報を書き出してみました。

Protocol Buffer

protoでは1GBを超えるメッセージは非推奨なので、大きい画像を転送する際は代替戦略が必要のようです。

https://protobuf.dev/programming-guides/techniques/

Large Data Sets

Protocol Buffers are not designed to handle large messages. As a general rule of thumb, if you are dealing in messages larger than a megabyte each, it may be time to consider an alternate strategy.

大規模データセット

プロトコルバッファは、大規模なメッセージを扱うようには設計されていません。経験則として、1メッセージあたり1メガバイトを超えるメッセージを扱う場合は、代替戦略を検討すべき時期かもしれません。

gRPCのストリーミング

gRPCでは、大容量転送のための規格があったりする。
詳細は割愛するが、データを分割転送する技術。
https://developer.android.com/guide/topics/connectivity/grpc?hl=ja

gRPC は、HTTP/2 での全二重双方向ストリーミングのサポートを最初から考慮して設計されています。ストリーミングでは、大量の情報のアップロードやダウンロードが必要なオペレーションなど、リクエストとレスポンスのサイズを任意に指定できます。

おわりに

画像アップロード実装に関連する基礎知識をまとめました。
これらの用語を知っておくと、技術記事やドキュメントが読みやすくなると思います。

もしも間違った表現や、怪しい内容があればコメントでも修正依頼でも良いのでご指摘いただけますと幸いです。
まさかり大歓迎です。

余談1
multipart/form-data の boundaryって後半部分は固定値ではなさそうでした。
どういう基準や理由で生成してるのかが不思議に思いました。
また機会あれば調べてみたいと思います。

余談2
MDNによると、

MIME タイプ省略した場合は、既定で text/plain;charset=US-ASCII

とのことだったので、Chromeで動作確認してみました。

imageまで指定すればpngは読み込めるものの、svgはダメそう。ブラウザのデフォルト補助機能だったりするのでしょうか。

正しいpng:data:image/png;base64,iVBORw0...

  • 表示された
    • data:image;base64,iVBORw0...
    • data:;base64,iVBORw0...
  • 表示されなかった
    • base64,iVBORw0...
    • iVBORw0...

正しいsvg:data:image/svg+xml;base64,PHN2Zy...

  • 表示されなかった
    • data:image/;base64,PHN2Zy...
    • data:image;base64,PHN2Zy...

























PortalKey Tech Blog

Discussion