← すべてのドキュメント

ファイルのアップロード

うまくいかない場合は、リクエストが送られない、413 エラー、保存後に画像が消えるといった問題について、画像アップロードのトラブルシューティングガイドに従ってください。

アップロードハンドラーは自分で書きます。ハンドラーは、ユーザーが挿入、貼り付け、またはドロップしたファイルを受け取ってサーバーに送信し、文書に入れる URL をエディターに返します。ダウンロードに含まれるサンプルには、ASP.NET Web Forms、MVC、ASP.NET Core、PHP 用の動作するハンドラーと、以下のクライアントスクリプトが含まれています。

ハンドラーを設定しないと、ファイルはコンテンツに埋め込まれる

ハンドラーを設定していない場合、何もアップロードされませんが、ファイルはどこかに置かなければならないので、文書の中に入ります。

  • 画像は base64 の data: URL として埋め込まれます。画像は保存する HTML の一部になり、base64 によってサイズが約 3 分の 1 増えます。3 MB の写真は、1 つのフィールドの中の約 4 MB のテキストになります。これは、データベースへの書き込み時に保存が失敗する、切り詰められる、タイムアウトするといった問題の、よくある原因です。
  • 文書はファイル名付きのリンクとして挿入されますが、機能するアドレスはありません。エディターはリンク先を一時的な blob: URL にしますが、blob: は許可されたスキームではないので、コンテンツのサニタイザーがそれを削除します。読者は開けないリンクを受け取ることになります。代わりに挿入を拒否してユーザーに伝えるには、fileUploadLocalObjectUrl: false を設定してください。

ハンドラーがあれば、コンテンツには /imageuploads/20260918-a1b2c3.png のような短い URL が入ります。既存の文書を確認するには、保存した HTML を見てください。src="data:image/... となっていれば、ハンドラーは使われていません。

Insert Gallery ダイアログの「Upload」ボタンが探すのは、グローバルな window.rte_file_upload_handler だけです。エディターごとの file_upload_handler オプションだけを設定した場合、ギャラリーに「Upload」ボタンは表示されません。

簡単な方法:エディターに URL を渡す

サーバーがすでにファイルを受け付けてその URL を返すようになっていれば、JavaScript を一切書く必要はありません。

var config = {};

// The whole setup: the editor POSTs the file here and uses the URL you answer with.
config.file_upload_url = "/imageupload.ashx";

var editor = new RichTextEditor("#div_editor", config);

エディターはファイルを multipart/form-data として POST し、レスポンスから URL を読み取ります。ほかのエディターのエンドポイントがすでに返している形式も理解するので、TinyMCE や CKEditor 向けに書いたサーバーもそのまま使えます。

レスポンスの本文対象
{"location": "/img/x.png"}TinyMCE
{"url": "/img/x.png"}CKEditor の simple upload adapter
{"urls": {"default": "..."}}CKEditor、複数のサイズ
READY:/img/x.png以下のサンプルのハンドラー
{"error": {"message": "..."}} または ERROR:messageいずれも失敗として報告されます

どの形式にも当てはまらないレスポンスは、挿入されずにアップロードの失敗として扱われます。そのため、OK と返すエンドポイントが画像のソースになることはありません。

config.file_upload_field = "file";          // form field name; CKEditor's endpoints use "upload"
config.file_upload_base_path = "https://cdn.example.com";  // prepended to a relative URL
config.file_upload_credentials = true;      // send cookies on a cross-origin upload
config.file_upload_reuse_filename = true;   // keep the original name (default: a unique one)
config.file_upload_headers = { "X-CSRF-Token": token };

2 人が同じエンドポイントに image.png をアップロードしても互いに上書きしないよう、アップロードされた各ファイルには既定で一意の名前が付けられます。再試行、キャンセル、進捗のイベントは、すべて独自のハンドラーの場合と同じように動作します。送信前にサイズを変更する、S3 に直接アップロードする、独自のダイアログを表示するといった必要がある場合は、代わりにハンドラーを書いてください。両方を設定した場合は、常にハンドラーが優先されます。

構文

// One global handler for every editor on the page.
// The Insert Gallery dialog's Upload button uses this one.
window.rte_file_upload_handler = function (file, callback, optionalIndex, optionalFiles) { ... };

