Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2411 - Compress large images attached in the chat #2428

Merged
merged 11 commits into from
Dec 2, 2024

Conversation

SebinSong
Copy link
Collaborator

@SebinSong SebinSong commented Nov 19, 2024

closes #2411



[NOTE]

  • As @taoeffect suggested in the issue, 400KB will be the size value to determine compression is needed.

  • The app uses HTML canvas API (toBlob(), drawImage()) to compress the image on the browser, which is a popular client-side image-compression npm package internally uses as well.

  • The compression outcome will be one of two most popular images formats on the web that support lossy-compression (image/webp or image/jpeg). According to tests I've done (pls feel free to check out this codepen I created for testing for yourself too), converting to 'webp' format apparently has a better compression efficiency than 'jpeg' (meaning it drops the file size below 400KB without sacrificing too much quality). So if the browser supports webp format, the outcome will always be webp and otherwise jpeg.

@SebinSong SebinSong self-assigned this Nov 19, 2024
Copy link

cypress bot commented Nov 19, 2024

group-income    Run #3494

Run Properties:  status check passed Passed #3494  •  git commit 321d2baf89 ℹ️: Merge 1985f28feda882fd83e1a646bca81364e2486c7c into 5f68446576179fd93a5428ebfba3...
Project group-income
Branch Review sebin/task/#2411-compress-images-before-uploading
Run status status check passed Passed #3494
Run duration 08m 46s
Commit git commit 321d2baf89 ℹ️: Merge 1985f28feda882fd83e1a646bca81364e2486c7c into 5f68446576179fd93a5428ebfba3...
Committer Sebin Song
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 10
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 111
View all changes introduced in this branch ↗︎

@SebinSong SebinSong changed the title [WIP] #2411 - Compress large image attachments in chat #2411 - Compress large image attachments in chat Nov 19, 2024
const sizeDiff = blob.size - IMAGE_ATTACHMENT_MAX_SIZE

if (sizeDiff <= 0 || // if the compressed image is already smaller than the max size, return the compressed image.
quality <= 0.3) { // Do not sacrifice the image quality too much.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the multiple tests I've done, when the quality factor value passed to canvas.toBlob() becomes <= 0.3, the outcome image starts to lose the quality to an unpleasant level. 0.3 here is a value I chose subjectively, but feel free to try compression test yourself using this codepen I created for this task and let me know if you would like a change.

@SebinSong SebinSong changed the title #2411 - Compress large image attachments in chat #2411 - Compress large images attached in the chat Nov 19, 2024
@taoeffect
Copy link
Member

So if the browser supports webp format, the outcome will always be webp and otherwise jpeg.

Question: what browsers don't support webp?

And if there are browsers that we want to support that don't support webp, wouldn't that mean that we shouldn't use it at all (since some users wouldn't be able to view the images)?

// url here is an instance of URL.createObjectURL(), which needs to be converted to a 'Blob'
const attachmentBlob = await objectURLtoBlob(url)
const attachmentBlob = needsIamgeCompression
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in Image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. fixed it.

export async function compressImage (imgUrl: string, sourceMimeType?: string): Promise<any> {
// Takes a source image url and generate a blob of the compressed image.

// According to the testing result, 0.8 is a good starting point for both resizingFactor and quality for .jpeg and .webp.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which testing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this shouldn't be a 'resizing factor' anyhow, but it should resize images to some arbitrary maximum size. E.g., not the real suggestion, the max size could be 1024×768, and images are scaled not to exceed this. The dimensions would come from how the images are displayed in the chat.

Copy link
Collaborator Author

@SebinSong SebinSong Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like I wrote in the desc, check this codepen I created for this task : https://codepen.io/freenaturalsoul/pen/vYoMWWv?editors=0100
I tried compressiing various large size images there. try it there.

this shouldn't be a 'resizing factor' anyhow, but it should resize images to some arbitrary maximum size.

It doesn't seem like you are suggesting this without knowing what the goal of the issue..? The goal of the issue Greg wrote is to reduce the image file-size down below a certain value (400KB). Sacrificing the quality and the size here is inevitable, at least when using Canvas.toBlob().

According to tests I've done, only changing the quality argument of that canvas API without reducing the image dimension along with it have lead to the outcomes getting blurry in earlier stages. That's why I chose an approach to decrease the quality factor and the dimension at the same time. Plus, that is something this article suggested too.

@corrideat
Copy link
Member

corrideat commented Nov 19, 2024

I've seen this new PR and I have a few general comments.

First off, and this is a subjective point, WebP is a Google-controlled format and I wouldn't use it without fallback for this very reason, favouring formats that are real standards like JPEG, JPEG XL, AVIF or HEIC (although HEIC is patent-encumbered and therefore might not be a good choice) (*). Furthermore, JPEG is more widely supported than WebP. If there's a potential for users to want to save the images, I'd stick to JPEG because anecdotally users aren't familiar with WebP and may not know what to do with them or may not be as widely supported. For example, on Windows JPEG opens by default on Paint and WebP opens by default on Edge.

Secondly, although WebP sometimes (often even) has superior compression than JPEG, I suspect most of the impressive gains here aren't because of WebP but rather because of re-encoding the image. Although I haven't tried this, I strongly suspect that doing the same thing but with JPEG will yield similar results (albeit the JPEG files will be on average slightly larger, slightly meaning about 30% larger, more or less).

Thirdly, I would avoid doing these operations without providing a way to 'opt out' because re-encoding images is a lossy process that may be unwanted. WhatsApp, for example, compresses images to sizes that display well in mobile, but provides a way to opt out by sending the image as a 'file'.

Fourthly, responding to Greg:

Question: what browsers don't support webp?

All major browsers (Chrome, Edge, Firefox, Safari) and their derivatives support WebP by default, although this can be configured.

And if there are browsers that we want to support that don't support webp, wouldn't that mean that we shouldn't use it at all (since some users wouldn't be able to view the images)?

