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

noto-fonts-emoji: Package both color and monochrome variants #254149

Merged
merged 6 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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