attach some images without converting them first

This commit is contained in:
or-else
2017-09-30 16:47:02 -07:00
parent 22e82b1c7f
commit 939e3e59cf
+172 -56
View File
@@ -50,6 +50,9 @@
// Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px. // Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px.
var REM_SIZE = 13; var REM_SIZE = 13;
// Size of the avatar image
var AVATAR_SIZE = 128;
// Number of chat messages to fetch in one call. // Number of chat messages to fetch in one call.
var MESSAGES_PAGE = 24; var MESSAGES_PAGE = 24;
@@ -61,6 +64,12 @@
// to adjust it to 1600 or 2400 for production. // to adjust it to 1600 or 2400 for production.
var MAX_IMAGE_SIZE = 768; var MAX_IMAGE_SIZE = 768;
// Supported image MIME types and corresponding file extensions.
var SUPPORTED_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/x-icon',
'image/vnd.microsoft.icon', 'image/svg', 'image/svg+xml'];
var MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'ico',
'ico', 'svg', 'svg'];
// Short representation of time in the past. // Short representation of time in the past.
function shortDateFormat(then) { function shortDateFormat(then) {
var locale = window.navigator.userLanguage || window.navigator.language; var locale = window.navigator.userLanguage || window.navigator.language;
@@ -83,8 +92,8 @@
'data:image/' + photo.type + ';base64,' + photo.data : null; 'data:image/' + photo.type + ';base64,' + photo.data : null;
} }
// Scale liner dimensions to fit uder certain size // Scale liner dimensions down to fit uder certain size.
function imageScaleDown(width, height, maxWidth, maxHeight) { function fitImageSize(width, height, maxWidth, maxHeight) {
if (!width || !height || !maxWidth || !maxHeight) { if (!width || !height || !maxWidth || !maxHeight) {
return null; return null;
} }
@@ -93,28 +102,121 @@
Math.min(height, maxHeight) / height Math.min(height, maxHeight) / height
); );
return {width: (width * scale) | 0, height: (height * scale) | 0}; var size = {width: (width * scale) | 0, height: (height * scale) | 0};
if (maxWidth == maxHeight) {
// Also calculate parameters for making the image square.
size.square = Math.min(width, height);
size.xoffset = ((width - size.square) / 2) | 0;
size.yoffset = ((height - size.square) / 2) | 0;
}
return size;
} }
// Convert uploaded image into a base64-encoded string. // Ensure file's extension matches mime content type
function imageFileToBase64(file, callback) { function fileNameForMime(fname, mime) {
var idx = SUPPORTED_FORMATS.indexOf(mime);
var ext = MIME_EXTENSIONS[idx];
var at = fname.lastIndexOf('.');
if (at >= 0) {
fname = fname.substring(0, at);
}
return fname + '.' + ext;
}
// Get mime type from data URL header.
function getMimeType(header) {
var mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header);
return (mime && mime.length > 1) ? mime[1] : null;
}
// Convert uploaded image into a base64-encoded string possibly scaling
// linear dimensions.
function imageFileScaledToBase64(file, width, height, forceSquare, onSuccess, onError) {
var img = new Image(); var img = new Image();
img.crossOrigin = 'Anonymous'; img.crossOrigin = 'Anonymous';
img.onerror = function(err) {
onError("Image format unrecognized");
}
img.onload = function() { img.onload = function() {
var size = imageScaleDown(this.width, this.height, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE); var size = fitImageSize(this.width, this.height, width, height);
if (!size) {
onError("Invalid image");
return;
}
var canvas = document.createElement('CANVAS'); var canvas = document.createElement('CANVAS');
canvas.width = size.width; if (forceSquare) {
canvas.height = size.height; canvas.width = canvas.height = width;
} else {
canvas.width = size.width;
canvas.height = size.height;
}
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, 0, 0, this.width, this.height, 0, 0, canvas.width, canvas.height); if (forceSquare) {
var imageBits = canvas.toDataURL("image/jpeg"); ctx.drawImage(this, size.xoffset, size.yoffset, size.square, size.square,
0, 0, canvas.width, canvas.height);
} else {
ctx.drawImage(this, 0, 0, this.width, this.height,
0, 0, canvas.width, canvas.height);
}
var mime = (forceSquare || SUPPORTED_FORMATS.indexOf(file.type) < 0) ? "image/jpeg" : file.type;
var imageBits = canvas.toDataURL(mime);
var parts = imageBits.split(',');
// Get actual image type: 'data:image/png;base64,'
mime = getMimeType(parts[0]);
if (!mime) {
onError("Unsupported image format");
return;
}
// Ensure the image is not too large
var quality = 0.78;
while (mime == "image/jpeg" && imageBits.length * 0.75 > MAX_ATTACHMENT_SIZE && quality > 0.15) {
imageBits = canvas.toDataURL(mime, quality);
quality *= 0.84;
}
if (imageBits.length * 0.75 > MAX_ATTACHMENT_SIZE) {
onError("The image size " + bytesToHumanSize(imageBits.length * 0.75) +
" exceeds the " + bytesToHumanSize(MAX_ATTACHMENT_SIZE) + " limit.", "err");
return;
}
canvas = null; canvas = null;
callback(imageBits, size.width, size.height); onSuccess(imageBits.split(',')[1], mime, size.width, size.height, fileNameForMime(file.name, mime));
}; };
img.src = URL.createObjectURL(file); img.src = URL.createObjectURL(file);
} }
// Convert uploaded image file to base64-encoded string without scaling/converting the image
function imageFileToBase64(file, onSuccess, onError) {
if (file.size > MAX_ATTACHMENT_SIZE) {
onError("The file size " + bytesToHumanSize(file.size) +
" exceeds the " + bytesToHumanSize(MAX_ATTACHMENT_SIZE) + " limit.", "err");
return;
}
var reader = new FileReader();
reader.addEventListener("load", function() {
var parts = reader.result.split(',');
var mime = getMimeType(parts[0]);
if (!mime) {
onError("Failed to process image file");
return;
}
// Get image size.
var img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function() {
onSuccess(parts[1], mime, this.width, this.height, fileNameForMime(file.name, mime));
}
img.onerror = function(err) {
onError("Image format unrecognized");
}
img.src = URL.createObjectURL(file);
}, false);
reader.readAsDataURL(file);
}
// Make shortcut icon appear with a green dot. // Make shortcut icon appear with a green dot.
function updateFavicon(on) { function updateFavicon(on) {
var oldIcon = document.getElementById("shortcut-icon"); var oldIcon = document.getElementById("shortcut-icon");
@@ -901,9 +1003,11 @@
<input type="password" placeholder="Repeat password" <input type="password" placeholder="Repeat password"
value={this.state.password2} onChange={this.handlePassword2Change} required /> value={this.state.password2} onChange={this.handlePassword2Change} required />
</div> </div>
<AvatarUpload onImageChanged={this.handleImageChanged} /> <AvatarUpload
onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div> </div>
<div className="panel-form-row"> <div className="panel-form-row">
<input type="text" placeholder="Email, e.g john.doe@example.com" <input type="text" placeholder="Email, e.g john.doe@example.com"
value={this.state.email} onChange={this.handleEmailChange} required/> value={this.state.email} onChange={this.handleEmailChange} required/>
</div> </div>
@@ -927,29 +1031,17 @@
}, },
handleFileUpload(e) { handleFileUpload(e) {
var instance = this; var instance = this;
var img = new Image(); imageFileScaledToBase64(e.target.files[0], AVATAR_SIZE, AVATAR_SIZE, true,
img.crossOrigin = 'Anonymous'; // Success
img.onload = function() { function(base64bits, mime) {
var canvas = document.createElement('CANVAS'); var du = makeImageUrl({data: base64bits, type: mime});
canvas.width = canvas.height = 128; instance.setState({dataUrl: du});
var ctx = canvas.getContext('2d'); instance.props.onImageChanged(du);
var sx = 0, sy = 0, size; },
if (this.height > this.width) { // Failure
size = this.width; function(err) {
sy = (this.height - size) / 2; this.props.onError(err, "err");
} else { });
size = this.height;
sx = (this.width - size) / 2;
}
ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, sx, sy, size, size, 0, 0, canvas.width, canvas.height);
var bits = canvas.toDataURL("image/jpeg");
canvas = null;
instance.setState({dataUrl: bits});
instance.props.onImageChanged(bits);
};
img.src = URL.createObjectURL(e.target.files[0]);
}, },
render: function() { render: function() {
return ( return (
@@ -1119,7 +1211,8 @@
this.props.state === 'register' ? this.props.state === 'register' ?
<CreateAccountView <CreateAccountView
onCreateAccount={this.props.onCreateAccount} onCreateAccount={this.props.onCreateAccount}
onCancel={this.props.onCancel} /> : onCancel={this.props.onCancel}
onError={this.props.onError} /> :
this.props.state === 'settings' ? this.props.state === 'settings' ?
<SettingsView onCancel={this.props.onCancel} <SettingsView onCancel={this.props.onCancel}
@@ -1130,7 +1223,8 @@
<EditAccountView <EditAccountView
login={this.props.login} login={this.props.login}
onSubmit={this.props.onUpdateAccount} onSubmit={this.props.onUpdateAccount}
onCancel={this.props.onCancel} /> : onCancel={this.props.onCancel}
onError={this.props.onError} /> :
this.props.state === 'contacts' ? this.props.state === 'contacts' ?
<ContactsView <ContactsView
@@ -1145,7 +1239,8 @@
foundContacts={this.props.foundContacts} foundContacts={this.props.foundContacts}
onInitFind={this.props.onInitFind} onInitFind={this.props.onInitFind}
onSearchContacts={this.props.onSearchContacts} onSearchContacts={this.props.onSearchContacts}
onCreateTopic={this.props.onCreateTopic} /> : onCreateTopic={this.props.onCreateTopic}
onError={this.props.onError} /> :
null} null}
</div> </div>
@@ -1244,7 +1339,8 @@
avatar={this.state.avatar} avatar={this.state.avatar}
uid={this.state.address} uid={this.state.address}
title={this.state.fullName} title={this.state.fullName}
onImageChanged={this.handleImageChanged} /> onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div> </div>
<div className="hr" /> <div className="hr" />
<div className="panel-form-column"> <div className="panel-form-column">
@@ -1793,7 +1889,9 @@
<input type="text" id="new-topic-priv" placeholder="Visible to you only" <input type="text" id="new-topic-priv" placeholder="Visible to you only"
value={this.state.private} onChange={this.handlePrivateChange} /> value={this.state.private} onChange={this.handlePrivateChange} />
</div> </div>
<AvatarUpload onImageChanged={this.handleImageChanged} /> <AvatarUpload
onError={this.props.onError}
onImageChanged={this.handleImageChanged} />
</div> </div>
<div className="dialog-buttons"> <div className="dialog-buttons">
<button className={submitClasses} onClick={this.handleSubmit}>Create</button> <button className={submitClasses} onClick={this.handleSubmit}>Create</button>
@@ -2225,7 +2323,8 @@
readOnly={!this.state.owner} readOnly={!this.state.owner}
uid={this.props.topic} uid={this.props.topic}
title={this.state.fullName} title={this.state.fullName}
onImageChanged={this.handleImageChanged} /> onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div> </div>
<div className="hr" /> <div className="hr" />
<div className="panel-form-column"> <div className="panel-form-column">
@@ -2366,7 +2465,7 @@
attr.key = elementKey; attr.key = elementKey;
if (style == "IM") { if (style == "IM") {
// Additional processing for images // Additional processing for images
var size = imageScaleDown(data.width, data.height, var size = fitImageSize(data.width, data.height,
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24); Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24);
attr.className = "inline-image"; attr.className = "inline-image";
attr.style = size ? { width: size.width + "px", height: size.height + "px" } : null; attr.style = size ? { width: size.width + "px", height: size.height + "px" } : null;
@@ -2835,19 +2934,34 @@
handleAttachImage: function(e) { handleAttachImage: function(e) {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
var file = e.target.files[0];
var instance = this; var instance = this;
imageFileToBase64(file, function(bits, width, height) { var files = e.target.files;
if (bits.length > MAX_ATTACHMENT_SIZE) { // Check if the uploaded file is indeed an image and if it isn't too large.
this.props.onError("The image size " + bytesToHumanSize(bits.length) + if (files[0].size > MAX_ATTACHMENT_SIZE || SUPPORTED_FORMATS.indexOf(files[0].type) < 0) {
" exceeds the " + bytesToHumanSize(MAX_ATTACHMENT_SIZE) + " limit.", "err"); // Convert image for size or format.
return; imageFileScaledToBase64(files[0], MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, false,
} // Success
function(bits, mime, width, height, fname) {
instance.props.sendMessage(Drafty.insertImage(null, instance.props.sendMessage(Drafty.insertImage(null,
0, file.type, bits.split(',')[1], width, height, 0, mime, bits, width, height, fname));
file.name)); },
}); // Failure
function(err) {
instance.props.onError(err, "err");
});
} else {
// Image can be uploaded as is. No conversion is needed.
imageFileToBase64(files[0],
// Success
function(bits, mime, width, height, fname) {
instance.props.sendMessage(Drafty.insertImage(null,
0, mime, bits, width, height, fname));
},
// Failure
function(err) {
instance.props.onError(err, "err");
});
}
} }
}, },
@@ -2967,7 +3081,7 @@
return null; return null;
} }
var instance = this; var instance = this;
var size = imageScaleDown(this.props.content.width, this.props.content.height, var size = fitImageSize(this.props.content.width, this.props.content.height,
this.state.width, this.state.hight); this.state.width, this.state.hight);
if (size) { if (size) {
size.width += "px"; size.width += "px";
@@ -3520,6 +3634,7 @@
onNewTopic={this.handleNewTopic} onNewTopic={this.handleNewTopic}
onCancel={this.handleSidepanelCancel} onCancel={this.handleSidepanelCancel}
onOnlineChange={this.handleTopicSelectedOnline} onOnlineChange={this.handleTopicSelectedOnline}
onError={this.handleError}
onInitFind={this.tnInitFind} onInitFind={this.tnInitFind}
contactsSearchQuery={this.state.contactsSearchQuery} contactsSearchQuery={this.state.contactsSearchQuery}
@@ -3566,6 +3681,7 @@
onLeaveTopic={this.handleLeaveUnsubRequest} onLeaveTopic={this.handleLeaveUnsubRequest}
onAddMember={this.handleManageGroupMembers} onAddMember={this.handleManageGroupMembers}
onInitFind={this.tnInitFind} onInitFind={this.tnInitFind}
onError={this.handleError}
showContextMenu={this.handleShowContextMenu} showContextMenu={this.handleShowContextMenu}
/> />