Repository: safe-global/safe-config-service

Issue:
Token logos are too large/heavy
**Describe the bug**
I did a spot check on different coin icons we are receiving from the gateway.

https://safe-transaction-assets.gnosis-safe.io/chains/4/currency_logo.png – 3258x3258 px, 161 Kb
https://safe-transaction-assets.gnosis-safe.io/chains/43114/currency_logo.png – 1500x1500, 41 Kb

I suppose many other icons are also unnecessarily big. This is very bad for the page loading speed.

## Frontend requirements
* Token logos should be sufficiently big, so that we can downscale them to different sizes in different components.
* But not too big. 512x512 would be good enough for most situations, including hi-res (aka Retina) screens.
* Images should be optimized, e.g. via OptiPng. This will already halve the current sizes.
 
Try it on optipng.com and there's a CLI util of the same name.

## To summarize
Please do the following:
* Resize all the existing icons to 512x512 px
* If the icon is smaller, keep the current size
* Run them through optipng
* Set up a pipeline to do the same for new icons

## Automated pipeline
Whenever we upload an image, we should programmatically perform the same actions as requested above for existing images.
Namely:
* resize it to 512 px on the largest side (e.g. 512x512, or 300x512, or 512x400 etc)
* if the original is smaller, keep the original size
* optimize using OptiPng

## Why OptiPng
OptiPng is a de-facto standard CLI tool to remove unnecessary kilobytes from PNGs and JPGs.
In most cases, it reduces the size of a file by 100% (i.e. halves the size).
The resource cost is negligible as this will be happening on the upload of a single image.


Discussion:
I will take care of resizing the logos @fmrsabino 
@katspaugh Logos were updated to optimized png `512x512` when possible, both in staging and mainnet. I'm moving the issue to the `config-service` for further analysis of `Set up a pipeline to do the same for new icons`

Resolving patch:
diff --git a/src/chains/migrations/0035_alter_chain_currency_logo_uri.py b/src/chains/migrations/0035_alter_chain_currency_logo_uri.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/0035_alter_chain_currency_logo_uri.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.10 on 2021-12-24 12:26
+
+from django.db import migrations, models
+
+import chains.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("chains", "0034_add_public_rpc_url"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="chain",
+            name="currency_logo_uri",
+            field=models.ImageField(
+                max_length=255,
+                upload_to=chains.models.native_currency_path,
+                validators=[chains.models.validate_native_currency_size],
+            ),
+        ),
+    ]
diff --git a/src/chains/models.py b/src/chains/models.py
--- a/src/chains/models.py
+++ b/src/chains/models.py
@@ -1,7 +1,9 @@
 import os
 import re
+from typing import IO
 
 from django.core.exceptions import ValidationError
+from django.core.files.images import get_image_dimensions
 from django.core.validators import RegexValidator
 from django.db import models
 from django.db.models import QuerySet
@@ -24,6 +26,12 @@ def native_currency_path(instance: "Chain", filename: str) -> str:
     return f"chains/{instance.id}/currency_logo{file_extension}"
 
 
+def validate_native_currency_size(image: str | IO[bytes]) -> None:
+    image_width, image_height = get_image_dimensions(image)
+    if image_width > 512 or image_height > 512:
+        raise ValidationError("Image width and height need to be at most 512 pixels")
+
+
 class Chain(models.Model):
     class RpcAuthentication(models.TextChoices):
         API_KEY_PATH = "API_KEY_PATH"
@@ -62,7 +70,9 @@ class RpcAuthentication(models.TextChoices):
     currency_symbol = models.CharField(max_length=255)
     currency_decimals = models.IntegerField(default=18)
     currency_logo_uri = models.ImageField(
-        upload_to=native_currency_path, max_length=255
+        validators=[validate_native_currency_size],
+        upload_to=native_currency_path,
+        max_length=255,
     )
     transaction_service_uri = models.URLField()
     vpc_transaction_service_uri = models.URLField()


Test patch:
diff --git a/src/chains/tests/factories.py b/src/chains/tests/factories.py
--- a/src/chains/tests/factories.py
+++ b/src/chains/tests/factories.py
@@ -35,7 +35,7 @@ class Meta:
     currency_name = factory.Faker("cryptocurrency_name")
     currency_symbol = factory.Faker("cryptocurrency_code")
     currency_decimals = factory.Faker("pyint")
-    currency_logo_uri = factory.django.ImageField()
+    currency_logo_uri = factory.django.ImageField(width=50, height=50)
     transaction_service_uri = factory.Faker("url")
     vpc_transaction_service_uri = factory.Faker("url")
     theme_text_color = factory.Faker("hex_color")
diff --git a/src/chains/tests/test_views.py b/src/chains/tests/test_views.py
--- a/src/chains/tests/test_views.py
+++ b/src/chains/tests/test_views.py
@@ -1,6 +1,8 @@
 from decimal import Decimal
 from typing import Any
 
+import factory
+from django.core.exceptions import ValidationError
 from django.urls import reverse
 from faker import Faker
 from rest_framework.test import APITestCase
@@ -259,6 +261,29 @@ def test_null_ens_registry_address(self) -> None:
         self.assertEqual(response.json()["ensRegistryAddress"], None)
 
 
+class ChainCurrencyLogoTests(APITestCase):
+    def test_image_max_size_validation(self) -> None:
+        chain = ChainFactory.create(
+            currency_logo_uri=factory.django.ImageField(width=512, height=512)
+        )
+
+        chain.full_clean()  # should not rise any exception
+
+    def test_image_width_greater_than_512(self) -> None:
+        with self.assertRaises(ValidationError):
+            chain = ChainFactory.create(
+                currency_logo_uri=factory.django.ImageField(width=513, height=50)
+            )
+            chain.full_clean()
+
+    def test_image_height_greater_than_512(self) -> None:
+        with self.assertRaises(ValidationError):
+            chain = ChainFactory.create(
+                currency_logo_uri=factory.django.ImageField(width=50, height=513)
+            )
+            chain.full_clean()
+
+
 class ChainGasPriceTests(APITestCase):
     faker = Faker()