// Or per editor, for the image dialog, paste and drag-and-drop.
var config = {};
config.file_upload_handler = function (file, callback, optionalIndex, optionalFiles) { ... };
var editor = new RichTextEditor("#div_editor", config);
file

Type: File — name、size、type を持つ、ブラウザーの File オブジェクトです。XMLHttpRequest または fetch で送信します。

callback

Type: Function callback(url, errorMessage) — 必ず 1 回だけ呼び出してください。成功したら保存した URL を渡し、失敗したら null とメッセージを渡します。メッセージはエディターがユーザーに表示します。ユーザーがキャンセルしたアップロードも失敗として扱います。

optionalIndex, optionalFiles

複数のファイルが一度に挿入された場合、ハンドラーはファイルごとに呼び出されます。optionalFiles はファイル全体の集合、optionalIndex はそのファイルの位置なので、進捗を「2 / 5」のように表示できます。

サンプルのハンドラーはプレーンテキストで応答します。ステータス 200 の READY:<url>、またはステータス 500 の ERROR:<message> です。以下のクライアントスクリプトが、それらを 2 つの形のコールバックに変換します。

アップロードのキャンセル

ハンドラーから abort() メソッドを持つオブジェクト(または abort 関数そのもの)を返すと、エディターは実行中のリクエストを止められます。

config.file_upload_handler = function (file, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/imageupload.ashx");
    // ... send the file, then callback(url) or callback(null, message)
    xhr.send(formdata);

    // Returning this is what lets the editor stop the request.
    return { abort: function () { xhr.abort(); } };
};

editor.cancelUploads();          // abort everything in flight; returns the count
editor.getPendingUploadCount();  // how many are still running

ハンドラーが abort 関数を提供している場合、cancelUploads() と destroy() は実行中のリクエストを中止します。対象の画像が削除されている場合、遅れて届いた結果は破棄されますが、画像を削除しただけでネットワーク通信がすぐに中止されるとは限りません。キャンセルされたアップロードの遅れたコールバックは無視されるので、遅いレスポンスによって削除した画像が戻ってくることはありません。uploadcancel イベントが、ファイルと理由とともに発生します。何も返さなくても動作しますが、その場合はアップロードを中止できません。

すでにコンテンツに埋め込まれている画像

ハンドラーが呼び出されるのは、ユーザーが挿入、貼り付け、またはドロップしたファイルについてです。ほかの方法で入ってきたコンテンツ(setHTMLCode で設定したもの、Web ページやほかのエディターから貼り付けたもの、下書きから復元したもの)は base64 の data: URL のままなので、それらの画像は HTML の中に保存され、ハンドラーがそれを目にすることはありません。保存する前に uploadEmbeddedImages を呼び出してください。

ネイティブのフォームの場合は、以下の ID を使い、action を文書を保存するエンドポイントに置き換えてください。アップロードハンドラーは、エディターを作成する前に設定します。この例は、アップロードの完了を待ち、解決されていないローカルの画像を拒否し、ネイティブの検証と送信ボタンを維持します。

<form id="article-form" action="/your-document-save-endpoint" method="post">
  <!-- Include your framework's CSRF token field here. -->
  <div id="div_editor"></div>
  <input id="content" name="content" type="hidden">
  <p id="upload-save-status" role="status" aria-live="polite"></p>
  <button type="submit">Save document</button>
</form>
var form = document.getElementById("article-form");
var content = document.getElementById("content");
var saveStatus = document.getElementById("upload-save-status");
var scanning = false, approvedHTML = null;

