mirror of
https://github.com/cupcakearmy/glyphance.git
synced 2025-09-09 15:40:41 +00:00
inisital commit
This commit is contained in:
77
src/assets/config.schema.yaml
Normal file
77
src/assets/config.schema.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
$schema: https://json-schema.org/draft/2020-12/schema
|
||||
$id: https://github.com/cupcakearmy/glyphance
|
||||
title: Config
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
fonts:
|
||||
$ref: "#/$defs/fonts"
|
||||
output:
|
||||
$ref: "#/$defs/output"
|
||||
context:
|
||||
type: string
|
||||
|
||||
$defs:
|
||||
fonts:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
patternProperties:
|
||||
"^[a-zA-z \\-_]+$":
|
||||
$ref: "#/$defs/font"
|
||||
|
||||
font:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/font-variation"
|
||||
|
||||
css:
|
||||
# https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face#descriptors
|
||||
type: object
|
||||
additionalProperties: false
|
||||
patternProperties:
|
||||
? "^ascent-override|descent-override|font-display|font-family|font-stretch|font-style|font-weight|font-feature-settings|font-variation-settings|line-gap-override|size-adjust|src|unicode-range$"
|
||||
: type: string
|
||||
|
||||
font-variation:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
requiredProperties:
|
||||
- file
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
variable:
|
||||
type: boolean
|
||||
css:
|
||||
$ref: "#/$defs/css"
|
||||
range:
|
||||
type: string
|
||||
patter: ^U\+[\da-zA-z]{4}(-[\da-zA-z]{4})?$
|
||||
|
||||
output:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
dir:
|
||||
type: string
|
||||
prefix:
|
||||
type: string
|
||||
css:
|
||||
$ref: "#/$defs/css"
|
||||
clean:
|
||||
type: boolean
|
||||
formats:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- woff2
|
||||
- woff
|
||||
ranges:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
patternProperties:
|
||||
"^[a-zA-z \\-_]+$":
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/$defs/range"
|
7
src/assets/css.template
Normal file
7
src/assets/css.template
Normal file
@@ -0,0 +1,7 @@
|
||||
/* $range */
|
||||
@font-face {
|
||||
font-family: '$font';
|
||||
src: $src;
|
||||
unicode-range: $unicodes;
|
||||
$additional
|
||||
}
|
1
src/assets/ranges.json
Normal file
1
src/assets/ranges.json
Normal file
@@ -0,0 +1 @@
|
||||
{" cyrillic-ext": ["U+0460-052F", "U+1C80-1C88", "U+20B4", "U+2DE0-2DFF", "U+A640-A69F", "U+FE2E-FE2F"], " cyrillic": ["U+0301", "U+0400-045F", "U+0490-0491", "U+04B0-04B1", "U+2116"], " greek-ext": ["U+1F00-1FFF"], " greek": ["U+0370-03FF"], " vietnamese": ["U+0102-0103", "U+0110-0111", "U+0128-0129", "U+0168-0169", "U+01A0-01A1", "U+01AF-01B0", "U+1EA0-1EF9", "U+20AB"], " latin-ext": ["U+0100-024F", "U+0259", "U+1E00-1EFF", "U+2020", "U+20A0-20AB", "U+20AD-20CF", "U+2113", "U+2C60-2C7F", "U+A720-A7FF"], " latin": ["U+0000-00FF", "U+0131", "U+0152-0153", "U+02BB-02BC", "U+02C6", "U+02DA", "U+02DC", "U+2000-206F", "U+2074", "U+20AC", "U+2122", "U+2191", "U+2193", "U+2212", "U+2215", "U+FEFF", "U+FFFD"]}
|
77
src/cmd_optimise.py
Normal file
77
src/cmd_optimise.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import string
|
||||
import subprocess
|
||||
|
||||
import utils
|
||||
|
||||
with open(utils.asset_path('ranges.json')) as f:
|
||||
default_ranges = json.load(f)
|
||||
|
||||
default_variation = {
|
||||
'variable': False,
|
||||
'css': {}
|
||||
}
|
||||
|
||||
|
||||
def optimise(config):
|
||||
# Check if destination is fine
|
||||
destination = os.path.abspath(config['output']['dir'])
|
||||
try:
|
||||
if not os.path.isdir(destination):
|
||||
os.makedirs(destination, exist_ok=True)
|
||||
if not os.access(destination, os.W_OK):
|
||||
raise Exception()
|
||||
except:
|
||||
print(f'Output directory not writable: "{destination}"')
|
||||
exit(1)
|
||||
|
||||
# Clean
|
||||
if config['output']['clean']:
|
||||
print(f'Cleaning: "{destination}"')
|
||||
shutil.rmtree(destination)
|
||||
os.makedirs(destination)
|
||||
|
||||
# Go over each font and variation of it.
|
||||
# Then go over every range and format and generate the subset
|
||||
# Finally add the relevant CSS
|
||||
|
||||
css = ''
|
||||
with open(utils.asset_path('css.template')) as f:
|
||||
template = string.Template(f.read())
|
||||
for font, variations in config['fonts'].items():
|
||||
css += f'\n\n/* {font} */\n'
|
||||
for variation in variations:
|
||||
variation = utils.update_deep(default_variation, variation)
|
||||
print(f"Processing: {font} {os.path.basename(variation['file'])}")
|
||||
source = os.path.join(config['context'], variation['file'])
|
||||
for format in config['output']['formats']:
|
||||
for range, codes in default_ranges.items():
|
||||
unicodes = ', '.join(codes)
|
||||
|
||||
# Create unique key on all parameters
|
||||
key = font+variation['file']+format+unicodes
|
||||
key = hashlib.sha1(key.encode()).hexdigest()
|
||||
|
||||
# Generate subset
|
||||
output_file = os.path.join(destination, f'{key}.{format}')
|
||||
print(f" {range}@{format} -> {output_file}")
|
||||
command = f'pyftsubset --unicodes="{unicodes}" --layout-features="*" --flavor="{format}" --output-file="{output_file}" {source}'
|
||||
subprocess.call(command, shell=True)
|
||||
|
||||
# Generate CSS
|
||||
ending = format
|
||||
if variation['variable']:
|
||||
ending += '-variations'
|
||||
|
||||
src = f"url({config['output']['prefix']}{os.path.basename(output_file)}) format('{ending}')"
|
||||
merged = utils.update_deep(config['output']['css'], variation['css'])
|
||||
additional = '\n '.join([f"{key}: {value};" for key, value in merged.items()])
|
||||
css += template.substitute(range=range, font=font, src=src,
|
||||
unicodes=unicodes, additional=additional)
|
||||
|
||||
with open(os.path.join(destination, 'fonts.css'), 'w') as f:
|
||||
f.write(css.strip())
|
48
src/config.py
Normal file
48
src/config.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
|
||||
import jsonschema
|
||||
import yaml
|
||||
|
||||
import flags
|
||||
import utils
|
||||
|
||||
default_config = {
|
||||
'output': {
|
||||
'dir': 'generated',
|
||||
'formats': ['woff2'],
|
||||
'prefix': '/',
|
||||
'css': {
|
||||
'font-display': 'swap',
|
||||
'font-style': 'normal',
|
||||
'font-weight': '400',
|
||||
},
|
||||
'clean': False,
|
||||
},
|
||||
"context": ".",
|
||||
}
|
||||
|
||||
|
||||
def validate(config):
|
||||
schema_file = utils.asset_path('config.schema.yaml')
|
||||
with open(schema_file, 'r') as f:
|
||||
schema = yaml.safe_load(f)
|
||||
return jsonschema.validate(config, schema)
|
||||
|
||||
|
||||
def load(path):
|
||||
# Load config
|
||||
path = os.path.abspath(path)
|
||||
with open(path, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Setting dynamic defaults
|
||||
default_config['context'] = os.path.dirname(path)
|
||||
if flags.OUTPUT_DIRECTORY != None:
|
||||
default_config['output']['dir'] = flags.OUTPUT_DIRECTORY
|
||||
if flags.CLEAN != None:
|
||||
default_config['output']['clean'] = flags.CLEAN
|
||||
if flags.PREFIX != None:
|
||||
default_config['output']['prefix'] = flags.PREFIX
|
||||
|
||||
# Merge defaults
|
||||
return utils.update_deep(default_config, config)
|
4
src/flags.py
Normal file
4
src/flags.py
Normal file
@@ -0,0 +1,4 @@
|
||||
VERBOSE = False
|
||||
OUTPUT_DIRECTORY = None
|
||||
CLEAN = None
|
||||
PREFIX = None
|
26
src/gen_ranges.py
Normal file
26
src/gen_ranges.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
output = './src/json/ranges.json'
|
||||
|
||||
|
||||
guide = requests.get('https://fonts.googleapis.com/css2?family=Roboto&display=swap',
|
||||
headers={'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0'})
|
||||
|
||||
block_height = 9
|
||||
lines = guide.text.strip().split('\n')
|
||||
ranges = {}
|
||||
for i in range(len(lines)//9):
|
||||
offset = i * block_height
|
||||
variation = lines[offset: offset+block_height]
|
||||
name = variation[0][2:-3]
|
||||
codes = variation[7].replace('unicode-range:', '').replace(';', '').split(',')
|
||||
codes = [code.strip() for code in codes]
|
||||
ranges[name] = codes
|
||||
|
||||
output = os.path.abspath(output)
|
||||
os.makedirs(os.path.dirname(output), exist_ok=True)
|
||||
with open('./src/json/ranges.json', 'w') as f:
|
||||
json.dump(ranges, f)
|
32
src/main.py
Normal file
32
src/main.py
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import click
|
||||
|
||||
import cmd_optimise
|
||||
import flags
|
||||
from config import load, validate
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.version_option("0.1.0")
|
||||
@click.option('-v', '--verbose', is_flag=True, default=False, help="Run in verbose mode.")
|
||||
@click.option('-c', '--config', type=click.Path(), required=True, help="Path to the config file.")
|
||||
@click.option('-o', '--output-directory', type=click.Path(), help="Path to the output directory.")
|
||||
@click.option('--clean', is_flag=True, help="Clean the output directory before generating.")
|
||||
@click.option('--prefix', help="Prefix for the generated css font URLs.")
|
||||
def cli(verbose, config, output_directory, clean, prefix):
|
||||
# Flags
|
||||
flags.VERBOSE = verbose
|
||||
if flags.VERBOSE:
|
||||
click.echo("Running in verbose mode.")
|
||||
flags.OUTPUT_DIRECTORY = output_directory
|
||||
flags.CLEAN = clean
|
||||
flags.PREFIX = prefix
|
||||
|
||||
# Run
|
||||
c = load(config)
|
||||
validate(c)
|
||||
cmd_optimise.optimise(c)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli(auto_envvar_prefix="GLYPHANCE")
|
23
src/utils.py
Normal file
23
src/utils.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import collections.abc
|
||||
import copy
|
||||
import os
|
||||
|
||||
|
||||
def update_deep(d, u, first=True):
|
||||
"""
|
||||
Deep update
|
||||
https://stackoverflow.com/a/3233356/2425183
|
||||
"""
|
||||
if first:
|
||||
d = copy.deepcopy(d)
|
||||
u = copy.deepcopy(u)
|
||||
for k, v in u.items():
|
||||
if isinstance(v, collections.abc.Mapping):
|
||||
d[k] = update_deep(d.get(k, {}), v, False)
|
||||
else:
|
||||
d[k] = v
|
||||
return d
|
||||
|
||||
|
||||
def asset_path(name: str) -> str:
|
||||
return os.path.join(os.path.dirname(__file__), 'assets', name)
|
Reference in New Issue
Block a user