Upload files
You write the upload handler. It receives the file the user inserted, pasted or dropped, sends it to your server, and hands the editor back the URL to put in the document. The samples in the download include a working handler for ASP.NET Web Forms, MVC, ASP.NET Core and PHP, plus the client script below.
Configure a handler, or files are embedded in the content
With no handler configured, nothing is uploaded and the file still has to go somewhere, so it goes into the document:
- An image is embedded as a base64
data:URL. The picture becomes part of the HTML you save, and base64 adds about a third: a 3 MB photo turns into roughly 4 MB of text in one field. This is the usual cause of a save that fails, truncates, or times out when writing to the database. - A document is inserted as a link carrying the file name, but without a working address: the editor points it at a temporary
blob:URL, and the content sanitizer removes that, becauseblob:is not one of the schemes it allows. The reader gets a link that cannot be opened. SetfileUploadLocalObjectUrl: falseto refuse the insert and tell the user instead.
With a handler, the content holds a short URL such as /imageuploads/20260918-a1b2c3.png. To check an existing document, look at the HTML you stored: src="data:image/... means the handler is not being used.
The Insert Gallery dialog’s Upload button looks only for the window.rte_file_upload_handler global. If you set only the per-editor file_upload_handler option, the gallery shows no Upload button.
Syntax
// 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 — the browser’s File object, with name, size and type. Send it with XMLHttpRequest or fetch.
callback
Type: Function callback(url, errorMessage)— call it exactly once. On success pass the stored URL; on failure pass null and a message, which the editor shows to the user. An upload the user cancels is a failure too.
optionalIndex, optionalFiles
When several files are inserted at once, the handler is called for each one: optionalFiles is the whole set and optionalIndexis the position of this file, so progress can be reported as “2 of 5”.
The sample handlers answer in plain text: READY:<url> with status 200, or ERROR:<message> with status 500. The client script below turns those into the two callback forms.
Limiting the size of uploads and of the content
Set maxUploadFileSize and the editor refuses a larger file itself, before reading it. Nothing is embedded and nothing is sent, and the user is told which size was rejected and what the limit is:
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;
};It covers every route a file arrives by: the image dialog, the document dialog, paste, and drag-and-drop. Images already embedded inside pasted HTML (from Word, for example) are not covered, because refusing one mid-paste would leave the document half converted; maxHTMLLength below is the backstop for that.
Keep the check in your handler as well, since a browser is not a trustworthy place to enforce a limit. The sample handler rejects anything over 4,000,000 bytes and answers ERROR:file too big.
Two server limits must also be above your largest file:
- ASP.NET:
maxRequestLengthin<httpRuntime>(kilobytes, default 4096) andmaxAllowedContentLengthin<requestLimits>(bytes, default about 30 MB). - PHP:
upload_max_filesize(default 2 MB) andpost_max_size. Below these, PHP discards the file before your handler runs and the editor reports that no file was received.
To cap the document itself, set a character limit. Both options are off by default (0), and when an edit crosses the limit the editor reverts to the last good state and shows a message rather than saving something your database will reject:
var config = {};
config.maxHTMLLength = 60000; // characters INCLUDING the HTML tags; 0 = no limit
config.maxTextLength = 0; // characters EXCLUDING the tags; 0 = no limitSet maxHTMLLength a little below the size of the column you store the HTML in. Handle the reachmaxlength hook to replace the default alert with your own dialog.
Shrinking photographs before they are uploaded
A phone camera produces files far larger than any web page needs, and refusing them with maxUploadFileSize only tells the writer to go and find a different picture. The editor has no resizing setting of its own; the handler is where it belongs, because only your application knows how large the stored copy should be. Scale the image on a canvas and upload the result instead of the original:
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;
};Measured on a 2400 × 1600 photograph: the 3.9 MB original is uploaded as a 1600 × 1067 JPEG of 94 KB, and the document holds the URL your server returned.
Two things to keep in mind. The conversion to JPEG discards transparency, so branch on file.typeif your users paste logos as well as photographs. And re-encoding strips the file's metadata — usually welcome, since that is where a photograph's GPS coordinates live, but not if you rely on it.
When an upload fails
Call the callback with null and a message: callback(null, "http-error-413"). The editor shows The file could not be uploaded. followed by your message in brackets, and the uploadfailedhook fires first, so you can show your own dialog instead. That hook receives your message exactly as you passed it — the raw code, not the sentence around it — so you can branch on it.
A failed upload does not leave the image out: the editor falls back to embedding it in the content as a base64 data: URL, the same as if no handler were configured. An upload endpoint that quietly rejects large files therefore produces exactly the oversized HTML you were trying to avoid. Set maxUploadFileSize to the limit your endpoint enforces, so the file is refused before it is read.
Template JavaScript code
This is the client script from the samples: it shows a progress dialog with a Cancel button, posts the file, and calls back with the URL or the error.
var uploadhandlerpath = "/imageupload.ashx";
function rte_file_upload_handler(file, callback, optionalIndex, optionalFiles) {
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;background-color:rgba(15,23,42,0.28)");
dialogouter.setAttribute("role", "dialog");
dialogouter.setAttribute("aria-modal", "true");
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;");
line1.id = "rte-upload-progress-title-" + Date.now();
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;
}
console.log(totalsize, optionalIndex, optionalFiles)
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;");
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.innerText = "Cancel";
btn.onclick = function () {
uploadcancelled = true;
xh.abort();
}
var xh = new XMLHttpRequest();
xh.open("POST", uploadhandlerpath + "?name=" + encodeURIComponent(file.name) + "&type=" + encodeURIComponent(file.type) + "&size=" + file.size, true);
xh.onload = xh.onabort = xh.onerror = function (pe) {
console.log(pe);
console.log(xh);
dialogouter.remove();
if (pe.type == "load") {
if (xh.status != 200) {
console.log("uploaderror", pe);
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:")) {
console.log("File uploaded to " + xh.responseText.substring(6));
callback(xh.responseText.substring(6));
}
else {
callback(null, "http-error-" + xh.responseText);
}
}
else if (uploadcancelled) {
console.log("uploadcancelled", pe);
callback(null, "cancelled");
}
else {
console.log("uploaderror", pe);
callback(null, pe.type);
}
}
xh.upload.onprogress = function (pe) {
console.log(pe);
//pe.total
var percent = totalsize ? Math.floor(100 * (sentsize + pe.loaded) / totalsize) : 0;
line2.innerText = percent + "%";
progressbg.style.cssText = "width:" + percent + "%";
}
xh.send(file);
}How an uploaded image is aligned
The editor applies no alignment of its own. An inserted image is an inline <img> with no alignattribute and no style, so it sits in the text and follows the alignment of the paragraph that contains it — left, unless that paragraph is centred or right-aligned:
<!-- 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>There is therefore no “default image alignment” setting to change. If images look centred everywhere, the cause is a rule in your own stylesheet, or paragraphs that are centred. Check the HTML you stored: if the <img> carries no alignment, it is your CSS.
Users set alignment per image by selecting it and using the alignment menu on the image toolbar. To change what every image does, write the rule in your own CSS and give the editor the same rule, so the editing view matches the published page:
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";Further reading
- Image and file uploads, done properly — the failure cases a handler has to cover: uploads that fail halfway, pasted images, orphaned files.
- Image uploads and the gallery in ASP.NET Web Forms and MVC and in ASP.NET Core — handler code, where images are stored, and a folder-browsing gallery endpoint.
- Image uploads and the gallery in PHP — the same for PHP, including the upload limits above.