Reactで、画像をBase64のデータURLに変換する方法
画像ファイルを Base64 のデータURL(data:image/png;base64,… の形の文字列)にするには、FileReader の readAsDataURL() を使います。ファイルの中身をそのまま Base64 にするので、データURLから戻したバイト列は元のファイルと同じになります。
ここでは、readAsDataURL() を Promise で包んだ関数と、ファイルを選ぶとデータURLを表示してコピーできるコンポーネントを作りました。あわせて、canvas の toDataURL() を使うと中身が変わること、Uint8Array の toBase64() でも同じ文字列を作れることを確かめています。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Chromium 153 です。使ったのは React 19.3.1、TypeScript 7.0.2、esbuild 0.28.2、Playwright 1.63.0 です。画像は Pillow 10.2.0 で描いた PNG(240×160)と JPEG(240×160)、手で書いた SVG の3つで、写真ではありません。Playwright で Chromium を動かし、ファイルを選んでコピーするところまで操作しました。Firefox と Safari、スマートフォンのブラウザ、大きな画像(数 MB 以上)は試していません。

readAsDataURL() を Promise にする
FileReader は、読み終わると load イベントを出し、result にデータURLの文字列を入れます。イベントのままでは await で待てないので、Promise で包んだ関数にしておきます。引数は Blob にしているので、<input type="file"> で選んだ File も、fetch() で取ってきた Blob も渡せます。
// File や Blob の中身を、そのまま Base64 のデータURL(data:<MIMEタイプ>;base64,…)にする
export function fileToDataURL(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
データURLの先頭の data:image/png;base64, は、Blob の MIME タイプから付きます。検証では PNG が data:image/png;base64,、JPEG が data:image/jpeg;base64,、SVG が data:image/svg+xml;base64, になりました。MIME タイプが空の Blob では data:application/octet-stream;base64, になりました。Base64 の部分だけが欲しいときは、MDN の説明にあるとおり、先頭の data:*/*;base64, を取り除きます。
選んだ画像をデータURLにして、表示とコピーをする
App は、ファイルが選ばれたら fileToDataURL で変換し、プレビューの img、文字数、データURLの文字列を表示します。「コピーする」ボタンを押すと、navigator.clipboard.writeText() で文字列をクリップボードに入れます。
import { useState } from 'react';
import type { ChangeEvent } from 'react';
import { fileToDataURL } from './dataUrl';
type Converted = { name: string; size: number; dataUrl: string };
export function App() {
const [converted, setConverted] = useState<Converted | null>(null);
const [error, setError] = useState('');
const [copied, setCopied] = useState(false);
async function handleFile(e: ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setCopied(false);
try {
const dataUrl = await fileToDataURL(file);
setConverted({ name: file.name, size: file.size, dataUrl });
setError('');
} catch {
setConverted(null);
setError('ファイルを読み込めませんでした');
}
}
async function handleCopy() {
if (!converted) return;
await navigator.clipboard.writeText(converted.dataUrl);
setCopied(true);
}
return (
<main>
<input type="file" accept="image/*" aria-label="画像を選ぶ" onChange={handleFile} />
{error && <p role="alert" className="error">{error}</p>}
{converted && (
<>
<img className="preview" src={converted.dataUrl} alt="選んだ画像のプレビュー" />
<p className="info">
{converted.name}:元のファイル {converted.size.toLocaleString()} バイト → データURL {converted.dataUrl.length.toLocaleString()} 文字
</p>
<textarea className="output" readOnly rows={4} value={converted.dataUrl} aria-label="データURL" />
<div className="toolbar">
<button type="button" onClick={handleCopy}>コピーする</button>
<span className="copied" aria-live="polite">{copied ? 'コピーしました' : ''}</span>
</div>
</>
)}
</main>
);
}
PNG、JPEG、SVG の順に選んでコピーしたところ、どれもプレビューが表示されました。欄に出た文字列とクリップボードに入った文字列は、fileToDataURL を直接呼んだ結果と同じでした。
sample.png: sample.png:元のファイル 4,960 バイト → データURL 6,638 文字 | プレビュー 表示できた 240x160 | 欄の文字列が readAsDataURL の結果と同じ: true | コピーした文字列が同じ: true
sample.jpg: sample.jpg:元のファイル 13,486 バイト → データURL 18,007 文字 | プレビュー 表示できた 240x160 | 欄の文字列が readAsDataURL の結果と同じ: true | コピーした文字列が同じ: true
sample.svg: sample.svg:元のファイル 185 バイト → データURL 274 文字 | プレビュー 表示できた 120x80 | 欄の文字列が readAsDataURL の結果と同じ: true | コピーした文字列が同じ: true
コピーしたあとに別のファイルを選ぶと、setCopied(false) で「コピーしました」の表示が消えます。writeText() は、安全なコンテキスト(HTTPS のページや、localhost・127.0.0.1 のページ)でしか使えません。検証では http://127.0.0.1 のページで動かしました。
読み込みに失敗したときの表示は、readAsDataURL() を error イベントを出すだけのものに差し替えたページで確かめました。「ファイルを読み込めませんでした」と表示され、プレビューは出ませんでした。
データURLから戻すと、元のファイルと同じバイト列になる
readAsDataURL() が返した文字列から先頭を取り除き、Node.js の Buffer.from(…, 'base64') で戻して、元のファイルと比べました。3つとも、戻したバイト列は元のファイルと同じでした。
sample.png: 元 4960 バイト | 先頭 data:image/png;base64, | データURL 6638 文字(うち Base64 6616 文字、4×ceil(4960/3) = 6616) | 戻したバイト列が元と同じ: true(sha256 1c87496920c16d34 / 1c87496920c16d34)
sample.jpg: 元 13486 バイト | 先頭 data:image/jpeg;base64, | データURL 18007 文字(うち Base64 17984 文字、4×ceil(13486/3) = 17984) | 戻したバイト列が元と同じ: true(sha256 3d58ad7047819064 / 3d58ad7047819064)
sample.svg: 元 185 バイト | 先頭 data:image/svg+xml;base64, | データURL 274 文字(うち Base64 248 文字、4×ceil(185/3) = 248) | 戻したバイト列が元と同じ: true(sha256 439632eb4ffd6039 / 439632eb4ffd6039)
Base64 は、3バイトを4文字で表します。検証でも、Base64 の部分の文字数は、3つとも元のバイト数を3で割って切り上げ、4をかけた数と同じでした。4,960 バイトの PNG なら 6,616 文字で、先頭の data:image/png;base64, の22文字を足すと 6,638 文字です。データURLは元のファイルより3分の1ほど長くなるので、大きな画像を HTML や JSON に埋め込むと、そのぶん重くなります。
canvas の toDataURL() では、中身が描き直される
画像を canvas に描いて toDataURL() を呼んでも、データURLは作れます。ただし、これは canvas の画素から画像を作り直す書き方です。比べるために、createImageBitmap() で読み込んだ画像を同じ大きさの canvas に描き、toDataURL() で書き出しました。
sample.png → toDataURL(): 先頭 data:image/png;base64, | 8370 文字(中のファイル 6261 バイト、元は 4960 バイト) | 元と同じバイト列: false
sample.jpg → toDataURL(): 先頭 data:image/png;base64, | 115486 文字(中のファイル 86597 バイト、元は 13486 バイト) | 元と同じバイト列: false
sample.jpg → toDataURL('image/jpeg', 0.92): 先頭 data:image/jpeg;base64, | 20035 文字(中のファイル 15008 バイト、元は 13486 バイト) | 元と同じバイト列: false
3つとも、データURLに入っている画像ファイルは、元と違うバイト列になりました。形式を指定しないと PNG で書き出されるので、JPEG を渡すと PNG になり、13,486 バイトから 86,597 バイトに増えました。'image/jpeg' を指定しても、元のファイルと同じにはならず、15,008 バイトになりました。縮小や切り抜きをしてから送りたいときは canvas を使い、選んだファイルをそのまま文字列にしたいときは readAsDataURL() を使う、と分けると迷いません。
Uint8Array の toBase64() でも、同じ文字列を作れる
Uint8Array の toBase64() を使うと、イベントを待たずに書けます。Blob の arrayBuffer() でバイト列を取り出し、先頭の data:<MIMEタイプ>;base64, は自分で付けます。MIME タイプが空のときは、readAsDataURL() と同じ application/octet-stream にしています。
// Uint8Array の toBase64() で、データURLを組み立てる書き方
export async function fileToDataURLWithToBase64(file: Blob): Promise<string> {
const bytes = new Uint8Array(await file.arrayBuffer());
return `data:${file.type || 'application/octet-stream'};base64,${bytes.toBase64()}`;
}
PNG、JPEG、SVG と、MIME タイプを空にした PNG の4つで、readAsDataURL() の結果と1文字も違わない文字列になりました。
sample.png: 同じ文字列: true(6638 文字)
sample.jpg: 同じ文字列: true(18007 文字)
sample.svg: 同じ文字列: true(274 文字)
sample.png(MIME タイプなし): 同じ文字列: true(6653 文字)
toBase64() は新しいメソッドで、MDN の対応表では Chrome 140、Firefox 133、Safari 18.2 から使えます。TypeScript 7.0 では、tsconfig.json の lib を ESNext にすると型チェックが通りました。ES2022 や ES2025 では、どちらも次のエラーになりました。
src/toBase64.ts(4,74): error TS2339: Property 'toBase64' does not exist on type 'Uint8Array<ArrayBuffer>'.
古いブラウザでも動かしたいなら、readAsDataURL() の書き方を使います。
画像の URL からデータURLにする
画像の URL から作るときは、fetch() で Blob を取ってきて、同じ fileToDataURL に渡します。
// 画像の URL から読み込んで、データURLにする(別のオリジンの画像は、CORS で許可されていないと読めない)
export async function urlToDataURL(url: string): Promise<string> {
const res = await fetch(url);
if (!res.ok) throw new Error(`読み込めませんでした(${res.status})`);
return fileToDataURL(await res.blob());
}
ページと同じオリジンの画像は、ファイルを選んだときと同じ 6,638 文字のデータURLになりました。ポート番号だけを変えた別のオリジンのサーバーから読むと、Access-Control-Allow-Origin を返さないサーバーでは TypeError: Failed to fetch で失敗しました。Access-Control-Allow-Origin: * を返すサーバーからは読めました。別のオリジンの画像は、CORS で読み込みが許可されていないと、fetch() で中身を取れません。
/sample.png: 成功 6638 文字
http://127.0.0.1:5174/sample.png: 失敗 TypeError: Failed to fetch
http://127.0.0.1:5175/sample.png: 成功 6638 文字
/missing.png: 失敗 Error: 読み込めませんでした(404)
fetch() は、サーバーが 404 を返しても失敗にはなりません。そのため、res.ok を見て自分でエラーにしています。ない画像の URL を渡すと、読み込めませんでした(404) のエラーになりました。





