Skip to content

Commit

Permalink
Merge pull request #254149 from nbraud/noto-emoji
Browse files Browse the repository at this point in the history
  • Loading branch information
mkg20001 authored Sep 12, 2023
2 parents 9a12fb6 + 98d30d8 commit 7163f12
Show file tree
Hide file tree
Showing 13 changed files with 287 additions and 16 deletions.
4 changes: 2 additions & 2 deletions doc/builders/packages/ibus.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ _Note: each language passed to `langs` must be an attribute name in `pkgs.hunspe

## Built-in emoji picker {#sec-ibus-typing-booster-emoji-picker}

The `ibus-engines.typing-booster` package contains a program named `emoji-picker`. To display all emojis correctly, a special font such as `noto-fonts-emoji` is needed:
The `ibus-engines.typing-booster` package contains a program named `emoji-picker`. To display all emojis correctly, a special font such as `noto-fonts-color-emoji` is needed:

On NixOS, it can be installed using the following expression:

```nix
{ pkgs, ... }: {
fonts.packages = with pkgs; [ noto-fonts-emoji ];
fonts.packages = with pkgs; [ noto-fonts-color-emoji ];
}
```
4 changes: 4 additions & 0 deletions nixos/doc/manual/release-notes/rl-2311.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@

- The `hail` NixOS module was removed, as `hail` was unmaintained since 2017.

- Package `noto-fonts-emoji` was renamed to `noto-fonts-color-emoji`;
see [#221181](/~https://github.com/NixOS/nixpkgs/issues/221181).


## Other Notable Changes {#sec-release-23.11-notable-changes}

- The Cinnamon module now enables XDG desktop integration by default. If you are experiencing collisions related to xdg-desktop-portal-gtk you can safely remove `xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];` from your NixOS configuration.
Expand Down
2 changes: 1 addition & 1 deletion nixos/modules/config/fonts/packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ in
gyre-fonts # TrueType substitutes for standard PostScript fonts
liberation_ttf
unifont
noto-fonts-emoji
noto-fonts-color-emoji
]);
};
}
2 changes: 1 addition & 1 deletion nixos/tests/fontconfig-default-fonts.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ./make-test-python.nix ({ lib, ... }:
nodes.machine = { config, pkgs, ... }: {
fonts.enableDefaultPackages = true; # Background fonts
fonts.packages = with pkgs; [
noto-fonts-emoji
noto-fonts-color-emoji
cantarell-fonts
twitter-color-emoji
source-code-pro
Expand Down
2 changes: 1 addition & 1 deletion nixos/tests/noto-fonts.nix
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
noto-fonts
noto-fonts-cjk-sans
noto-fonts-cjk-serif
noto-fonts-emoji
noto-fonts-color-emoji
];
fontconfig.defaultFonts = {
serif = [ "Noto Serif" "Noto Serif CJK SC" ];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
, libdeltachat
, makeDesktopItem
, makeWrapper
, noto-fonts-emoji
, noto-fonts-color-emoji
, pkg-config
, python3
, roboto
Expand Down Expand Up @@ -85,7 +85,7 @@ buildNpmPackage rec {
install -D build/icon.png \
$out/share/icons/hicolor/scalable/apps/deltachat.png
ln -sf ${noto-fonts-emoji}/share/fonts/noto/NotoColorEmoji.ttf \
ln -sf ${noto-fonts-color-emoji}/share/fonts/noto/NotoColorEmoji.ttf \
$out/lib/node_modules/deltachat-desktop/html-dist/fonts/noto/emoji
for font in $out/lib/node_modules/deltachat-desktop/html-dist/fonts/Roboto-*.ttf; do
ln -sf ${roboto}/share/fonts/truetype/$(basename $font) \
Expand Down
48 changes: 46 additions & 2 deletions pkgs/data/fonts/noto-fonts/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ rec {
sha256 = "sha256-y1103SS0qkZMhEL5+7kQZ+OBs5tRaqkqOcs4796Fzhg=";
};

noto-fonts-emoji =
noto-fonts-color-emoji =
let
version = "2.038";
emojiPythonEnv =
Expand Down Expand Up @@ -217,14 +217,58 @@ rec {
'';

meta = with lib; {
description = "Color and Black-and-White emoji fonts";
description = "Color emoji font";
homepage = "/~https://github.com/googlefonts/noto-emoji";
license = with licenses; [ ofl asl20 ];
platforms = platforms.all;
maintainers = with maintainers; [ mathnerd314 sternenseemann ];
};
};

noto-fonts-monochrome-emoji =
# Metadata fetched from
# https://www.googleapis.com/webfonts/v1/webfonts?key=${GOOGLE_FONTS_TOKEN}&family=Noto+Emoji
let metadata = with builtins; head (fromJSON (readFile ./noto-emoji.json)).items;
urlHashes = with builtins; fromJSON (readFile ./noto-emoji.hashes.json);

in
stdenvNoCC.mkDerivation {
pname = "noto-fonts-monochrome-emoji";
version = "${lib.removePrefix "v" metadata.version}.${metadata.lastModified}";
preferLocalBuild = true;

dontUnpack = true;
srcs = let
weightNames = {
"300" = "Light";
regular = "Regular";
"500" = "Medium";
"600" = "SemiBold";
"700" = "Bold";
};
in lib.mapAttrsToList
(variant: url: fetchurl { name = "NotoEmoji-${weightNames.${variant}}.ttf";
hash = urlHashes.${url};
inherit url; } )
metadata.files;

installPhase = ''
for src in $srcs; do
install -D $src $out/share/fonts/noto/$(stripHash $src)
done
'';

meta = with lib; {
description = "Monochrome emoji font";
homepage = "https://fonts.google.com/noto/specimen/Noto+Emoji";
license = [ licenses.ofl ];
maintainers = [ maintainers.nicoo ];

platforms = platforms.all;
sourceProvenance = [ sourceTypes.binaryBytecode ];
};
};

noto-fonts-emoji-blob-bin =
let
pname = "noto-fonts-emoji-blob-bin";
Expand Down
7 changes: 7 additions & 0 deletions pkgs/data/fonts/noto-fonts/noto-emoji.hashes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob_10jwvS-FGJCMY.ttf": "sha256-9ndQqJJzsCkR6KcYRNVW3wXWMxcH+0QzFgQQdCG8vSo=",
"http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-r0jwvS-FGJCMY.ttf": "sha256-AXGLdWebddyJhTKMW/D/6tW8ODcaXrUM96m2hN9wYlg=",
"http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-Z0jwvS-FGJCMY.ttf": "sha256-wzF9kKNMeQTYZ2QUT5pIgauhl2qMpZ2nMLNTeAJuqtQ=",
"http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob911TwvS-FGJCMY.ttf": "sha256-NIelE8X+lKtH6yT3eFPZV7zYUR3Y5GnNobAbf7AckR0=",
"http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob9M1TwvS-FGJCMY.ttf": "sha256-zkJuJ8YlTrUV+28wHIqny3yQvjvZqEPG4WXYmaLcY8A="
}
30 changes: 30 additions & 0 deletions pkgs/data/fonts/noto-fonts/noto-emoji.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"kind": "webfonts#webfontList",
"items": [
{
"family": "Noto Emoji",
"variants": [
"300",
"regular",
"500",
"600",
"700"
],
"subsets": [
"emoji"
],
"version": "v46",
"lastModified": "2023-09-07",
"files": {
"300": "http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob_10jwvS-FGJCMY.ttf",
"regular": "http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-r0jwvS-FGJCMY.ttf",
"500": "http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-Z0jwvS-FGJCMY.ttf",
"600": "http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob911TwvS-FGJCMY.ttf",
"700": "http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob9M1TwvS-FGJCMY.ttf"
},
"category": "sans-serif",
"kind": "webfonts#webfont",
"menu": "http://fonts.gstatic.com/s/notoemoji/v46/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-r0gwuQeU.ttf"
}
]
}
183 changes: 183 additions & 0 deletions pkgs/data/fonts/noto-fonts/noto-emoji.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env nix-shell
#! nix-shell -i "python3 -I" -p python3

from contextlib import contextmanager
from pathlib import Path
from typing import Iterable, Optional
from urllib import request

import hashlib, json


def getMetadata(apiKey: str, family: str = "Noto Emoji"):
'''Fetch the Google Fonts metadata for a given family.
An API key can be obtained by anyone with a Google account (🚮) from
`https://developers.google.com/fonts/docs/developer_api#APIKey`
'''
from urllib.parse import urlencode

with request.urlopen(
"https://www.googleapis.com/webfonts/v1/webfonts?" +
urlencode({ 'key': apiKey, 'family': family })
) as req:
return json.load(req)

def getUrls(metadata) -> Iterable[str]:
'''Fetch all files' URLs from Google Fonts' metadata.
The metadata must obey the API v1 schema, and can be obtained from:
https://www.googleapis.com/webfonts/v1/webfonts?key=${GOOGLE_FONTS_TOKEN}&family=${FAMILY}
'''
return ( url for i in metadata['items'] for _, url in i['files'].items() )


def hashUrl(url: str, *, hash: str = 'sha256'):
'''Compute the hash of the data from HTTP GETing a given `url`.
The `hash` must be an algorithm name `hashlib.new` accepts.
'''
with request.urlopen(url) as req:
return hashlib.new(hash, req.read())


def sriEncode(h) -> str:
'''Encode a hash in the SRI format.
Takes a `hashlib` object, and produces a string that
nixpkgs' `fetchurl` accepts as `hash` parameter.
'''
from base64 import b64encode
return f"{h.name}-{b64encode(h.digest()).decode()}"

def validateSRI(sri: Optional[str]) -> Optional[str]:
'''Decode an SRI hash, return `None` if invalid.
This is not a full SRI hash parser, hash options aren't supported.
'''
from base64 import b64decode

if sri is None:
return None

try:
hashName, b64 = sri.split('-', 1)

h = hashlib.new(hashName)
digest = b64decode(b64, validate=True)
assert len(digest) == h.digest_size

except:
return None
else:
return sri


def hashUrls(
urls: Iterable[str],
knownHashes: dict[str, str] = {},
) -> dict[str, str]:
'''Generate a `dict` mapping URLs to SRI-encoded hashes.
The `knownHashes` optional parameter can be used to avoid
re-downloading files whose URL have not changed.
'''
return {
url: validateSRI(knownHashes.get(url)) or sriEncode(hashUrl(url))
for url in urls
}


@contextmanager
def atomicFileUpdate(target: Path):
'''Atomically replace the contents of a file.
Yields an open file to write into; upon exiting the context,
the file is closed and (atomically) replaces the `target`.
Guarantees that the `target` was either successfully overwritten
with new content and no exception was raised, or the temporary
file was cleaned up.
'''
from tempfile import mkstemp
fd, _p = mkstemp(
dir = target.parent,
prefix = target.name,
)
tmpPath = Path(_p)

try:
with open(fd, 'w') as f:
yield f

tmpPath.replace(target)

except Exception:
tmpPath.unlink(missing_ok = True)
raise


if __name__ == "__main__":
from os import environ
from urllib.error import HTTPError

environVar = 'GOOGLE_FONTS_TOKEN'
currentDir = Path(__file__).parent
metadataPath = currentDir / 'noto-emoji.json'

try:
apiToken = environ[environVar]
metadata = getMetadata(apiToken)

except (KeyError, HTTPError) as exn:
# No API key in the environment, or the query was rejected.
match exn:
case KeyError if exn.args[0] == environVar:
print(f"No '{environVar}' in the environment, "
"skipping metadata update")

case HTTPError if exn.getcode() == 403:
print("Got HTTP 403 (Forbidden)")
if apiToken != '':
print("Your Google API key appears to be valid "
"but does not grant access to the fonts API.")
print("Aborting!")
raise SystemExit(1)

case HTTPError if exn.getcode() == 400:
# Printing the supposed token should be fine, as this is
# what the API returns on invalid tokens.
print(f"Got HTTP 400 (Bad Request), is this really an API token: '{apiToken}' ?")
case _:
# Unknown error, let's bubble it up
raise

# In that case just use the existing metadata
with metadataPath.open() as metadataFile:
metadata = json.load(metadataFile)

lastModified = metadata["items"][0]["lastModified"];
print(f"Using metadata from file, last modified {lastModified}")

else:
# If metadata was successfully fetched, validate and persist it
lastModified = metadata["items"][0]["lastModified"];
print(f"Fetched current metadata, last modified {lastModified}")
with atomicFileUpdate(metadataPath) as metadataFile:
json.dump(metadata, metadataFile, indent = 2)
metadataFile.write("\n") # Pacify nixpkgs' dumb editor config check

hashPath = currentDir / 'noto-emoji.hashes.json'
try:
with hashPath.open() as hashFile:
hashes = json.load(hashFile)
except FileNotFoundError:
hashes = {}

with atomicFileUpdate(hashPath) as hashFile:
json.dump(
hashUrls(getUrls(metadata), knownHashes = hashes),
hashFile,
indent = 2,
)
hashFile.write("\n") # Pacify nixpkgs' dumb editor config check
10 changes: 5 additions & 5 deletions pkgs/data/fonts/twitter-color-emoji/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
, python3
, which
, zopfli
, noto-fonts-emoji
, noto-fonts-color-emoji
}:

let
Expand All @@ -33,15 +33,15 @@ stdenv.mkDerivation rec {
inherit version;

srcs = [
noto-fonts-emoji.src
noto-fonts-color-emoji.src
twemojiSrc
];

sourceRoot = noto-fonts-emoji.src.name;
sourceRoot = noto-fonts-color-emoji.src.name;

postUnpack = ''
chmod -R +w ${twemojiSrc.name}
mv ${twemojiSrc.name} ${noto-fonts-emoji.src.name}
mv ${twemojiSrc.name} ${noto-fonts-color-emoji.src.name}
'';

nativeBuildInputs = [
Expand All @@ -67,7 +67,7 @@ stdenv.mkDerivation rec {
"s#http://scripts.sil.org/OFL#http://creativecommons.org/licenses/by/4.0/#"
];
in ''
${noto-fonts-emoji.postPatch}
${noto-fonts-color-emoji.postPatch}
sed '${templateSubstitutions}' NotoColorEmoji.tmpl.ttx.tmpl > TwitterColorEmoji.tmpl.ttx.tmpl
pushd ${twemojiSrc.name}/assets/72x72/
Expand Down
Loading

0 comments on commit 7163f12

Please sign in to comment.