form.addEventListener("submit", function (event) {
    var current = editor.getHTMLCode();
    if (approvedHTML !== null) {
        var approved = approvedHTML;
        approvedHTML = null;
        if (current !== approved || editor.getPendingUploadCount() > 0) {
            event.preventDefault();
            saveStatus.textContent = "Content changed. Wait for uploads, then save again.";
            return;
        }
        content.value = current; // Fresh snapshot in the native submit event.
        return;
    }
    event.preventDefault();
    if (scanning || editor.getPendingUploadCount() > 0) {
        saveStatus.textContent = "Images are still uploading. Wait, then save again.";
        return;
    }
    scanning = true;
    var submitter = event.submitter;
    saveStatus.textContent = "Checking embedded images...";
    try {
        editor.uploadEmbeddedImages(function (summary) {
            scanning = false;
            var localImage = Array.prototype.some.call(
                editor.getDocument().querySelectorAll("img"), function (image) {
                    return /^(data:|blob:|file:)/i.test((image.getAttribute("src") || "").trim());
                });
            if (summary.failed || summary.skipped || localImage || editor.getPendingUploadCount() > 0) {
                saveStatus.textContent = "Some images are not ready. Review and retry or remove them, then save again.";
                return;
            }
            approvedHTML = editor.getHTMLCode();
            saveStatus.textContent = "Images ready. Complete any required fields before saving.";
            // Re-run native validation; preserve the initiating submit button.
            if (submitter && submitter.form === form) form.requestSubmit(submitter);
            else form.requestSubmit();
            approvedHTML = null; // Validation may have stopped the submit event.
        });
    } catch (error) {
        scanning = false;
        approvedHTML = null;
        saveStatus.textContent = "Could not prepare images. Check your upload handler and try again.";
    }
});

これは現在の文書をたどり、data:image/ のソースをすべて File に変換し、1 つずつハンドラーに送信して、各ソースを返された URL に置き換えます。すでに http(s) にある画像はそのままです。ハンドラーが設定されていない場合は何も変更されず、すべての画像がスキップとして数えられます。

保留中の数が 0 でも、成功の証拠にはなりません。アップロードが失敗またはキャンセルされた後は、欠けている画像を確認してください。form.submit() を直接呼び出さないでください。ネイティブの検証と submit のリスナーを迂回してしまいます。画像のアップロードは文書の保存とは別のものです。サーバーは、POST された HTML を保存または表示する前に、認証、認可、サニタイズを行う必要があります。

自動でアップロードし、失敗を再試行する

automaticUploads は、上記のスキャンを自動的にスケジュールします。これは保存の完了を保証するものではありません。保留中とエラーのチェックは残し、保存用のスナップショットを取る前に、解決されていない埋め込みの画像を調べてください。

var config = {};
config.file_upload_handler = rte_file_upload_handler;

// Upload embedded base64 images by themselves, after setHTMLCode and after a paste.
config.automaticUploads = true;
config.automaticUploadsDelay = 300;   // ms debounce; a paste of several images uploads once

// Retry a failed upload before telling the user about it.
config.uploadRetryCount = 2;          // extra attempts after the first (default 0)
config.uploadRetryDelay = 1000;       // ms before the first retry; doubles after that

これは setHTMLCode の後と、HTML が挿入または貼り付けられた後に、デバウンスされて実行されます。ハンドラーが設定されていない場合は何もしません。uploadRetryCount は、もう 1 つのよくある失敗に対応します。以前は、接続が 1 回切れただけで、画像が埋め込まれたままになっていました。再試行が成功した場合は何も表示されず、試行のたびに uploadretry イベントが発生し、試行回数を使い切ると、これまでどおりコールバックのエラーが報告されます。

アップロードとコンテンツのサイズを制限する

maxUploadFileSize を設定すると、エディター自身が、それより大きなファイルを読み取る前に拒否します。何も埋め込まれず何も送信されず、どのサイズが拒否されたかと上限がユーザーに伝えられます。

var config = {};

// Largest file the editor will insert or upload, in bytes. 0 (the default) = no limit.
config.maxUploadFileSize = 5 * 1024 * 1024;   // 5 MB

// Optional: replace the default alert with your own dialog.
// The handler receives (state, name, message); set state.returnValue = true to say
// you have handled it, and the editor shows nothing itself.
config.oncustomdialog = function (state, name, message) {
  if (name !== "uploadtoobig") return;   // leave the others to the editor
  showMyDialog(message);
  state.returnValue = true;
};

これは、ファイルが入ってくるすべての経路(画像のダイアログ、文書のダイアログ、貼り付け、ドラッグ&ドロップ)に適用されます。貼り付けた HTML の中にすでに埋め込まれている画像(たとえば Word からのもの)は対象外です。貼り付けの途中で 1 つを拒否すると、文書が中途半端に変換された状態になってしまうからです。その場合の最後の備えが、下記の maxHTMLLength です。

ブラウザーは制限を適用する場所として信頼できないので、ハンドラーでのチェックも残してください。サンプルのハンドラーは 4,000,000 バイトを超えるものをすべて拒否し、ERROR:file too big を返します。

