%% cmgraded.sty %% %% Computer Modern Graded Fonts / LaTeX Package 'cmgraded' %% %% Classic LaTeX Look and Feel with LuaLaTeX and OpenType %% Fonts in different Grades of Blackness %% %% This LaTeX package with name "cmgraded" contains OpenType and Type 1 %% (Adobe/PostScript) fonts named "Computer Modern Graded" that have been %% generated from the original "Computer Modern" fonts by Donald E. Knuth %% and TeX metrics by applying different grades of "blackness" via METAFONT %% that make the fonts gradually darker, while keeping metrics intact and %% not turning a regular weight font visually into a bold one. %% %% Copyright (c) 2026 Alain Stalder (alain@exactphilosophy.net). %% https://www.exactphilosophy.net/patafont %% %% This work may be distributed and/or modified under the conditions of the %% LaTeX Project Public License, either version 1.3c of this license or (at %% your option) any later version. The latest version of this license is in %% https://www.latex-project.org/lppl.txt and version 1.3c or later is part %% of all distributions of LaTeX version 2008 or later. %% %% This work has the LPPL maintenance status 'maintained'. %% Current Maintainer of this work is Alain Stalder (alain@exactphilosophy.net). %% %% The fonts in this package are licensed separately under the SIL Open Font %% License v1.1, with Reserved Font Name "Computer Modern Graded", see the %% OFL.txt included with this package and note that the OpenType fonts also %% contain the full license inside in their name tables. %% \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{cmgraded}[2026/07/29 v1.0.0 Grade-applied METAFONT Computer Modern as OpenType and Type 1] \RequirePackage{iftex} \RequirePackage{kvoptions} \SetupKeyvalOptions{family=cmg,prefix=cmg@} \DeclareStringOption[19]{grade} \DeclareStringOption[]{text}% shorthand for serif+sans+typewriter \DeclareStringOption[]{serif}% auto|type1|opentype|none \DeclareStringOption[]{sans}% \DeclareStringOption[]{typewriter}% \DeclareStringOption[]{math}% \DeclareBoolOption[false]{allowunsupported}% \DeclareStringOption[classic]{italiccorrection}% classic|modern: refine the OpenType-text italic correction (lualatex) \DeclareStringOption[modern]{hyphenation}% accentsclassic|modern: OT1-style accent vs full LuaTeX hyphenation (lualatex) \DeclareStringOption[classic]{ttsentencespacing}% classic|modern: two spaces after a period in \texttt (classic cmtt EXTRASPACE) vs lualatex's single (lualatex OpenType) \DeclareStringOption[modern]{quotedsentencespacing}% classic|modern (GLOBAL): pdflatex-OT1's narrower vs lualatex's sentence space after '.' \ProcessKeyvalOptions* \def\cmg@auto{auto}\def\cmg@typeone{type1}\def\cmg@opentype{opentype}\def\cmg@none{none}\def\cmg@empty{} \def\cmg@err#1{\PackageError{cmgraded}{#1}{See the package documentation.}} \def\cmg@info#1{\PackageInfo{cmgraded}{#1}} %% italiccorrection (classic|modern) and hyphenation (accentsclassic|modern): %% 'classic'/'accentsclassic' reproduce OT1, 'modern' uses the full LuaTeX %% behaviour. Hyphenation is 'accentsclassic' rather than 'classic' because it %% reproduces OT1 only for *accents* (hyphenating an accented word up to its first %% accent); other words follow LuaTeX's own patterns, which differ from pdflatex's %% below this package. Both refine the OpenType-text path under lualatex; under %% other engines the classic behaviour is inherent, so the keys are a quiet no-op. \def\cmg@classic{classic}\def\cmg@modern{modern}\def\cmg@accentsclassic{accentsclassic} \newif\ifcmg@italicclassic \newif\ifcmg@hyphenclassic \newif\ifcmg@ttspaceclassic \newif\ifcmg@quotedclassic \ifx\cmg@italiccorrection\cmg@classic \cmg@italicclassictrue \else\ifx\cmg@italiccorrection\cmg@modern \cmg@italicclassicfalse \else \cmg@err{italiccorrection must be `classic' or `modern' (got `\cmg@italiccorrection')}\cmg@italicclassictrue\fi\fi \ifx\cmg@hyphenation\cmg@accentsclassic \cmg@hyphenclassictrue \else\ifx\cmg@hyphenation\cmg@modern \cmg@hyphenclassicfalse \else \cmg@err{hyphenation must be `accentsclassic' or `modern' (got `\cmg@hyphenation')}\cmg@hyphenclassicfalse\fi\fi \ifx\cmg@ttsentencespacing\cmg@classic \cmg@ttspaceclassictrue \else\ifx\cmg@ttsentencespacing\cmg@modern \cmg@ttspaceclassicfalse \else \cmg@err{ttsentencespacing must be `classic' or `modern' (got `\cmg@ttsentencespacing')}\cmg@ttspaceclassictrue\fi\fi \ifx\cmg@quotedsentencespacing\cmg@classic \cmg@quotedclassictrue \else\ifx\cmg@quotedsentencespacing\cmg@modern \cmg@quotedclassicfalse \else \cmg@err{quotedsentencespacing must be `classic' or `modern' (got `\cmg@quotedsentencespacing')}\cmg@quotedclassicfalse\fi\fi %% Missing-grade signpost: only some grades are installed (the package ships the %% seven presets a..g); a numeric grade whose fonts are absent gets a pointer to %% the full set rather than a cryptic missing-font failure. Shown at most once. \newif\ifcmg@gradeshown \def\cmg@gradegone{\ifcmg@gradeshown\else\cmg@gradeshowntrue \cmg@err{grade \cmg@grade\space is not installed. This package ships the seven presets a..g (grades 0, 10, 19, 32, 44, 57, 72); the full range of grades 0..72 for finer control is available from https://www.exactphilosophy.net/patafont}\fi} %% Grade letters a..g (case-insensitive): memorable aliases for the seven %% perceptually-even grades (a=lightest ... g=heaviest). Numeric grades 0..72 %% still work for fine control. Mapping: a=0 b=10 c=19 d=32 e=44 f=57 g=72. \newif\ifcmg@gletter \newif\ifcmg@gok \def\cmg@ckdigits#1{% set \cmg@gokfalse if any token of the value is not a digit \ifx\relax#1\else \ifnum`#1>`/ \ifnum`#1<`: \else\cmg@gokfalse\fi\else\cmg@gokfalse\fi \expandafter\cmg@ckdigits \fi} \def\cmg@gradealias#1#2{\def\cmg@tmp{#1}\ifx\cmg@grade\cmg@tmp\def\cmg@grade{#2}\cmg@glettertrue\fi} \cmg@gradealias{a}{0}\cmg@gradealias{A}{0}% \cmg@gradealias{b}{10}\cmg@gradealias{B}{10}% \cmg@gradealias{c}{19}\cmg@gradealias{C}{19}% \cmg@gradealias{d}{32}\cmg@gradealias{D}{32}% \cmg@gradealias{e}{44}\cmg@gradealias{E}{44}% \cmg@gradealias{f}{57}\cmg@gradealias{F}{57}% \cmg@gradealias{g}{72}\cmg@gradealias{G}{72}% \ifcmg@gletter\else \cmg@goktrue \expandafter\cmg@ckdigits\cmg@grade\relax \ifcmg@gok\else \cmg@err{grade `\cmg@grade' is invalid; use an integer 0..72 or a letter a..g}% \def\cmg@grade{19}% \fi \fi %% grade range 0..72 \ifnum\cmg@grade<0 \cmg@err{grade \cmg@grade\space out of range 0..72}\fi \ifnum\cmg@grade>72 \cmg@err{grade \cmg@grade\space out of range 0..72}\fi %% italiccorrection/hyphenation act only on the OpenType-text path under lualatex %% (both are luatex mechanisms). Under other engines they are a quiet no-op -- the %% classic behaviour is inherent there -- so there is nothing to check here. %% quotedsentencespacing=classic (opt-in, GLOBAL): give the closing double quote %% U+201D a space factor of 1000, so a sentence-ending '.'/':' just before it does %% not carry its extra (non-French) space into the following gap -- matching %% pdflatex+OT1, where the closing quote resets the factor. Default 'modern' keeps %% lualatex's (sentence) space. GLOBAL because \sfcode is keyed by character code, %% not font, so it affects '."' spacing for every font in the document. Only the %% Unicode (OpenType) engines address U+201D directly; pdftex is 8-bit and would %% reject the code point, so it is skipped there (and OT1 already matches). Applied %% at \begin{document} so it wins over babel/csquotes etc. \ifcmg@quotedclassic \ifPDFTeX\else \AtBeginDocument{\sfcode"201D=1000\relax}% \fi \fi %% NFSS Type 1 families, the map, and the OpenType (fontspec) family names. \edef\cmg@t@r {cmgg\cmg@grade r}% \edef\cmg@t@ss{cmgg\cmg@grade ss}% \edef\cmg@t@tt{cmgg\cmg@grade tt}% \edef\cmg@t@m {cmgg\cmg@grade m}% \edef\cmg@t@sy{cmgg\cmg@grade sy}% \edef\cmg@t@ex{cmgg\cmg@grade ex}% \edef\cmg@map {cmgg\cmg@grade.map}% \edef\cmg@o@serif{Computer Modern Graded G\cmg@grade\space Serif}% \edef\cmg@o@sans {Computer Modern Graded G\cmg@grade\space Sans}% \edef\cmg@o@tt {Computer Modern Graded G\cmg@grade\space Typewriter}% %% Engine 'auto' formats (text and math). \ifLuaTeX \def\cmg@textauto{opentype}\def\cmg@mathauto{type1}% \else\ifXeTeX \def\cmg@textauto{opentype}\def\cmg@mathauto{type1}% \else\ifPDFTeX \def\cmg@textauto{type1}\def\cmg@mathauto{type1}% \else \cmg@err{unsupported engine (need pdflatex, lualatex, or xelatex)}% \def\cmg@textauto{type1}\def\cmg@mathauto{type1}% \fi\fi\fi %% Effective per-face value: longhand overrides the 'text' shorthand, else auto. \def\cmg@effface#1#2{% \ifx#2\cmg@empty \ifx\cmg@text\cmg@empty \let#1\cmg@auto \else \let#1\cmg@text \fi \else \let#1#2\fi} \cmg@effface\cmg@serifF\cmg@serif \cmg@effface\cmg@sansF\cmg@sans \cmg@effface\cmg@ttF\cmg@typewriter \ifx\cmg@math\cmg@empty \let\cmg@mathF\cmg@auto \else \let\cmg@mathF\cmg@math \fi %% The three text faces share ONE format: collect explicit type1/opentype %% signals; more than one distinct -> error; else the engine 'auto' text format. \let\cmg@fmt\relax \def\cmg@recordfmt#1{% \ifx\cmg@fmt\relax \let\cmg@fmt#1\else \ifx\cmg@fmt#1\else \cmg@err{all text faces must share one format (got a mix of type1 and opentype)}% \fi\fi} \def\cmg@seefmt#1{% \ifx#1\cmg@typeone \cmg@recordfmt\cmg@typeone \else \ifx#1\cmg@opentype \cmg@recordfmt\cmg@opentype \fi\fi} \cmg@seefmt\cmg@serifF \cmg@seefmt\cmg@sansF \cmg@seefmt\cmg@ttF \ifx\cmg@fmt\relax \let\cmg@fmt\cmg@textauto \fi \ifx\cmg@mathF\cmg@auto \let\cmg@mathF\cmg@mathauto \fi %% Presence (on unless 'none') and derived flags. \newif\ifcmg@serifon \newif\ifcmg@sanson \newif\ifcmg@tton \ifx\cmg@serifF\cmg@none \cmg@serifonfalse \else \cmg@serifontrue \fi \ifx\cmg@sansF \cmg@none \cmg@sansonfalse \else \cmg@sansontrue \fi \ifx\cmg@ttF \cmg@none \cmg@ttonfalse \else \cmg@ttontrue \fi \newif\ifcmg@anytext \ifcmg@serifon\cmg@anytexttrue\fi \ifcmg@sanson\cmg@anytexttrue\fi \ifcmg@tton\cmg@anytexttrue\fi \newif\ifcmg@texttypeone \newif\ifcmg@textopentype \newif\ifcmg@mathtypeone \ifcmg@anytext \ifx\cmg@fmt\cmg@typeone \cmg@texttypeonetrue\fi \ifx\cmg@fmt\cmg@opentype \cmg@textopentypetrue\fi \fi \ifx\cmg@mathF\cmg@typeone \cmg@mathtypeonetrue\fi %% Support policy: unless 'allowunsupported', restrict to the maintained combo %% (lualatex + OpenType text + Type 1 math). Checked here at load, where the %% engine and the resolved text format are both known. One \cmg@unsupported message, %% parameterised by engine/format/detail; only one case can apply (engine-disjoint). \def\cmg@unsupported#1#2#3{% \cmg@err{To use the #1 engine (incl. for using #2)#3, you must set the package key 'allowunsupported', e.g. '\string\usepackage[allowunsupported]\string{cmgraded\string}', but note that #1#3 is only technically supported in a basic way with no plans to ever fully support it; the focus of this package is on OpenType fonts in lualatex to quite faithfully reproduce the output of the original latex including especially grading/blackening for the Computer Modern fonts with OT1 font encoding using METAFONT mechanisms, typically ignoring typographical changes after Knuth's freeze around 1992; see the package documentation for more details and note that other packages would be free to use the fonts in this package and add features into other directions}% } \unless\ifcmg@allowunsupported \ifPDFTeX \cmg@unsupported{pdftex}{pdflatex}{}% \else\ifXeTeX \cmg@unsupported{xetex}{xelatex}{}% \else\ifLuaTeX \ifcmg@texttypeone \cmg@unsupported{luatex}{lualatex}{\space with Type 1 (Adobe/PostScript) fonts}% \fi \fi\fi\fi \fi %% Guardrails (capability-based; only pdflatex+OpenType is a hard 'no'). \ifx\cmg@mathF\cmg@opentype \cmg@err{OpenType math is not yet available (planned as a later phase); use math=type1 or math=none}\fi \ifPDFTeX \ifcmg@textopentype \cmg@err{pdflatex cannot embed OpenType; use text=type1}\fi \fi %% Register the map whenever any Type 1 side is active. \newif\ifcmg@anytypeone \ifcmg@texttypeone\cmg@anytypeonetrue\fi \ifcmg@mathtypeone\cmg@anytypeonetrue\fi \ifcmg@anytypeone \IfFileExists{ot1cmgg\cmg@grade r.fd}{}{\cmg@gradegone}% grade's Type 1 fonts present? \ifLuaTeX \pdfextension mapfile{+\cmg@map}% \else\ifPDFTeX \pdfmapfile{+\cmg@map}% \else\ifXeTeX \AtBeginDocument{\special{dvipdfmx:config f \cmg@map}}% \fi\fi\fi \fi %% --- text --- \ifcmg@textopentype \RequirePackage{fontspec}% %% The classic refinements below are luatex mechanisms; skip them under xetex %% (which has no \directlua), where their absence is harmless. \ifLuaTeX %% italiccorrection=classic: register the luaotfload patch BEFORE the fonts %% load, so every CMG oblique face gets the exact METAFONT charic (else %% luaotfload's ink-based, grade-drifting correction is used). \ifcmg@italicclassic \directlua{local f=kpse.find_file("cmgraded-italiccorrection.lua") if f then dofile(f) end}% \fi %% hyphenation=accentsclassic: reproduce OT1+pdflatex, where TeX's \accent %% makes an accented letter a boundary -- only the run before a word's first %% accent is hyphenated automatically (the precomposed OpenType glyphs would %% otherwise hyphenate in full, the modern LuaTeX way). \ifcmg@hyphenclassic \directlua{local f=kpse.find_file("cmgraded-hyphenation.lua") if f then dofile(f) end}% \cmg@info{hyphenation=accentsclassic: accented words are hyphenated only up to their first accent, as in OT1+pdflatex (mark the rest by hand with \string\-); use hyphenation=modern for full (default LuaTeX) hyphenation}% \fi %% ttsentencespacing=classic: luaotfload zeroes the extra sentence space %% (fontdimen7) for monospaced fonts, so lualatex puts ONE space after a %% period in \texttt where classic cmtt gives two. Restore it -- EXTRASPACE %% = a full space, as in cmtt10.tfm -- whenever the typewriter font is %% selected (per size), so \nonfrenchspacing yields the classic two spaces. \ifcmg@tton\ifcmg@ttspaceclassic \AddToHook{selectfont}{\ifx\f@family\ttdefault \fontdimen7\font=\fontdimen2\font\relax \fi}% \fi\fi \fi \ifcmg@serifon \begingroup\edef\x{\endgroup\noexpand\setmainfont{\cmg@o@serif}}\x\fi \ifcmg@sanson \begingroup\edef\x{\endgroup\noexpand\setsansfont{\cmg@o@sans}}\x\fi \ifcmg@tton \begingroup\edef\x{\endgroup\noexpand\setmonofont{\cmg@o@tt}}\x\fi \fi \ifcmg@texttypeone \AtBeginDocument{% \def\cmg@T@one{T1}% \ifx\encodingdefault\cmg@T@one \cmg@err{Type 1 Computer Modern Graded text needs OT1; remove \string\usepackage[T1]{fontenc}}% \fi \renewcommand\encodingdefault{OT1}% \ifcmg@serifon \let\rmdefault\cmg@t@r \fi \ifcmg@sanson \let\sfdefault\cmg@t@ss \fi \ifcmg@tton \let\ttdefault\cmg@t@tt \fi \normalfont }% \fi %% Force our begindocument code to run AFTER fontspec's, so fontspec has already %% set up its private legacy-maths font (digits/+/=/()) before we re-point it %% to the blackened cmr below. fontspec renamed that symbol font across versions %% ('typeonemaths' -> 'legacymaths'), so we re-point whichever one is present. %% \SetSymbolFont/\SetMathAlphabet are preamble-only, so %% this must stay in the begindocument hook (not begindocument/end, where they are %% already disabled) -- the ordering is enforced by rule, not by hook choice. %% Without the rule the order is fragile: e.g. quotedsentencespacing=classic registers an earlier %% \AtBeginDocument, which made this block run BEFORE fontspec, so symtypeonemaths was %% not yet defined and the \@ifundefined guard below silently skipped the override, %% leaking stock cmr into math. fontspec's begindocument label is engine-specific %% (fontspec-luatex / fontspec-xetex); both rules are inert if that label is absent. \DeclareHookRule{begindocument}{fontspec-luatex}{before}{cmgraded}% \DeclareHookRule{begindocument}{fontspec-xetex}{before}{cmgraded}% %% --- math (Type 1) --- re-declares the math symbol fonts at begin-document, %% overriding fontspec's begindocument math re-declaration. \ifcmg@mathtypeone \AtBeginDocument{% \begingroup\edef\x{\endgroup \noexpand\SetSymbolFont{operators}{normal}{OT1}{\cmg@t@r}{m}{n}% \noexpand\SetSymbolFont{operators}{bold}{OT1}{\cmg@t@r}{bx}{n}% \noexpand\SetSymbolFont{letters}{normal}{OML}{\cmg@t@m}{m}{it}% \noexpand\SetSymbolFont{letters}{bold}{OML}{\cmg@t@m}{b}{it}% \noexpand\SetSymbolFont{symbols}{normal}{OMS}{\cmg@t@sy}{m}{n}% \noexpand\SetSymbolFont{symbols}{bold}{OMS}{\cmg@t@sy}{b}{n}% \noexpand\SetSymbolFont{largesymbols}{normal}{OMX}{\cmg@t@ex}{m}{n}% }\x \@ifundefined{symtypeonemaths}{}{% \begingroup\edef\x{\endgroup \noexpand\SetSymbolFont{typeonemaths}{normal}{OT1}{\cmg@t@r}{m}{n}% \noexpand\SetSymbolFont{typeonemaths}{bold}{OT1}{\cmg@t@r}{bx}{n}% }\x }% \@ifundefined{symlegacymaths}{}{% \begingroup\edef\x{\endgroup \noexpand\SetSymbolFont{legacymaths}{normal}{OT1}{\cmg@t@r}{m}{n}% \noexpand\SetSymbolFont{legacymaths}{bold}{OT1}{\cmg@t@r}{bx}{n}% }\x }% \begingroup\edef\x{\endgroup \noexpand\SetMathAlphabet{\noexpand\mathrm}{normal}{OT1}{\cmg@t@r}{m}{n}% \noexpand\SetMathAlphabet{\noexpand\mathrm}{bold}{OT1}{\cmg@t@r}{bx}{n}% \noexpand\SetMathAlphabet{\noexpand\mathbf}{normal}{OT1}{\cmg@t@r}{bx}{n}% \noexpand\SetMathAlphabet{\noexpand\mathbf}{bold}{OT1}{\cmg@t@r}{bx}{n}% \noexpand\SetMathAlphabet{\noexpand\mathit}{normal}{OT1}{\cmg@t@r}{m}{it}% \noexpand\SetMathAlphabet{\noexpand\mathit}{bold}{OT1}{\cmg@t@r}{bx}{it}% \noexpand\SetMathAlphabet{\noexpand\mathsf}{normal}{OT1}{\cmg@t@ss}{m}{n}% \noexpand\SetMathAlphabet{\noexpand\mathsf}{bold}{OT1}{\cmg@t@ss}{bx}{n}% \noexpand\SetMathAlphabet{\noexpand\mathtt}{normal}{OT1}{\cmg@t@tt}{m}{n}% \noexpand\SetMathAlphabet{\noexpand\mathtt}{bold}{OT1}{\cmg@t@tt}{bx}{n}% }\x }% \fi \endinput