#! /usr/bin/env python3 #################################################### # Generate a fakenewcomputermodern.sty package and # # associated font files that work with different # # TeX engines. # # # # Author: Scott Pakin # #################################################### import dateutil.parser import os import re import subprocess import sys import tempfile from pathlib import Path # Specify the set of food-allergy symbols. allergy_symbols = [ ('E033', 'crustaceans'), ('E034', 'eggs'), ('E035', 'gluten'), ('E036', 'fish'), ('E037', 'lupin'), ('E038', 'milk'), ('E039', 'mollusks'), ('E03A', 'mustard'), ('E03B', 'peanuts'), ('E03C', 'sesame'), ('E03D', 'soy'), ('E03E', 'nuts'), ('E03F', 'celery'), ('E040', 'SOO'), ] # Specify the subset of math symbols we want to present. math_symbols = [ ('E032', 'nleftleftarrows'), ('E033', 'nrightrightarrows'), ('E034', 'twoheadhookrightarrow'), ('E035', 'twoheadhookleftarrow'), ('E036', 'tconvolution'), ('E037', 'dconvolution'), ] def kpsewhich(fname, must_exist=True, format=None): 'Find a filename in the TeX tree.' cmd = ['kpsewhich'] if format is not None: cmd.append(f'--format={format}') cmd.append(fname) proc = subprocess.run(cmd, capture_output=True, check=must_exist, encoding='utf-8') if proc.returncode != 0: return None return proc.stdout.strip() def header_string(c): 'Return a "generated file" header string using a comment character.' full_name = os.path.abspath(sys.argv[0]) gen_line = 'This is a generated file. DO NOT EDIT.' edit_line = f'Edit {full_name} instead.' max_chars = max(len(gen_line), len(edit_line)) comment_line = c * (max_chars + 4) return '\n'.join([ comment_line, '%s %-*.*s %s' % (c, max_chars, max_chars, gen_line, c), '%s %-*.*s %s' % (c, max_chars, max_chars, edit_line, c), comment_line, '', ]) def get_sans_symbols(): '''Return a list of (hex, name) tuples of symbols to declare with newcomputermodern's sans-serif variant.''' # Parse Aegean numbers from fspdefault.tex. aegean_symbols = [] aegean_re = re.compile(r'\\newcommand\*\{\\(aegean[a-z]+)\}\{\\textsf\{\\char"([0-9A-F]+)\}\}') fsp = kpsewhich('fspdefault.tex') with open(fsp) as r: for ln in r: match = aegean_re.match(ln) if match is None: continue aegean_symbols.append((match[2], match[1])) # Return all sans-serif symbols we care about. return allergy_symbols + aegean_symbols def get_braille_symbols(): 'Return a list of (hex, name) tuples of braille symbols,' # Read Unicode names from unicode.txt. braille = [] dig2let = str.maketrans('123456', 'ABCDEF') with open('unicode.txt') as r: for ln in r: # Find 6-bit braille patterns. fields = ln.split(None, 1) if len(fields) != 2: continue code = int(fields[0], 16) if code < 0x2801 or code >= 0x2840: continue # Convert the name from something like "braille pattern # dots-1234" to something like "brailleABCD" for use as a # control sequence. name = fields[1][21:].strip() # Skip "braille pattern dots-". name = 'braille' + name.translate(dig2let) braille.append((hex(code)[2:].upper(), name)) braille.append(('2820', 'braillecapital')) return braille def get_serif_symbols(): '''Return a list of (hex, name) tuples of symbols to declare with newcomputermodern's serif variant.''' return get_braille_symbols() def write_dummy_sty(): '''Create an empty fakenewcomputermodern.sty file to use when newcomputermodern.sty is unavailable.''' with open('fakenewcomputermodern.sty', 'w') as w: w.write(header_string('%')) w.write('\n') w.write('\\endinput\n') def read_provides_package(sty): '''Extract the \\ProvidesPackage line from newcomputermodern.sty. Abort if such a line is not found.''' # Extract the \ProvidesPackage line. provides_package = None with open(sty) as r: for ln in r: if ln.startswith('\\ProvidesPackage'): provides_package = ln break if provides_package is None: raise RuntimeError(f'failed to find a \\ProvidesPackage line in {sty}') # At the time of this writing (6-Jan-2026), newcomputermodern's # \ProvidesPackage version string lacks a date. This causes # \fakeusepackage to crash. As a workaround we try to extract the date # from newcomputermodern's documentation, newcm-doc.pdf. unversioned = '\\ProvidesPackage{newcomputermodern}' if re.search(r'\[\d+[-/]\d+[-/]\d+\s', provides_package) is not None: # newcomputermodern provides a date now. return provides_package doc = kpsewhich('newcm-doc.pdf', must_exist=False, format='TeX system documentation') if doc is None: # No documentation file. Return an unversioned \ProvidesPackage line. return unversioned proc = subprocess.run([ 'pdfinfo', '-isodates', doc, ], capture_output=True, check=False, encoding='utf-8') if proc.returncode != 0: # pdfinfo failed. Return an unversioned \ProvidesPackage line. return unversioned date_str = None for ln in proc.stdout.split('\n'): fields = ln.split(None, maxsplit=1) if len(fields) == 2 and fields[0] == 'CreationDate:': date_str = fields[1] break if date_str is None: # No creation date found. Return an unversioned \ProvidesPackage line. return unversioned try: date = dateutil.parser.isoparse(date_str) except ValueError: # Creation date is not parseable. Return an unversioned # \ProvidesPackage line. return unversioned match = re.search(r'\[(.*)\]', provides_package) if match is None: ver_str = 'v0.0 Faked New Computer Modern' else: ver_str = '%04d-%02d-%02d %s' % \ (date.year, date.month, date.day, match[1]) return '\\ProvidesPackage{newcomputermodern}[%s]\n' % ver_str def write_lualatex_sty(sty): '''Create a basic fakenewcomputermodern.sty file to use with LuaLaTeX. It replicates newcomputermodern's \\ProvidesPackage line and defines a few helper macros.''' with open('fakenewcomputermodern.sty', 'w') as w: # Write a style-file header. w.write(header_string('%')) w.write('\n') w.write(read_provides_package(sty)) w.write('\\RequirePackage{fontspec}\n') w.write('\n') # Define symbols from newcomputermodern's serif font. w.write(r'\newcommand*{\NCMrmfamily}{\fontspec{NewCM10-Book}}' + '\n') for cp, sym in get_serif_symbols(): w.write('\\DeclareRobustCommand*{\\NCM%s}{{\\NCMrmfamily\\char"%s}}\n' % (sym, cp)) w.write('\n') # Define symbols from newcomputermodern's sans-serif font. w.write(r'\newcommand*{\NCMsffamily}{\fontspec{NewCMSans10-Book}}' + '\n') for cp, sym in get_sans_symbols(): w.write('\\DeclareRobustCommand*{\\NCM%s}{{\\NCMsffamily\\char"%s}}\n' % (sym, cp)) w.write('\n') # Define symbols from newcomputermodern's math font. w.write(r'\newcommand*{\NCMmathfamily}{\fontspec{NewCMMath-Book}}' + '\n') for cp, sym in math_symbols: w.write('\\DeclareRobustCommand*{\\NCM%s}{{\\NCMmathfamily\\char"%s}}\n' % (sym, cp)) w.write('\n') w.write('\\endinput\n') def create_auxiliary_files(otf, base, symbols): '''Generate a .pfb file, a .tfm file, and a .enc file. Return a map-file line that associates all three of these.''' # Create an encoding file, to be deleted on exit. enc_in = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', suffix='.enc') enc_in.write(header_string('%')) enc_in.write('\n') enc_in.write(f'/{base} [\n') for i, (cp, sym) in enumerate(symbols): enc_in.write(' /u%s %% %3d: %s\n' % (cp, i, sym)) enc_in.write(']\n') enc_in.flush() # Use otftotfm to generate various auxiliary files. otf = kpsewhich(otf) map_fname = base + '.map' try: # We don't want otftotfm to append to an existing file. os.remove(map_fname) except FileNotFoundError: pass subprocess.run(['otftotfm', '--encoding=' + enc_in.name, f'--map-file={map_fname}', '--no-updmap', otf, base], check=True, encoding='utf-8') # Parse the map file for the name of the generated encoding file. enc = None toks = [] with open(map_fname, encoding='utf-8') as r: for ln in r: toks = ln.split() if len(toks) == 6: enc = toks[4][2:] break if enc is None: raise RuntimeError(f'failed to parse {map_fname}') # Rename the encoding file from something like "a_2wefkb.enc" to # "{base}.enc". os.rename(enc, f'{base}.enc') # Replace the name of the .enc file in the line read from the map # file with "{base}.enc" then remove the map file. toks[4] = toks[4][:2] + base + '.enc' os.remove(map_fname) # Return the modified line read from the map file. return ' '.join(toks) def write_pdflatex_sty(sty): '''Create a fakenewcomputermodern.sty package and related font maps to use with pdfLaTeX. The package replicates newcomputermodern's \\ProvidesPackage line, loads pdfLaTeX-compatible fonts, and defines a few helper macros.''' # Write a style-file header. with open('fakenewcomputermodern.sty', 'w') as w: w.write(header_string('%')) w.write('\n') w.write(read_provides_package(sty)) w.write('\n') # Define symbols from newcomputermodern's serif font. serif_symbols = get_serif_symbols() serif_map_line = create_auxiliary_files('NewCM10-Book.otf', 'newcm', serif_symbols) with open('fakenewcomputermodern.sty', 'a') as w: w.write('\\pdfmapline{=%s}\n' % serif_map_line) w.write(r'\font\newcm=newcm at 10pt' + '\n') for i, (cp, sym) in enumerate(serif_symbols): w.write('\\DeclareRobustCommand*{\\NCM%s}{{\\newcm\\char"%02X}} %% Was "%s\n' % (sym, i, cp)) w.write('\n') # Define symbols from newcomputermodern's sans-serif font. sans_symbols = get_sans_symbols() sans_map_line = create_auxiliary_files('NewCMSans10-Book.otf', 'newcmsans', sans_symbols) with open('fakenewcomputermodern.sty', 'a') as w: w.write('\\pdfmapline{=%s}\n' % sans_map_line) w.write(r'\font\newcmsans=newcmsans at 10pt' + '\n') for i, (cp, sym) in enumerate(sans_symbols): w.write('\\DeclareRobustCommand*{\\NCM%s}{{\\newcmsans\\char"%02X}} %% Was "%s\n' % (sym, i, cp)) w.write('\n') # Define symbols from newcomputermodern's math font. math_map_line = create_auxiliary_files('NewCMMath-Book.otf', 'newcmmath', math_symbols) with open('fakenewcomputermodern.sty', 'a') as w: w.write('\\pdfmapline{=%s}\n' % math_map_line) w.write(r'\font\newcmmath=newcmmath at 10pt' + '\n') for i, (cp, sym) in enumerate(math_symbols): w.write('\\DeclareRobustCommand*{\\NCM%s}{{\\newcmmath\\char"%02X}} %% Was "%s\n' % (sym, i, cp)) w.write('\n') w.write('\\endinput\n') if __name__ == '__main__': # Determine which TeX engine we're targeting. if len(sys.argv) == 1: sys.stderr.write('Usage: %s \n' % sys.argv[0]) sys.exit(1) latex = Path(sys.argv[1]).stem.lower() # Determine what sort of file(s) we need to generate. sty = kpsewhich('newcomputermodern.sty', must_exist=False) if sty is None: # newcomputermodern.sty is unavailable. write_dummy_sty() sys.exit(0) if latex in ['lualatex', 'xelatex']: # LuaLaTeX or XeLaTeX write_lualatex_sty(sty) sys.exit(0) if latex == 'pdflatex': # pdfLaTeX write_pdflatex_sty(sty) sys.exit(0) raise RuntimeError(f'unrecognized LaTeX command {latex}')