サーバーの 2 つの制限も、最大のファイルより大きくしておく必要があります。

  • ASP.NET: <httpRuntime> の maxRequestLength(キロバイト単位、既定値は 4096)と、<requestLimits> の maxAllowedContentLength(バイト単位、既定値は約 30 MB)。
  • PHP: upload_max_filesize(既定値は 2 MB)と post_max_size。これらより大きいファイルは、ハンドラーが実行される前に PHP によって破棄され、エディターはファイルが届かなかったと報告します。

文書自体に上限を設けるには、文字数の制限を設定します。どちらのオプションも既定ではオフ(0)で、編集が制限を超えると、エディターはデータベースに拒否されるものを保存するのではなく、最後の正常な状態に戻してメッセージを表示します。

var config = {};
config.maxHTMLLength = 60000;  // characters INCLUDING the HTML tags; 0 = no limit
config.maxTextLength = 0;      // characters EXCLUDING the tags; 0 = no limit

maxHTMLLength は、HTML を保存する列のサイズより少し小さく設定してください。既定のアラートを独自のダイアログに置き換えるには、reachmaxlength フックを処理します。

アップロードする前に写真を縮小する

スマートフォンのカメラは、どんな Web ページにも必要ないほど大きなファイルを作ります。maxUploadFileSize で拒否しても、書き手に別の画像を探すよう伝えるだけです。エディター自体にはサイズ変更の設定はありません。保存するコピーの大きさを知っているのはアプリケーションだけなので、それはハンドラーで行うべきものです。canvas で画像を縮小し、元の画像の代わりにその結果をアップロードしてください。

config.file_upload_handler = function (file, callback, optionalIndex, optionalFiles) {
    var MAXWIDTH = 1600, MAXHEIGHT = 1600, QUALITY = 0.82;

    function send(blob, name) {
        // your own upload code: post "blob" under the name "name",
        // then callback(theUrl) or callback(null, theError).
    }

    // Leave anything that is not a still photograph alone. GIFs would lose
    // their animation, and a PDF is not something a canvas can redraw.
    if (file.type.indexOf("image/") !== 0 || file.type === "image/gif") {
        send(file, file.name);
        return;
    }

    var url = URL.createObjectURL(file);
    var img = new Image();
    img.onload = function () {
        var scale = Math.min(1, MAXWIDTH / img.width, MAXHEIGHT / img.height);
        if (scale === 1) { URL.revokeObjectURL(url); send(file, file.name); return; }  // already small enough

        var canvas = document.createElement("canvas");
        canvas.width = Math.round(img.width * scale);
        canvas.height = Math.round(img.height * scale);
        canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
        URL.revokeObjectURL(url);

        canvas.toBlob(function (blob) {
            if (!blob) { send(file, file.name); return; }   // the browser refused: send the original
            send(blob, file.name.replace(/\.[^.]+$/, "") + ".jpg");
        }, "image/jpeg", QUALITY);
    };
    img.onerror = function () { URL.revokeObjectURL(url); callback(null, "not-an-image"); };
    img.src = url;
};

2400 × 1600 の写真で計測した結果:3.9 MB の元の画像は 94 KB の 1600 × 1067 の JPEG としてアップロードされ、文書にはサーバーが返した URL が入ります。

注意すべき点が 2 つあります。JPEG への変換では透明度が失われるので、ユーザーが写真だけでなくロゴも貼り付ける場合は、file.type で処理を分けてください。また、再エンコードするとファイルのメタデータが取り除かれます。写真の GPS 座標はそこにあるので通常は歓迎すべきことですが、メタデータに依存している場合は問題になります。

アップロードが失敗したとき

null とメッセージを渡してコールバックを呼び出します(callback(null, "http-error-413"))。エディターは The file could not be uploaded. の後に、かっこで囲んだメッセージを表示します。その前に uploadfailed フックが発生するので、代わりに独自のダイアログを表示することもできます。このフックは、渡したメッセージをそのまま(周りの文ではなく生のコードとして)受け取るので、それによって処理を分けられます。

設定済みのアップロードハンドラーが失敗を報告すると、現在配布されているコアは base64 のプレビューを残さず、画像を取り除きます。maxUploadFileSize をエンドポイントが適用する上限に設定し、ファイルを読み取る前に拒否されるようにしてください。

JavaScript のテンプレートコード