This is a good point. I think the testing for WebP support is misplaced because, while it may be necessary, it can't test the viewing side.

Based on this, I'd suggest the following changes:

  1. Dropping WebP (admittedly, this point has subjective reasons)
  2. Dropping the resizing factor as well in favour of a max size that displays well in the range of devices GI is expected to run. See https://gs.statcounter.com/screen-resolution-stats/ for common screen sizes.
  3. Provide some way to opt out of post processing images when it'd be undesirable, since this reduces quality and destroys metadata.

(*) Of these, JPEG and AVIF are the only realistic choices (other than WebP) in terms of widespread support. AVIF should compress as well as WebP on average, but efficient AVIF encoding is extremely slow from my experience. Moreover, AVIF has the same 'format weirdness' that WebP has, as do all of the newer formats, so in reality it's JPEG or WebP.

@corrideat
Copy link
Member

And a last comment:

The 'long time to load' issue can be addressed by resizing images to different sizes, so that devices with small screens (which are also more likely to have a slower connection) load images faster. However, this would have the effect of requiring more server storage than a one-size-fits-all solution.

@SebinSong
Copy link
Collaborator Author

@taoeffect

Question: what browsers don't support webp?

That idea was based on this can-I-use result:
https://caniuse.com/webp

I saw 96.81% there (Also, I myself has not been used this image format commonly in my life either) and so I went ahead to search to see if the world has a javascript logic to check the browser support for it. and then I ran into this article written by Google: https://developers.google.com/speed/webp/faq#in_your_own_javascript

The same article has Which web browsers natively support WebP? section there too and it appears the support level is quite good though.
So I can remove the logic to check the support if you think it's not necessary.

@SebinSong
Copy link
Collaborator Author

SebinSong commented Nov 19, 2024

@corrideat

First off, and this is a subjective point, WebP is a Google-controlled format and I wouldn't use it without fallback.

This is a subjective opinion. So not taking it unless Greg wants to drop it.

  1. Dropping the resizing factor

Like I explained in a comment above, reducing the quality argument to canvas.toBlob and image dimension at the same time have made the image outcome less blurry, when I tested (via this codepen I created. Actually countless times with various different image formats.)
Also, this is an approach this article suggests too.

Although I haven't tried this, I strongly suspect that doing the same thing but with JPEG will yield similar results.

Try using canvas.toBlob(imageEl, type, quality) with specifying type to either image/jpeg or image/webp and then compare the results. you will see that passing image/webp achieves similar level of file-size compression with higher value of quality argument.

  1. Provide some way to opt out of post processing images when it'd be undesirable.

I think this is something out of scope of the issue #2411 . You can discuss with Greg after this PR about adding this feature to the app and create an issue and work on it yourself.

@corrideat
Copy link
Member

corrideat commented Nov 19, 2024

Like I explained in a comment above, reducing the quality argument to canvas.toBlob and image dimension at the same time have made the image outcome less blurry,

I didn't say not to drop the image size. What I said, however, is resizing the image to a fixed size. The reason for this is that most of the image size savings come from reducing the image resolution regardless of image format, it is more consistent and the image will appear complete. For example, if I upload a 40000px by 30000px image, unless I really intend the image to be displayed in its entirety (see point 3 about opting out), likely it'll display well at 2000px by 1500px in any reasonable screen that people use. This is what chat apps already do.

will see that passing image/webp achieves similar level of file-size compression with higher value of quality argument.

