mirror of
https://github.com/discourse/discourse.git
synced 2025-05-28 13:51:18 +08:00
DEV: Refactor image compression for iOS (#32652)
This commit is contained in:
@ -1,8 +1,15 @@
|
||||
async function fileToDrawable(file) {
|
||||
return await createImageBitmap(file);
|
||||
async function fileToDrawable(file, isIOS) {
|
||||
if (!isIOS) {
|
||||
return await createImageBitmap(file);
|
||||
} else {
|
||||
// iOS has performance issues with createImageBitmap on large images
|
||||
// this workaround borrowed from https://github.com/Donaldcwl/browser-image-compression/blob/master/lib/utils.js
|
||||
const dataUrl = await getDataUrlFromFile(file);
|
||||
return await loadImage(dataUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function drawableToImageData(drawable) {
|
||||
function drawableToImageData(drawable, isIOS) {
|
||||
const width = drawable.width,
|
||||
height = drawable.height,
|
||||
sx = 0,
|
||||
@ -12,13 +19,33 @@ function drawableToImageData(drawable) {
|
||||
|
||||
let canvas = new OffscreenCanvas(width, height);
|
||||
|
||||
// Check if the canvas is too large
|
||||
// iOS _still_ enforces a max pixel count of 16,777,216 per canvas
|
||||
const maxLimit = 4096;
|
||||
const maximumPixelCount = maxLimit * maxLimit;
|
||||
|
||||
if (isIOS && width * height > maximumPixelCount) {
|
||||
const ratio = Math.min(maxLimit / width, maxLimit / height);
|
||||
|
||||
canvas.width = Math.floor(width * ratio);
|
||||
canvas.height = Math.floor(height * ratio);
|
||||
}
|
||||
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
throw "Could not create canvas context";
|
||||
}
|
||||
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
|
||||
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// iOS strikes again, need to clear canvas to free up memory
|
||||
if (isIOS) {
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
ctx && ctx.clearRect(0, 0, 1, 1);
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
@ -45,9 +72,9 @@ function jpegDecodeFailure(type, imageData) {
|
||||
return imageData.data[3] === 0;
|
||||
}
|
||||
|
||||
export async function fileToImageData(file) {
|
||||
const drawable = await fileToDrawable(file);
|
||||
const imageData = drawableToImageData(drawable);
|
||||
export async function fileToImageData(file, isIOS) {
|
||||
const drawable = await fileToDrawable(file, isIOS);
|
||||
const imageData = drawableToImageData(drawable, isIOS);
|
||||
|
||||
if (isTransparent(file.type, imageData)) {
|
||||
throw "Image has transparent pixels, won't convert to JPEG!";
|
||||
@ -59,3 +86,21 @@ export async function fileToImageData(file) {
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
function getDataUrlFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = (e) => reject(e);
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import { fileToImageData } from "discourse/lib/media-optimization-utils";
|
||||
export default class MediaOptimizationWorkerService extends Service {
|
||||
@service appEvents;
|
||||
@service siteSettings;
|
||||
@service capabilities;
|
||||
|
||||
worker = null;
|
||||
workerUrl = getAbsoluteURL("/javascripts/media-optimization-worker.js");
|
||||
@ -64,7 +65,7 @@ export default class MediaOptimizationWorkerService extends Service {
|
||||
|
||||
let imageData;
|
||||
try {
|
||||
imageData = await fileToImageData(file.data);
|
||||
imageData = await fileToImageData(file.data, this.capabilities.isIOS);
|
||||
} catch (error) {
|
||||
this.logIfDebug(error);
|
||||
return resolve();
|
||||
|
@ -4641,7 +4641,7 @@ en:
|
||||
images:
|
||||
too_large: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size_kb}KB), please resize it and try again."
|
||||
too_large_humanized: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size}), please resize it and try again."
|
||||
larger_than_x_megapixels: "Sorry, the image you are trying to upload is too large (maximum dimension is %{max_image_megapixels} megapixels), please resize it and try again."
|
||||
larger_than_x_megapixels: "Sorry, the image with filename %{original_filename} is too large (maximum dimension is %{max_image_megapixels} megapixels), please resize it and try again."
|
||||
size_not_found: "Sorry, but we couldn't determine the size of the image. Maybe your image is corrupted?"
|
||||
placeholders:
|
||||
too_large: "(image larger than %{max_size_kb}KB)"
|
||||
|
@ -307,6 +307,7 @@ class UploadCreator
|
||||
I18n.t(
|
||||
"upload.images.larger_than_x_megapixels",
|
||||
max_image_megapixels: SiteSetting.max_image_megapixels,
|
||||
original_filename: @upload.original_filename,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
@ -121,6 +121,7 @@ onmessage = async function (e) {
|
||||
case "compress":
|
||||
try {
|
||||
DedicatedWorkerGlobalScope.debugMode = e.data.settings.debug_mode;
|
||||
|
||||
let optimized = await optimize(
|
||||
e.data.file,
|
||||
e.data.fileName,
|
||||
|
@ -111,7 +111,11 @@ RSpec.describe Upload do
|
||||
upload = UploadCreator.new(huge_image, "image.png").create_for(user_id)
|
||||
expect(upload.persisted?).to eq(false)
|
||||
expect(upload.errors.messages[:base].first).to eq(
|
||||
I18n.t("upload.images.larger_than_x_megapixels", max_image_megapixels: 10),
|
||||
I18n.t(
|
||||
"upload.images.larger_than_x_megapixels",
|
||||
max_image_megapixels: 10,
|
||||
original_filename: upload.original_filename,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
|
Reference in New Issue
Block a user