ダウンロードに含まれる動作するハンドラーを基にしたこのクライアントスクリプトは、「Cancel」ボタン付きの進捗ダイアログを表示し、ファイルを POST して、URL またはエラーを渡してコールバックを呼び出します。結果は 1 回だけ確定し、60 秒でタイムアウトし、キーボードでのキャンセルに対応し、フォーカスを元に戻します。ページ単位の rte_upload_state カウンターは、エディターの API ではなくこの例のものです。複数のエディターがあるアプリケーションでは、エディターごとに分けてください。

var uploadhandlerpath = "/imageupload.ashx";

function rte_file_upload_handler(file, callback, optionalIndex, optionalFiles) {
	var originalCallback = callback;
	var finished = false;
	rte_upload_state.pending++;
	callback = function (url, error) {
		if (finished) return;
		finished = true;
		if (error) rte_upload_state.error = String(error);
		try { originalCallback(url, error); }
		finally { rte_upload_state.pending--; }
	};
	try {

	function append(parent, tagname, csstext) {
		var tag = parent.ownerDocument.createElement(tagname);
		if (csstext) tag.style.cssText = csstext;
		parent.appendChild(tag);
		return tag;
	}

	var uploadcancelled = false;

	var dialogouter = append(document.body, "div", "display:flex;align-items:center;justify-content:center;z-index:2147483646;position:fixed;inset:0;padding:16px;box-sizing:border-box;background-color:rgba(15,23,42,0.28)");
	var dialoginner = append(dialogouter, "div", "box-sizing:border-box;min-width:260px;max-width:calc(100vw - 32px);padding:16px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;box-shadow:0 16px 40px rgba(15,23,42,.2)");

	var line1 = append(dialoginner, "div", "margin:0 0 10px;color:#172033;font:700 15px/1.3 Aptos,Segoe UI,sans-serif;");
	var previousFocus = document.activeElement;
	window.__rteDemoUploadDialogId = (window.__rteDemoUploadDialogId || 0) + 1;
	line1.id = "rte-upload-label-" + window.__rteDemoUploadDialogId;
	dialogouter.setAttribute("role", "dialog");
	dialogouter.setAttribute("aria-modal", "true");
	dialogouter.setAttribute("aria-labelledby", line1.id);
	line1.innerText = "Uploading...";

	var totalsize = file.size;
	var sentsize = 0;

	if (optionalFiles && optionalFiles.length > 1) {
		totalsize = 0;
		for (var i = 0; i < optionalFiles.length; i++) {
			totalsize += optionalFiles[i].size;
			if (i < optionalIndex) sentsize = totalsize;
		}
		line1.innerText = "Uploading..." + (optionalIndex + 1) + "/" + optionalFiles.length;
	}

	var line2 = append(dialoginner, "div", "margin:0 0 8px;color:#52657e;font:600 12px/1.3 Aptos,Segoe UI,sans-serif;");
	line2.innerText = "0%";

	var progressbar = append(dialoginner, "div", "height:8px;overflow:hidden;border-radius:4px;background:#e7eef7;");
	progressbar.setAttribute("role", "progressbar");
	progressbar.setAttribute("aria-label", "File upload progress");
	progressbar.setAttribute("aria-valuemin", "0");
	progressbar.setAttribute("aria-valuemax", "100");
	progressbar.setAttribute("aria-valuenow", "0");
	var progressbg = append(progressbar, "div", "width:0;height:100%;border-radius:inherit;background:#2487e8;transition:width 120ms ease;");

	var line3 = append(dialoginner, "div", "display:flex;justify-content:flex-end;margin-top:14px;");
	var btn = append(line3, "button");
	btn.style.cssText = "min-height:32px;padding:7px 11px;border:1px solid #bdd0e6;border-radius:7px;background:#fff;color:#315277;font:700 12px/1 Aptos,Segoe UI,sans-serif;cursor:pointer;";
	btn.type = "button";
	btn.setAttribute("aria-label", "Cancel file upload");
	dialoginner.addEventListener("keydown", function (event) {
		if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); btn.click(); }
		else if (event.key === "Tab" && !event.ctrlKey && !event.metaKey && !event.altKey) { event.preventDefault(); btn.focus(); }
	});
	btn.focus();
	btn.innerText = "Cancel";
	btn.onclick = function () {
		uploadcancelled = true;
		xh.abort();
	}

	var xh = new XMLHttpRequest();
	xh.timeout = 60000;
	xh.open("POST", uploadhandlerpath + "?name=" + encodeURIComponent(file.name || "edited-image.png") + "&type=" + encodeURIComponent(file.type || "image/png") + "&size=" + file.size, true);
	xh.onload = xh.onabort = xh.onerror = xh.ontimeout = function (pe) {
		if (finished) return;
		var restoreFocus = dialoginner.contains(document.activeElement);
		dialogouter.remove();
		try { if (restoreFocus && previousFocus && previousFocus.isConnected && typeof previousFocus.focus === "function") previousFocus.focus(); } catch (error) { /* Never prevent upload settlement. */ }
		if (pe.type == "load") {
			if (xh.status != 200) {
				if (xh.responseText.startsWith("ERROR:")) {
					callback(null, "http-error-" + xh.responseText.substring(6));
				}
				else {
					callback(null, "http-error-" + xh.status);
				}
			}
			else if (xh.responseText.startsWith("READY:")) {
				var resolved;
				try {
					var value = xh.responseText.substring(6).trim();
					if (!value) throw new Error('Missing URL');
					resolved = new URL(value, new URL(uploadhandlerpath, document.baseURI));
					if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:') throw new Error('Invalid URL');
				} catch (error) { callback(null, 'Server returned an invalid image URL'); return; }
				callback(resolved.href);
			}
			else {
				callback(null, "http-error-" + xh.responseText);
			}
		}
		else if (uploadcancelled) {
			callback(null, "cancelled");
		}
		else {
			callback(null, pe.type);
		}
	}
	xh.upload.onprogress = function (pe) {
		if (finished) return;
		//pe.total
		var percent = totalsize > 0 ? Math.max(0, Math.min(100, Math.floor(100 * (sentsize + pe.loaded) / totalsize))) : 0;
		line2.innerText = percent + "%";
		progressbar.setAttribute("aria-valuenow", String(percent));

		progressbg.style.width = percent + "%";
	}
	xh.send(file);
	return function () { if (!finished) { uploadcancelled = true; xh.abort(); } };
	} catch (error) {
		if (finished) throw error;
		var restoreFailedFocus = dialoginner && dialoginner.contains(document.activeElement);
		if (dialogouter && dialogouter.parentNode) dialogouter.parentNode.removeChild(dialogouter);
		try { if (restoreFailedFocus && previousFocus && previousFocus.isConnected && typeof previousFocus.focus === "function") previousFocus.focus(); } catch (focusError) { /* Preserve upload error. */ }
		callback(null, 'Could not start upload');
	}
}

