DEV: Refactor image compression for iOS (#32652)

This commit is contained in:
Penar Musaraj
2025-05-09 09:51:30 -04:00
committed by GitHub
parent 90c19ee54d
commit f3c41af772
6 changed files with 63 additions and 11 deletions

View File

@ -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;
});
}

View File

@ -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();

View File

@ -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)"

View File

@ -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

View File

@ -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,

View File

@ -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