Yes, that is expected. WebP is supposed to compress better (I have some counterexamples where it doesn't), which ultimately depends on a lot of factors, including the specific encoder used. There's a Google study on this in fact,
which found exactly this: https://developers.google.com/speed/webp/docs/webp_study.

@SebinSong
Copy link
Collaborator Author

SebinSong commented Nov 19, 2024

@corrideat
Thanks for clarification. Now I understand what you meant. Then, I feel we would def need to make sure the image's physical size doesn't get too small and too large either. I will go with 1024×768 you suggested first and see what Greg thinks.

@SebinSong SebinSong changed the title #2411 - Compress large images attached in the chat [WIP] #2411 - Compress large images attached in the chat Nov 19, 2024
@SebinSong SebinSong changed the title [WIP] #2411 - Compress large images attached in the chat #2411 - Compress large images attached in the chat Nov 19, 2024
@SebinSong
Copy link
Collaborator Author

SebinSong commented Nov 19, 2024

Updated the PR again here with Ricardo's feedback:

  • Do not reduce the image dimension unless its physical size is too large.

So if the given image's physical size is beyond width 1024px and height 768px, the compressed image's dimension will be reduced to those max values. Otherwise, it doesn't change the dimension at all and only uses quality parameter to canvas.toBlob() for compression. (Just tested a few images after this update and it seems to work well too)

Let me know if we would like to change 1024X768 to something else here.

Comment on lines 86 to 87
// If image's physical size is greater than the max dimension, resize the image to the max dimension.
const imageMaxDimension = { width: 1024, height: 768 }
Copy link
Member

@taoeffect taoeffect Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's double this to 2048 x 1536

Copy link
Member

@corrideat corrideat Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that doubling it quadruples the image size.

2048*1536 / (1<<20) === 3

I.e., an uncompressed image of that size would take up 3 MiB. Of course, this would generally be less with JPEG or WebP compression, but it'll come with some quality loss.

ETA: This comment is meant just as an observation. Also, the uncompressed size is actually 9 MiB, not 3 MiB as originally stated. The reason is that there are three channels (RGB) instead of just one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Adobe, JPEG typically has a 10:1 compression ratio over BMP (BMP is essentially uncompressed):

Nonetheless, JPEG files often achieve compression of 10:1 with no significant decrease in quality. (https://www.adobe.com/creativecloud/file-types/image/comparison/bmp-vs-jpeg.html)

Assuming that WebP is about 30% more efficient and a ratio of 13:1, this is about 0.70 MiB. So, a 2048*1536 will likely have reduced quality to reach the 400 KiB goal.

@SebinSong
Copy link
Collaborator Author

SebinSong commented Dec 2, 2024

@taoeffect
Updated the maximum image dimension to 2048 x 1536 as you requested. For a reminder, it means:

a) if the image file size is > 400kb and physical size is larger than 2048 x 1536 -> compresses the image by reducing the dimension to the max size and using quality factor to canvas.toBlob() method.

b) if image file size is > 400kb but physical size is less than 2048 x 1536 -> compresses the image only using the quality factor to canvas.toBlob() method.


I was worried increasing this max dimension might lead to noticeable low quality of the compression outcome. But so far, I haven't seen any visibly bad quality from the compressed images. So this adjustment seems okay.

@@ -20,6 +20,7 @@ export const CHAT_ATTACHMENT_SUPPORTED_EXTENSIONS = [
// TODO: fetch this value from a server API
export const MEGABYTE = 1 << 20
export const CHAT_ATTACHMENT_SIZE_LIMIT = 30 * MEGABYTE // in byte.
export const IMAGE_ATTACHMENT_MAX_SIZE = 400000 // 400KB
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create a new entry for KILOBYTE just below MEGABYTE?

export const KILOBYTE = 1 << 10

Then:

export const IMAGE_ATTACHMENT_MAX_SIZE = 400 * KILOBYTE

This is a slightly larger number than 400000

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

return blob
} else {
// if the size difference is greater than 100KB, reduce the next compression factors by 10%, otherwise 5%.
const minusFactor = sizeDiff > 100 * 1000 ? 0.1 : 0.05
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this can be edited to 100 * KILOBYTE once that constant is defined.

Copy link
Member

@taoeffect taoeffect left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic work @SebinSong! Two minor changes related to defining KILOBYTE and this should be good to merge 👍

Copy link
Member

@taoeffect taoeffect left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic work @SebinSong! 👏

@taoeffect taoeffect merged commit 3996fa4 into master Dec 2, 2024
4 checks passed
@taoeffect taoeffect deleted the sebin/task/#2411-compress-images-before-uploading branch December 2, 2024 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Compress images before uploading them
3 participants