everything seems to work as expected
This commit is contained in:
+111
-97
@@ -77,132 +77,146 @@ export function fileNameForMime(fname, mime) {
|
||||
}
|
||||
|
||||
// Scale uploaded image to fit under certain dimensions and byte size, optionally constraining to a square.
|
||||
// On success calls onSuccess callback with the scaled image as Blob.
|
||||
export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare, onSuccess, onError) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onerror = function(err) {
|
||||
onError("Image format unrecognized");
|
||||
}
|
||||
img.onload = async function() {
|
||||
// Once the image is loaded, the URL is no longer needed.
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
// Calculate the desired image dimensions.
|
||||
const dim = fitImageSize(this.width, this.height, maxWidth, maxHeight, forceSquare);
|
||||
if (!dim) {
|
||||
onError("Invalid image");
|
||||
return;
|
||||
// Returns a promise which is resolven on success or rejected on failure.
|
||||
export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onerror = function(err) {
|
||||
reject(new Error("Image format unrecognized"));
|
||||
}
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = dim.dstWidth;
|
||||
canvas.height = dim.dstHeight;
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
|
||||
0, 0, dim.dstWidth, dim.dstHeight);
|
||||
img.onload = async function() {
|
||||
// Once the image is loaded, the URL is no longer needed.
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg';
|
||||
// Generate blob to check size of the image.
|
||||
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
|
||||
if (!blob) {
|
||||
onError("Unsupported image format");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the image is not too large. Shrink the image keeping the aspect ratio.
|
||||
// Do nothing if maxsize is <= 0.
|
||||
while (maxSize > 0 && blob.length > maxSize) {
|
||||
dim.dstWidth = (dim.dstWidth * 0.70710678118) | 0;
|
||||
dim.dstHeight = (dim.dstHeight * 0.70710678118) | 0;
|
||||
// Calculate the desired image dimensions.
|
||||
const dim = fitImageSize(this.width, this.height, maxWidth, maxHeight, forceSquare);
|
||||
if (!dim) {
|
||||
reject(new Error("Invalid image"));
|
||||
return;
|
||||
}
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = dim.dstWidth;
|
||||
canvas.height = dim.dstHeight;
|
||||
ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
|
||||
0, 0, dim.dstWidth, dim.dstHeight);
|
||||
blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
|
||||
}
|
||||
|
||||
canvas = null;
|
||||
onSuccess(mime, blob, dim.dstWidth, dim.dstHeight, fileNameForMime(fileOrBlob.name, mime));
|
||||
};
|
||||
img.src = URL.createObjectURL(fileOrBlob);
|
||||
const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg';
|
||||
// Generate blob to check size of the image.
|
||||
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
|
||||
if (!blob) {
|
||||
reject(new Error("Unsupported image format"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the image is not too large. Shrink the image keeping the aspect ratio.
|
||||
// Do nothing if maxsize is <= 0.
|
||||
while (maxSize > 0 && blob.length > maxSize) {
|
||||
dim.dstWidth = (dim.dstWidth * 0.70710678118) | 0;
|
||||
dim.dstHeight = (dim.dstHeight * 0.70710678118) | 0;
|
||||
canvas.width = dim.dstWidth;
|
||||
canvas.height = dim.dstHeight;
|
||||
ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
|
||||
0, 0, dim.dstWidth, dim.dstHeight);
|
||||
blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
|
||||
}
|
||||
|
||||
canvas = null;
|
||||
resolve({mime: mime, blob: blob, width: dim.dstWidth, height: dim.dstHeight, name: fileNameForMime(fileOrBlob.name, mime)});
|
||||
};
|
||||
img.src = URL.createObjectURL(fileOrBlob);
|
||||
});
|
||||
}
|
||||
|
||||
// Scale and crop image according to specified dimensions.
|
||||
// The coordinates are in unscaled image pixels, i.e. cut the rectangle first then scale it.
|
||||
export function imageCrop(mime, objURL, left, top, width, height, scale, onSuccess, onError) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onerror = function(err) {
|
||||
onError("Image format unrecognized");
|
||||
}
|
||||
img.onload = async function() {
|
||||
// Once the image is loaded, the URL is no longer needed.
|
||||
URL.revokeObjectURL(img.src);
|
||||
// returns a promise.
|
||||
export function imageCrop(mime, objURL, left, top, width, height, scale) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onerror = (err) => {
|
||||
reject(new Error("Image format unrecognized"));
|
||||
};
|
||||
img.onload = () => {
|
||||
// Once the image is loaded, the URL is no longer needed.
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = width * scale;
|
||||
canvas.height = height * scale;
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(this, left, top, width, height, 0, 0, canvas.width, canvas.height);
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = width * scale;
|
||||
canvas.height = height * scale;
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(this, left, top, width, height, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
mime = SUPPORTED_IMAGE_FORMATS.includes(mime) ? mime : 'image/jpeg';
|
||||
// Generate blob to check size of the image.
|
||||
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
|
||||
if (!blob) {
|
||||
onError("Unsupported image format");
|
||||
return;
|
||||
}
|
||||
// Allow GC.
|
||||
canvas = null;
|
||||
onSuccess(mime, blob, width, height);
|
||||
};
|
||||
img.src = objURL;
|
||||
mime = SUPPORTED_IMAGE_FORMATS.includes(mime) ? mime : 'image/jpeg';
|
||||
// Generate blob to check size of the image.
|
||||
canvas.toBlob((blob) => {
|
||||
// Allow GC.
|
||||
canvas = null;
|
||||
if (blob) {
|
||||
resolve({mime: mime, blob: blob, width: width, height: height});
|
||||
} else {
|
||||
reject(new Error("Unsupported image format"));
|
||||
}
|
||||
}, mime);
|
||||
};
|
||||
img.src = objURL;
|
||||
});
|
||||
}
|
||||
|
||||
// Convert File to base64 string.
|
||||
export function fileToBase64(file, onSuccess) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', function() {
|
||||
onSuccess(file.type, reader.result.split(',')[1], file.name);
|
||||
// Convert file to base64 string.
|
||||
export function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (evt) => {
|
||||
reject(reader.error);
|
||||
};
|
||||
reader.onload = () => {
|
||||
resolve({mime: file.type, bits: reader.result.split(',')[1], name: file.name});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// Convert Blob to base64 string.
|
||||
export function blobToBase64(blob, onSuccess) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', function() {
|
||||
onSuccess(blob.type, reader.result.split(',')[1]);
|
||||
// Convert Blob to base64 string. Returns a promise resolved with the base64 string and mime.
|
||||
export function blobToBase64(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (evt) => {
|
||||
reject(reader.error);
|
||||
};
|
||||
reader.onload = () => {
|
||||
resolve({mime: blob.type, bits: reader.result.split(',')[1]});
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
|
||||
// File pasted from the clipboard. It's either an inline image or a file attachment.
|
||||
// FIXME: handle large files out of band.
|
||||
export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
|
||||
var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
|
||||
for (var i in items) {
|
||||
var item = items[i];
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
|
||||
if (!Array.isArray(items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i in items) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
var file = item.getAsFile();
|
||||
const file = item.getAsFile();
|
||||
if (!file) {
|
||||
console.error("Failed to get file object from pasted file item", item.kind, item.type);
|
||||
onError("Failed to get file object from pasted file item");
|
||||
continue;
|
||||
}
|
||||
if (file.type && file.type.split('/')[0] == 'image') {
|
||||
// Handle inline image
|
||||
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
|
||||
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false, onImageSuccess, onError);
|
||||
} else {
|
||||
imageFileToBase64(file, onImageSuccess, onError);
|
||||
}
|
||||
onImageSuccess(file);
|
||||
} else {
|
||||
// Handle file attachment
|
||||
fileToBase64(file, onAttachmentSuccess, onError)
|
||||
onAttachmentSuccess(file);
|
||||
}
|
||||
// Indicate that the pasted data contains a file.
|
||||
return true;
|
||||
@@ -263,7 +277,7 @@ export function base64ToBlob(str, mime) {
|
||||
|
||||
return new Blob([buf], { type: mime });
|
||||
} catch(err) {
|
||||
console.log("Failed to convert base64 to blob: ", err);
|
||||
console.error("Failed to convert base64 to blob: ", err);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
+49
-6
@@ -7,8 +7,8 @@ import LazyImage from '../widgets/lazy-image.jsx'
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
import { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
|
||||
import { blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass } from './strformat.js';
|
||||
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass, shortenFileName } from './strformat.js';
|
||||
import { sanitizeImageUrl } from './utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -29,6 +29,7 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
// Size the already scaled image.
|
||||
function handleImageData(el, data, attr) {
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
@@ -72,8 +73,48 @@ function handleImageData(el, data, attr) {
|
||||
return el;
|
||||
}
|
||||
|
||||
function shortenFileName(filename) {
|
||||
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename;
|
||||
function quotedImage(data) {
|
||||
let promise;
|
||||
// Get the blob from the image data.
|
||||
if (data.val) {
|
||||
const blob = base64ToBlob(data.val, data.mime);
|
||||
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
|
||||
} else {
|
||||
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
} else {
|
||||
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scale the blob.
|
||||
return promise
|
||||
.then(blob => {
|
||||
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, true)
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
data.size = scaled.blob.size;
|
||||
data.width = scaled.width;
|
||||
data.height = scaled.height;
|
||||
delete data.ref;
|
||||
// Keeping the original file name, if provided: ex.data.name;
|
||||
|
||||
return blobToBase64(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.name;
|
||||
data.width = IMAGE_THUMBNAIL_DIM;
|
||||
data.height = IMAGE_THUMBNAIL_DIM;
|
||||
data.maxWidth = IMAGE_THUMBNAIL_DIM;
|
||||
data.maxHeight = IMAGE_THUMBNAIL_DIM;
|
||||
// Rethrow.
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
||||
@@ -233,9 +274,11 @@ function inlineImageAttr(attr, data) {
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
maxWidth: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
maxHeight: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
}
|
||||
attr.className = 'inline-image';
|
||||
attr.alt = shortenFileName((data && data.name) || this.formatMessage(messages.drafty_image));
|
||||
attr.alt = shortenFileName(data && data.name, 16) || this.formatMessage(messages.drafty_image);
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
@@ -287,7 +330,7 @@ export function quoteFormatter(style, data, values, key) {
|
||||
}
|
||||
el = React.Fragment;
|
||||
values = [<i key="ex" className="material-icons">attachment</i>,
|
||||
shortenFileName(fname || this.formatMessage(messages.drafty_attachment))];
|
||||
shortenFileName(fname, 16) || this.formatMessage(messages.drafty_attachment)];
|
||||
break;
|
||||
}
|
||||
return React.createElement(el, attr, values);
|
||||
|
||||
@@ -37,6 +37,15 @@ export function bytesToHumanSize(bytes) {
|
||||
return count.toFixed(round) + ' ' + sizes[bucket];
|
||||
}
|
||||
|
||||
// Shorten a file name to be under maxLength by clipping out the middle.
|
||||
export function shortenFileName(filename, maxLength) {
|
||||
if (typeof filename != 'string') {
|
||||
return filename;
|
||||
}
|
||||
return filename.length > maxLength ?
|
||||
filename.slice(0, maxLength/2 - 1) + '…' + filename.slice(1 - maxLength/2) : filename;
|
||||
}
|
||||
|
||||
// Get 32 bit integer hash value for a string. Ideally it should produce the same value
|
||||
// as Java's String#hash().
|
||||
function stringToColorHash(value) {
|
||||
|
||||
Reference in New Issue
Block a user