emoji: Convert spritesheets to webp.

This provides significant size savings:

| Emoji set   | png size | webp size | webp/png percent |
| ----------- | -------- | --------- | ---------------- |
| google-blob |  1968954 |   1373350 |           69.75% |
| twitter     |  2972820 |   2149672 |           72.31% |
| google      |  3455270 |   2327834 |           67.37% |

Since these are the largest assets that we ship to clients, it is
worth shaving off every byte we can.
This commit is contained in:
Alex Vandiver 2024-08-22 16:08:03 +00:00 committed by Tim Abbott
parent a2517e1115
commit 38053e9c7c
7 changed files with 45 additions and 16 deletions

View File

@ -28,6 +28,7 @@ VENV_DEPENDENCIES = [
"jq", # No longer used in production (clean me up later) "jq", # No longer used in production (clean me up later)
"libsasl2-dev", # For building python-ldap from source "libsasl2-dev", # For building python-ldap from source
"libvips", # For thumbnailing "libvips", # For thumbnailing
"libvips-tools",
] ]
COMMON_YUM_VENV_DEPENDENCIES = [ COMMON_YUM_VENV_DEPENDENCIES = [
@ -46,6 +47,7 @@ COMMON_YUM_VENV_DEPENDENCIES = [
"openssl-devel", "openssl-devel",
"jq", "jq",
"vips", # For thumbnailing "vips", # For thumbnailing
"vips-tools",
] ]
REDHAT_VENV_DEPENDENCIES = [ REDHAT_VENV_DEPENDENCIES = [

View File

@ -4,6 +4,7 @@
# works. # works.
import os import os
import shutil import shutil
import subprocess
import sys import sys
from collections.abc import Iterator, Sequence from collections.abc import Iterator, Sequence
from typing import Any from typing import Any
@ -49,7 +50,7 @@ div.emoji,
span.emoji span.emoji
{{ {{
display: inline-block; display: inline-block;
background-image: url(~emoji-datasource-{emojiset}/img/{alt_name}/sheets-256/64.png); background-image: url(../emoji/{emojiset}.webp);
background-size: {background_size}; background-size: {background_size};
background-repeat: no-repeat; background-repeat: no-repeat;
@ -116,15 +117,16 @@ def main() -> None:
# /srv/zulip-emoji-cache/*/web gets copied to ZULIP_PATH/web/generated # /srv/zulip-emoji-cache/*/web gets copied to ZULIP_PATH/web/generated
# These must not be symlinked so webpack can resolve module references. # These must not be symlinked so webpack can resolve module references.
TARGET_EMOJI_STYLES = os.path.join(ZULIP_PATH, "web", "generated", "emoji-styles") for subdir in ("emoji", "emoji-styles"):
os.makedirs(TARGET_EMOJI_STYLES, exist_ok=True) target_dir = os.path.join(ZULIP_PATH, "web", "generated", subdir)
to_remove = set(os.listdir(TARGET_EMOJI_STYLES)) os.makedirs(target_dir, exist_ok=True)
source_emoji_dump = os.path.join(emoji_cache_path, "web", "emoji-styles") to_remove = set(os.listdir(target_dir))
source_emoji_dump = os.path.join(emoji_cache_path, "web", subdir)
for filename in os.listdir(source_emoji_dump): for filename in os.listdir(source_emoji_dump):
shutil.copy2(os.path.join(source_emoji_dump, filename), TARGET_EMOJI_STYLES) shutil.copy2(os.path.join(source_emoji_dump, filename), target_dir)
to_remove.discard(filename) to_remove.discard(filename)
for filename in to_remove: for filename in to_remove:
os.remove(os.path.join(TARGET_EMOJI_STYLES, filename)) os.remove(os.path.join(target_dir, filename))
def percent(f: float) -> str: def percent(f: float) -> str:
@ -334,6 +336,26 @@ def setup_emoji_farms(cache_path: str, emoji_data: list[dict[str, Any]]) -> None
generate_sprite_css_files(cache_path, emoji_data, emojiset, alt_name, fallback_emoji_data) generate_sprite_css_files(cache_path, emoji_data, emojiset, alt_name, fallback_emoji_data)
print(f"Converting {emojiset} sheet to webp...")
TARGET_EMOJI_SHEETS = os.path.join(cache_path, "web", "emoji")
os.makedirs(TARGET_EMOJI_SHEETS, exist_ok=True)
sheet_src = os.path.join(
NODE_MODULES_PATH,
f"emoji-datasource-{emojiset}",
"img",
alt_name,
"sheets-256",
"64.png",
)
sheet_dst = os.path.join(TARGET_EMOJI_SHEETS, f"{emojiset}.webp")
# From libwebp: [Q is] between 0 and 100. For lossy, 0 gives
# the smallest size and 100 the largest. For lossless, this
# parameter is the amount of effort put into the
# compression: 0 is the fastest but gives larger files
# compared to the slowest, but best, 100.
subprocess.check_call(["vips", "copy", sheet_src, f"{sheet_dst}[lossless=true,Q=100]"])
# Set up standard emoji sets. # Set up standard emoji sets.
for emojiset in ["google", "twitter"]: for emojiset in ["google", "twitter"]:
setup_emoji_farm(emojiset, emoji_data) setup_emoji_farm(emojiset, emoji_data)

1
web/.gitignore vendored
View File

@ -1,4 +1,5 @@
# From emoji # From emoji
/generated/emoji
/generated/emoji-styles /generated/emoji-styles
# From passing pygments data to the frontend # From passing pygments data to the frontend
/generated/pygments_data.json /generated/pygments_data.json

5
web/src/assets.d.ts vendored
View File

@ -13,6 +13,11 @@ declare module "*.png" {
export default url; export default url;
} }
declare module "*.webp" {
const url: string;
export default url;
}
// Declare the style loader for CSS files. This is used in the // Declare the style loader for CSS files. This is used in the
// `import` statements in the `emojisets.ts` file. // `import` statements in the `emojisets.ts` file.
declare module "!style-loader?*" { declare module "!style-loader?*" {

View File

@ -1,8 +1,7 @@
import google_sheet from "emoji-datasource-google/img/google/sheets-256/64.png";
import google_blob_sheet from "emoji-datasource-google-blob/img/google/sheets-256/64.png";
import twitter_sheet from "emoji-datasource-twitter/img/twitter/sheets-256/64.png";
import octopus_url from "../../static/generated/emoji/images-google-64/1f419.png"; import octopus_url from "../../static/generated/emoji/images-google-64/1f419.png";
import google_blob_sheet from "../generated/emoji/google-blob.webp";
import google_sheet from "../generated/emoji/google.webp";
import twitter_sheet from "../generated/emoji/twitter.webp";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";

View File

@ -147,7 +147,7 @@ run_test("paste_handler_converter", () => {
// Emojis // Emojis
input = input =
'<meta http-equiv="content-type" content="text/html; charset=utf-8"><span style="color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">emojis:<span> </span></span><span aria-label="smile" class="emoji emoji-1f642" role="img" title="smile" style="height: 20px; width: 20px; position: relative; margin-top: -7px; vertical-align: middle; top: 3px; background-position: 55% 46.667%; display: inline-block; background-image: url(&quot;http://localhost:9991/webpack/files/srv/zulip-npm-cache/287cb53c1a095fe79651f095d5d8d60f7060baa7/node_modules/emoji-datasource-google/img/google/sheets-256/64.png&quot;); background-size: 6100%; background-repeat: no-repeat; text-indent: 100%; white-space: nowrap; overflow: hidden; color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">:smile:</span><span style="color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"><span> </span></span><span aria-label="family man woman girl" class="emoji emoji-1f468-200d-1f469-200d-1f467" role="img" title="family man woman girl" style="height: 20px; width: 20px; position: relative; margin-top: -7px; vertical-align: middle; top: 3px; background-position: 23.333% 75%; display: inline-block; background-image: url(&quot;http://localhost:9991/webpack/files/srv/zulip-npm-cache/287cb53c1a095fe79651f095d5d8d60f7060baa7/node_modules/emoji-datasource-google/img/google/sheets-256/64.png&quot;); background-size: 6100%; background-repeat: no-repeat; text-indent: 100%; white-space: nowrap; overflow: hidden; color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">:family_man_woman_girl:</span>'; '<meta http-equiv="content-type" content="text/html; charset=utf-8"><span style="color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">emojis:<span> </span></span><span aria-label="smile" class="emoji emoji-1f642" role="img" title="smile" style="height: 20px; width: 20px; position: relative; margin-top: -7px; vertical-align: middle; top: 3px; background-position: 55% 46.667%; display: inline-block; background-image: url(&quot;http://localhost:9991/webpack/files/generated/emoji/google.webp&quot;); background-size: 6100%; background-repeat: no-repeat; text-indent: 100%; white-space: nowrap; overflow: hidden; color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">:smile:</span><span style="color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"><span> </span></span><span aria-label="family man woman girl" class="emoji emoji-1f468-200d-1f469-200d-1f467" role="img" title="family man woman girl" style="height: 20px; width: 20px; position: relative; margin-top: -7px; vertical-align: middle; top: 3px; background-position: 23.333% 75%; display: inline-block; background-image: url(&quot;http://localhost:9991/webpack/files/generated/emoji/google.webp&quot;); background-size: 6100%; background-repeat: no-repeat; text-indent: 100%; white-space: nowrap; overflow: hidden; color: rgb(221, 222, 238); font-family: &quot;Source Sans 3&quot;, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">:family_man_woman_girl:</span>';
assert.equal( assert.equal(
copy_and_paste.paste_handler_converter(input), copy_and_paste.paste_handler_converter(input),
"emojis: :smile: :family_man_woman_girl:", "emojis: :smile: :family_man_woman_girl:",

View File

@ -192,7 +192,7 @@ const config = (
}, },
// load fonts and files // load fonts and files
{ {
test: /\.(eot|jpg|svg|ttf|otf|png|woff2?)$/, test: /\.(eot|jpg|svg|ttf|otf|png|webp|woff2?)$/,
type: "asset/resource", type: "asset/resource",
}, },
], ],