Repository: oppia/oppia

Issue:
Serve our images in WebP format
Replace our static images with WebP format. 

https://web.dev/serve-images-webp/

Steps:

1. Use `cwebp` to transform our PNG images in the `assets/images` folder into the WebP format (https://web.dev/serve-images-webp/#use-cwebp)
2. Replace the `<img>` tag used to load the image with `<picture>` and set both the original PNG image and new WeB image as sources there (https://web.dev/serve-images-webp/#serve-webp-images)

So for example:
```html
<img ng-src="<[$ctrl.getStaticImageUrl('/general/creator-feedback.png')]>" alt="">
```
will be replaced by:
```html
<picture>
  <source type="image/webp" ng-srcset="<[$ctrl.getStaticImageUrl('/general/creator-feedback.webp')]>">
  <source type="image/jpeg" ng-srcset="<[$ctrl.getStaticImageUrl('/general/creator-feedback.png')]>">
  <img ng-src="<[$ctrl.getStaticImageUrl('/general/creator-feedback.png')]>" alt="">
</picture>
```
3. In `scripts/build.py` add `'*.webp'` into the `FILEPATHS_PROVIDED_TO_FRONTEND`.
Serve our images in WebP format
Replace our static images with WebP format. 

https://web.dev/serve-images-webp/

Steps:

1. Use `cwebp` to transform our PNG images in the `assets/images` folder into the WebP format (https://web.dev/serve-images-webp/#use-cwebp)
2. Replace the `<img>` tag used to load the image with `<picture>` and set both the original PNG image and new WeB image as sources there (https://web.dev/serve-images-webp/#serve-webp-images)

So for example:
```html
<img ng-src="<[$ctrl.getStaticImageUrl('/general/creator-feedback.png')]>" alt="">
```
will be replaced by:
```html
<picture>
  <source type="image/webp" ng-srcset="<[$ctrl.getStaticImageUrl('/general/creator-feedback.webp')]>">
  <source type="image/jpeg" ng-srcset="<[$ctrl.getStaticImageUrl('/general/creator-feedback.png')]>">
  <img ng-src="<[$ctrl.getStaticImageUrl('/general/creator-feedback.png')]>" alt="">
</picture>
```
3. In `scripts/build.py` add `'*.webp'` into the `FILEPATHS_PROVIDED_TO_FRONTEND`.


Discussion:
@vojtechjelinek I'd like to add some something here:
1. The webP format is really better for images but they are not supported by Safari. So we can use can use browserCheckerService and serve the jpg/png images if the browser is Safari. 
@Showtim3 I plan to use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture to handle these cases and provide also the older version.
From previous discussion with @BenHenning, I believe we want to try and use SVG for images where possible since they scale better at different zoom levels. Would this be OK instead of WebP?

@seanlip I mean I'm okay with using SVG, some of our graphics are probably already png files generated from SVG and SVG files can be even smaller, could you tag someone who can prepare these into our assets folder?
Let me check with @BenHenning. In the meantime, could you please specify a full list of images that you would like SVGs for -- is it everything in assets/images?

(Also, just to check, are we actually using all of the images there? It takes time to prepare SVGs, so I wanted to ask -- if we could get rid of unused ones, that would help.)

Thanks!
@seanlip The most important files to transform to SVG are these in `assets/images/landing`, but in general all the files in `assets/images` that are easy to be transformed to SVG.

I've gone through all the images and they are all used. 
As mentioned in meeting, this doesn't seem directly related to our technical guidelines we want to enforce per https://docs.google.com/document/d/1J0Mvs1pe6ZQdHKAWgChb4AEXdqB_DovQevf-iXbu0Bc/edit. I think it's generally positive for us to move toward webp, but long-term we should follow the guidelines above per using SVGs and multiple tiers of WEBP images.
OK, sounds good, thanks @BenHenning.  

So yep -- @vojtechjelinek, I think it's fine to go ahead with this issue as stated! (It will take longer to get the SVGs, so we might as well do what we can in the meantime.)
@vojtechjelinek if no one is working on this issue can I work on this?
@darksun27 I have already assigned you to #8518 
@darksun27 I saw that you created PR for #8518, I will assign you this one so you have something to work no.
@vojtechjelinek can you please see if these are the only files which contain static images or I am missing out on something?
[https://pastebin.com/DR4VgpHU](https://pastebin.com/DR4VgpHU)
@darksun27 I'm not sure, try greping the codebase to see if you found everything, some images might also be included through TypeScript file.
@vojtechjelinek I greped this result searching for '<img ng-src="', ig these will only give me html files.
Will look for typescript files also.
Look into url-interpolation.service.ts there are methods for generating the image URLs so you might want to find the usages.
Sure! Thanks.
In the TypeScript files do I replace the path to webp images or something else?
Yes, replace it to the webp path.
I found some reference to the png images in the webpack bundle files. Do I need to change those?
No, these are generated from our regular TS files.
@vojtechjelinek I'd like to add some something here:
1. The webP format is really better for images but they are not supported by Safari. So we can use can use browserCheckerService and serve the jpg/png images if the browser is Safari. 
@Showtim3 I plan to use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture to handle these cases and provide also the older version.
From previous discussion with @BenHenning, I believe we want to try and use SVG for images where possible since they scale better at different zoom levels. Would this be OK instead of WebP?

@seanlip I mean I'm okay with using SVG, some of our graphics are probably already png files generated from SVG and SVG files can be even smaller, could you tag someone who can prepare these into our assets folder?
Let me check with @BenHenning. In the meantime, could you please specify a full list of images that you would like SVGs for -- is it everything in assets/images?

(Also, just to check, are we actually using all of the images there? It takes time to prepare SVGs, so I wanted to ask -- if we could get rid of unused ones, that would help.)

Thanks!
@seanlip The most important files to transform to SVG are these in `assets/images/landing`, but in general all the files in `assets/images` that are easy to be transformed to SVG.

I've gone through all the images and they are all used. 
As mentioned in meeting, this doesn't seem directly related to our technical guidelines we want to enforce per https://docs.google.com/document/d/1J0Mvs1pe6ZQdHKAWgChb4AEXdqB_DovQevf-iXbu0Bc/edit. I think it's generally positive for us to move toward webp, but long-term we should follow the guidelines above per using SVGs and multiple tiers of WEBP images.
OK, sounds good, thanks @BenHenning.  

So yep -- @vojtechjelinek, I think it's fine to go ahead with this issue as stated! (It will take longer to get the SVGs, so we might as well do what we can in the meantime.)
@vojtechjelinek if no one is working on this issue can I work on this?
@darksun27 I have already assigned you to #8518 
@darksun27 I saw that you created PR for #8518, I will assign you this one so you have something to work no.
@vojtechjelinek can you please see if these are the only files which contain static images or I am missing out on something?
[https://pastebin.com/DR4VgpHU](https://pastebin.com/DR4VgpHU)
@darksun27 I'm not sure, try greping the codebase to see if you found everything, some images might also be included through TypeScript file.
@vojtechjelinek I greped this result searching for '<img ng-src="', ig these will only give me html files.
Will look for typescript files also.
Look into url-interpolation.service.ts there are methods for generating the image URLs so you might want to find the usages.
Sure! Thanks.
In the TypeScript files do I replace the path to webp images or something else?
Yes, replace it to the webp path.
I found some reference to the png images in the webpack bundle files. Do I need to change those?
No, these are generated from our regular TS files.

Resolving patch:
diff --git a/scripts/build.py b/scripts/build.py
--- a/scripts/build.py
+++ b/scripts/build.py
@@ -125,7 +125,7 @@
 # JS hashes object.
 FILEPATHS_PROVIDED_TO_FRONTEND = (
     'images/*', 'videos/*', 'i18n/*', '*_directive.html', '*.directive.html',
-    '*.template.html', '*.png', '*.json')
+    '*.template.html', '*.png', '*.json', '*.webp')
 
 HASH_BLOCK_SIZE = 2**20
 
diff --git a/scripts/linters/general_purpose_linter.py b/scripts/linters/general_purpose_linter.py
--- a/scripts/linters/general_purpose_linter.py
+++ b/scripts/linters/general_purpose_linter.py
@@ -31,8 +31,8 @@
 
 EXCLUDED_PATHS = (
     'third_party/*', 'build/*', '.git/*', '*.pyc', 'CHANGELOG',
-    'integrations/*', 'integrations_dev/*', '*.svg', '*.gif',
-    '*.png', '*.zip', '*.ico', '*.jpg', '*.min.js', 'backend_prod_files/*',
+    'integrations/*', 'integrations_dev/*', '*.svg', '*.gif', '*.png',
+    '*.webp', '*.zip', '*.ico', '*.jpg', '*.min.js', 'backend_prod_files/*',
     'assets/scripts/*', 'core/tests/data/*', 'core/tests/build_sources/*',
     '*.mp3', '*.mp4', 'node_modules/*', 'typings/*', 'local_compiled_js/*',
     'webpack_bundles/*', 'core/tests/services_sources/*',


Test patch:
diff --git a/core/controllers/learner_dashboard_test.py b/core/controllers/learner_dashboard_test.py
--- a/core/controllers/learner_dashboard_test.py
+++ b/core/controllers/learner_dashboard_test.py
@@ -414,9 +414,8 @@ def test_get_suggestions_after_updating_suggestion_summary(self):
 
         self.assertEqual(
             messages_summary['author_username'], self.EDITOR_USERNAME)
-        self.assertTrue(
-            messages_summary['author_picture_data_url'].startswith(
-                'data:image/png;'))
+        self.assertTrue(test_utils.check_image_png_or_webp(
+            messages_summary['author_picture_data_url']))
         self.assertFalse(messages_summary.get('suggestion_html'))
         self.assertFalse(messages_summary.get('current_content_html'))
         self.assertFalse(messages_summary.get('description'))
@@ -448,9 +447,8 @@ def test_get_suggestions_after_updating_suggestion_summary(self):
 
         self.assertEqual(
             messages_summary['author_username'], self.EDITOR_USERNAME)
-        self.assertTrue(
-            messages_summary['author_picture_data_url'].startswith(
-                'data:image/png;'))
+        self.assertTrue(test_utils.check_image_png_or_webp(
+            messages_summary['author_picture_data_url']))
         self.assertEqual(
             utils.get_time_in_millisecs(first_suggestion.created_on),
             messages_summary['created_on_msecs'])
diff --git a/core/controllers/profile_test.py b/core/controllers/profile_test.py
--- a/core/controllers/profile_test.py
+++ b/core/controllers/profile_test.py
@@ -289,9 +289,8 @@ def test_can_update_profile_picture_data_url(self):
         self.login(self.OWNER_EMAIL)
         csrf_token = self.get_new_csrf_token()
         user_settings = user_services.get_user_settings(self.owner_id)
-        self.assertTrue(
-            user_settings.profile_picture_data_url.startswith(
-                'data:image/png;'))
+        self.assertTrue(test_utils.check_image_png_or_webp(
+            user_settings.profile_picture_data_url))
         self.put_json(
             feconf.PREFERENCES_DATA_URL,
             payload={'update_type': 'profile_picture_data_url',
diff --git a/core/domain/user_services_test.py b/core/domain/user_services_test.py
--- a/core/domain/user_services_test.py
+++ b/core/domain/user_services_test.py
@@ -66,7 +66,6 @@ def _get_continuous_computation_class(cls):
 
 class UserServicesUnitTests(test_utils.GenericTestBase):
     """Test the user services methods."""
-
     def test_set_and_get_username(self):
         gae_id = 'someUser'
         username = 'username'
diff --git a/core/templates/tests/console_errors.html b/core/templates/tests/console_errors.html
--- a/core/templates/tests/console_errors.html
+++ b/core/templates/tests/console_errors.html
@@ -9,7 +9,11 @@
       <base-content>
         <content>
           <div>
-            <img src="/build/fail/logo/288x128_logo_white.png">
+            <picture>
+              <source type="image/webp" srcset="/build/fail/logo/288x128_logo_white.webp">
+              <source type="image/png" srcset="/build/fail/logo/288x128_logo_white.png">
+              <img src="/build/fail/logo/288x128_logo_white.png">
+            </picture>
           </div>
         </content>
         <page-footer></page-footer>
diff --git a/core/tests/protractor/accessibility.js b/core/tests/protractor/accessibility.js
--- a/core/tests/protractor/accessibility.js
+++ b/core/tests/protractor/accessibility.js
@@ -51,7 +51,8 @@ describe('Cache Slugs', function() {
     async function() {
       await browser.get('/console_errors');
       var expectedErrors = [
-        'http://localhost:9001/build/fail/logo/288x128_logo_white.png'
+        'http://localhost:9001/build/fail/logo/288x128_logo_white.png',
+        'http://localhost:9001/build/fail/logo/288x128_logo_white.webp'
       ];
       await general.checkForConsoleErrors(expectedErrors);
     });
diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py
--- a/core/tests/test_utils.py
+++ b/core/tests/test_utils.py
@@ -146,6 +146,21 @@ def mock_load_template(filename):
     return file_content
 
 
+def check_image_png_or_webp(image_string):
+    """Checks if the image is in png or webp format only.
+
+    Args:
+        image_string: str. image url in base64 format.
+
+    Returns:
+        boolean. Returns true if image is in WebP format.
+    """
+    if (image_string.startswith('data:image/png') or
+            image_string.startswith('data:image/webp')):
+        return True
+    return False
+
+
 class URLFetchServiceMock(apiproxy_stub.APIProxyStub):
     """Mock for google.appengine.api.urlfetch."""
 
diff --git a/core/tests/test_utils_test.py b/core/tests/test_utils_test.py
--- a/core/tests/test_utils_test.py
+++ b/core/tests/test_utils_test.py
@@ -292,7 +292,7 @@ def test_fetch_gravatar_with_headers(self):
         user_email = 'user@example.com'
         expected_gravatar_filepath = os.path.join(
             self.get_static_asset_filepath(), 'assets', 'images', 'avatar',
-            'gravatar_example.png')
+            'gravatar_example.webp')
         with python_utils.open_file(
             expected_gravatar_filepath, 'rb', encoding=None) as f:
             gravatar = f.read()