// Single-page demo state, not an editor API.
var rte_upload_state = { pending: 0, error: '' };

var config = {
    file_upload_handler: rte_file_upload_handler,
    maxUploadFileSize: 4000000, // Match your server byte limit.
    allowBase64Images: false
};
var editor = new RichTextEditor("#div_editor", config);

アップロードした画像の配置

エディターは独自の配置を適用しません。挿入された画像は、align 属性もスタイルもないインラインの <img> なので、テキストの中に置かれ、それを含む段落の配置に従います。段落が中央揃えや右揃えでない限り、左揃えになります。

<!-- what the editor inserts: no align attribute, no style -->
<p>Some text <img src="/imageuploads/20260918-a1b2c3.png" loading="lazy" decoding="async"></p>

<!-- the same image, centred only because its paragraph is centred -->
<p style="text-align:center"><img src="/imageuploads/20260918-a1b2c3.png" loading="lazy" decoding="async"></p>

したがって、変更すべき「画像の既定の配置」の設定はありません。どこでも画像が中央揃えに見える場合、原因は自分のスタイルシートのルールか、中央揃えの段落です。保存した HTML を確認してください。<img> に配置の指定がなければ、原因は CSS です。

ユーザーは、画像を選択して画像ツールバーの配置メニューを使うことで、画像ごとに配置を設定します。すべての画像の配置を変えるには、自分の CSS にルールを書き、エディターにも同じルールを与えて、編集時の表示が公開ページと一致するようにしてください。

var config = {};

// Applies inside the editing area. Use the same rule on the pages that display
// the saved content, or the two will not look alike.
config.contentCssText = "p { text-align: left; } p img { float: none; }";

// Or load one stylesheet in both places:
config.contentCssUrl = "/css/editor-content.css";

関連情報