diff --git a/.github/workflows/FontArtifacts.yml b/.github/workflows/FontArtifacts.yml
new file mode 100644
index 00000000..9727d1c4
--- /dev/null
+++ b/.github/workflows/FontArtifacts.yml
@@ -0,0 +1,46 @@
+name: Verify generated font artifacts
+
+on:
+ push:
+ paths:
+ - CSharpMath.FontGenerator/**
+ - CSharpMath.FontGenerator.Tests/**
+ - CSharpMath.Rendering/Generated Fonts/**
+ - CSharpMath.Rendering/Reference Fonts/**
+ - .github/workflows/FontArtifacts.yml
+ pull_request:
+ paths:
+ - CSharpMath.FontGenerator/**
+ - CSharpMath.FontGenerator.Tests/**
+ - CSharpMath.Rendering/Generated Fonts/**
+ - CSharpMath.Rendering/Reference Fonts/**
+ - .github/workflows/FontArtifacts.yml
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: 10.x
+ - name: Restore generator tests
+ run: dotnet restore CSharpMath.FontGenerator.Tests/CSharpMath.FontGenerator.Tests.csproj
+ - name: Build and test generator
+ run: dotnet test CSharpMath.FontGenerator.Tests/CSharpMath.FontGenerator.Tests.csproj --configuration Release --no-restore
+ - name: Verify checked-in source and payload hashes
+ run: >-
+ dotnet run --project CSharpMath.FontGenerator/CSharpMath.FontGenerator.csproj
+ --configuration Release --no-restore --
+ verify "CSharpMath.Rendering/Reference Fonts" "CSharpMath.Rendering/Generated Fonts"
+ - name: Regenerate artifacts
+ run: >-
+ dotnet run --project CSharpMath.FontGenerator/CSharpMath.FontGenerator.csproj
+ --configuration Release --no-restore --
+ generate "CSharpMath.Rendering/Reference Fonts" "$RUNNER_TEMP/csharpmath-fonts"
+ - name: Require byte-for-byte deterministic output
+ shell: bash
+ run: diff --recursive --unified "CSharpMath.Rendering/Generated Fonts" "$RUNNER_TEMP/csharpmath-fonts"
diff --git a/.gitignore b/.gitignore
index 04a3ae08..d5221ff3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ CSharpMath.Xaml.Tests.NuGet/Test.*.png
# Ignore generated content
/.benchmarkresults
+**/GeneratedPrototype/
/.nupkgs
/.testcoverage
/.vscode
@@ -439,4 +440,4 @@ FodyWeavers.xsd
*.msi
*.msix
*.msm
-*.msp
\ No newline at end of file
+*.msp
diff --git a/CSharpMath.FontGenerator.Tests/CSharpMath.FontGenerator.Tests.csproj b/CSharpMath.FontGenerator.Tests/CSharpMath.FontGenerator.Tests.csproj
new file mode 100644
index 00000000..73974f42
--- /dev/null
+++ b/CSharpMath.FontGenerator.Tests/CSharpMath.FontGenerator.Tests.csproj
@@ -0,0 +1,6 @@
+
+ net8.0false
+
+
+
+
diff --git a/CSharpMath.FontGenerator.Tests/FontBlobTests.cs b/CSharpMath.FontGenerator.Tests/FontBlobTests.cs
new file mode 100644
index 00000000..ee5e67f7
--- /dev/null
+++ b/CSharpMath.FontGenerator.Tests/FontBlobTests.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using CSharpMath.FontGenerator;
+using Xunit;
+
+namespace CSharpMath.FontGenerator.Tests;
+
+public sealed class FontBlobTests {
+ private static readonly string Root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../"));
+ private static readonly string Sources = Path.Combine(Root, "CSharpMath.Rendering", "Reference Fonts");
+ private static readonly string Generated = Path.Combine(Root, "CSharpMath.Rendering", "Generated Fonts");
+ private static readonly string[] Faces = { "latinmodern-math", "AMS-Capital-Blackboard-Bold", "cyrillic-modern-nmr10" };
+
+ [Fact]
+ public void AllFacesVerifyAndRepeatedGenerationIsStable() {
+ foreach (var face in Faces) {
+ var source = Path.Combine(Sources, face + ".otf");
+ var blob = Path.Combine(Generated, face + ".csmfont");
+ Assert.NotEmpty(FontBlob.ReadAndValidate(blob, source));
+ var first = File.ReadAllBytes(blob);
+ using var temp = new TempDirectory();
+ FontBlob.Write(source, Path.Combine(temp.Path, "font.csmfont"));
+ Assert.Equal(first, File.ReadAllBytes(Path.Combine(temp.Path, "font.csmfont")));
+ }
+ }
+
+ [Fact]
+ public void HeaderFailuresAreRejected() {
+ var source = Path.Combine(Sources, "latinmodern-math.otf");
+ var original = File.ReadAllBytes(Path.Combine(Generated, "latinmodern-math.csmfont"));
+ foreach (var (offset, value) in new[] { (0, (byte)0), (4, (byte)0), (8, (byte)0), (120, (byte)0) }) {
+ var tampered = (byte[])original.Clone(); tampered[offset] ^= 0xFF;
+ using var temp = new TempDirectory(); var path = Path.Combine(temp.Path, "bad.csmfont"); File.WriteAllBytes(path, tampered);
+ Assert.Throws(() => FontBlob.ReadAndValidate(path, source));
+ }
+ var wrongSource = Path.Combine(Sources, "cyrillic-modern-nmr10.otf");
+ using var mismatch = new TempDirectory(); var mismatchPath = Path.Combine(mismatch.Path, "font.csmfont"); File.WriteAllBytes(mismatchPath, original);
+ Assert.Throws(() => FontBlob.ReadAndValidate(mismatchPath, wrongSource));
+ }
+
+ [Fact]
+ public void TableContainerExcludesShapingAndPreservesEveryOtherTable() {
+ foreach (var face in Faces) {
+ var sourceBytes = File.ReadAllBytes(Path.Combine(Sources, face + ".otf"));
+ var payload = FontBlob.BuildRequiredPayload(sourceBytes);
+ Assert.Equal(new byte[] { (byte)'T', (byte)'B', (byte)'L', 1 }, payload[..4]);
+ var tables = ReadTables(payload);
+ var tags = tables.Keys.ToArray();
+ Assert.Equal(tags.OrderBy(x => x, StringComparer.Ordinal), tags);
+ var count = BinaryPrimitives.ReadUInt32LittleEndian(payload.AsSpan(4, 4)); var p = 8;
+ for (var i = 0; i < count; i++) { p += 4; var len = BinaryPrimitives.ReadUInt32LittleEndian(payload.AsSpan(p, 4)); p += 4 + checked((int)len); }
+ Assert.DoesNotContain("GSUB", tags); Assert.DoesNotContain("GPOS", tags);
+ foreach (var (tag, bytes) in SourceTables(sourceBytes)) if (tag is not ("GSUB" or "GPOS")) Assert.Equal(bytes, tables[tag]);
+ }
+ }
+
+ [Fact]
+ public void ManifestFaceOrderIsStableAndVerifyRejectsTampering() {
+ using var generated = new TempDirectory();
+ Assert.Equal(0, Program.Main(new[] { "generate", Sources, generated.Path }));
+ using var manifest = JsonDocument.Parse(File.ReadAllText(Path.Combine(generated.Path, "manifest.json")));
+ var names = manifest.RootElement.GetProperty("fonts").EnumerateArray().Select(x => x.GetProperty("Source").GetString()).ToArray();
+ Assert.Equal(new[] { "AMS-Capital-Blackboard-Bold.otf", "cyrillic-modern-nmr10.otf", "latinmodern-math.otf" }, names);
+ foreach (var file in Directory.GetFiles(Generated)) File.Copy(file, Path.Combine(generated.Path, Path.GetFileName(file)), true);
+ var tampered = Path.Combine(generated.Path, "latinmodern-math.csmfont");
+ var bytes = File.ReadAllBytes(tampered); bytes[^1] ^= 0x80; File.WriteAllBytes(tampered, bytes);
+ Assert.Throws(() => Program.Main(new[] { "verify", Sources, generated.Path }));
+ }
+
+ private static Dictionary ReadTables(byte[] payload) { var result = new Dictionary(StringComparer.Ordinal); var n = BinaryPrimitives.ReadUInt32LittleEndian(payload.AsSpan(4, 4)); var p = 8; for (var i = 0; i < n; i++) { var tag = System.Text.Encoding.ASCII.GetString(payload, p, 4); p += 4; var len = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(payload.AsSpan(p, 4))); p += 4; result.Add(tag, payload.AsSpan(p, len).ToArray()); p += len; } return result; }
+ private static IEnumerable<(string Tag, byte[] Bytes)> SourceTables(byte[] bytes) { var n = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(4)); for (var i = 0; i < n; i++) { var p = 12 + i * 16; var tag = System.Text.Encoding.ASCII.GetString(bytes, p, 4); var offset = checked((int)BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(p + 8))); var len = checked((int)BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(p + 12))); yield return (tag, bytes.AsSpan(offset, len).ToArray()); } }
+ private sealed class TempDirectory : IDisposable { public string Path { get; } = System.IO.Directory.CreateTempSubdirectory("csmf-").FullName; public void Dispose() => Directory.Delete(Path, true); }
+}
diff --git a/CSharpMath.FontGenerator/CSharpMath.FontGenerator.csproj b/CSharpMath.FontGenerator/CSharpMath.FontGenerator.csproj
new file mode 100644
index 00000000..ab2c9125
--- /dev/null
+++ b/CSharpMath.FontGenerator/CSharpMath.FontGenerator.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
diff --git a/CSharpMath.FontGenerator/Program.cs b/CSharpMath.FontGenerator/Program.cs
new file mode 100644
index 00000000..5be58872
--- /dev/null
+++ b/CSharpMath.FontGenerator/Program.cs
@@ -0,0 +1,163 @@
+using System.Buffers.Binary;
+using System.IO.Compression;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+
+namespace CSharpMath.FontGenerator;
+
+/// Build-time format for the generated bundled-face inputs.
+internal static class FontBlob {
+ public const uint Schema = 1;
+ private const uint Magic = 0x31464D43; // CMF1, little endian
+ private const uint LittleEndianMarker = 0x01020304;
+
+ public static void Write(string source, string destination) {
+ var bytes = File.ReadAllBytes(source);
+ var hash = SHA256.HashData(bytes);
+ var name = Encoding.UTF8.GetBytes(Path.GetFileName(source));
+ bytes = BuildRequiredPayload(bytes);
+ using var compressed = new MemoryStream();
+ using (var brotli = new BrotliStream(compressed, CompressionLevel.SmallestSize, leaveOpen: true))
+ brotli.Write(bytes);
+ var payload = compressed.ToArray();
+ using var output = File.Create(destination);
+ Span header = stackalloc byte[88];
+ BinaryPrimitives.WriteUInt32LittleEndian(header, Magic);
+ BinaryPrimitives.WriteUInt32LittleEndian(header[4..], Schema);
+ BinaryPrimitives.WriteUInt32LittleEndian(header[8..], LittleEndianMarker);
+ hash.CopyTo(header[12..]);
+ SHA256.HashData(bytes).CopyTo(header[44..]);
+ BinaryPrimitives.WriteUInt32LittleEndian(header[76..], (uint)name.Length);
+ BinaryPrimitives.WriteUInt64LittleEndian(header[80..], (ulong)payload.Length);
+ output.Write(header);
+ output.Write(name);
+ output.Write(payload);
+ }
+
+ public static byte[] ReadAndValidate(string path, string source) {
+ using var input = File.OpenRead(path);
+ Span header = stackalloc byte[88];
+ if (input.Read(header) != header.Length) throw new InvalidDataException("Font blob header is truncated.");
+ if (BinaryPrimitives.ReadUInt32LittleEndian(header) != Magic) throw new InvalidDataException("Font blob magic mismatch.");
+ if (BinaryPrimitives.ReadUInt32LittleEndian(header[4..]) != Schema) throw new InvalidDataException("Font blob schema mismatch.");
+ if (BinaryPrimitives.ReadUInt32LittleEndian(header[8..]) != LittleEndianMarker) throw new InvalidDataException("Font blob endianness mismatch.");
+ var expectedHash = header[12..44].ToArray();
+ var payloadHash = header[44..76].ToArray();
+ var nameLength = BinaryPrimitives.ReadUInt32LittleEndian(header[76..]);
+ var length = BinaryPrimitives.ReadUInt64LittleEndian(header[80..]);
+ if (nameLength > 1024 || length > int.MaxValue) throw new InvalidDataException("Font blob lengths are invalid.");
+ var nameBytes = new byte[(int)nameLength];
+ try { input.ReadExactly(nameBytes); } catch (EndOfStreamException ex) { throw new InvalidDataException("Font blob name is truncated.", ex); }
+ var encodedName = Encoding.UTF8.GetString(nameBytes);
+ if (!string.Equals(encodedName, Path.GetFileName(source), StringComparison.Ordinal)) throw new InvalidDataException("Font blob source name mismatch.");
+ if (length > (ulong)(input.Length - input.Position)) throw new InvalidDataException("Font blob payload is truncated.");
+ var compressed = new byte[(int)length];
+ if (input.Read(compressed) != compressed.Length) throw new InvalidDataException("Font blob payload is truncated.");
+ byte[] payload;
+ try {
+ using var compressedStream = new MemoryStream(compressed, writable: false);
+ using var brotli = new BrotliStream(compressedStream, CompressionMode.Decompress);
+ using var uncompressed = new MemoryStream();
+ brotli.CopyTo(uncompressed);
+ payload = uncompressed.ToArray();
+ if (compressedStream.Position != compressedStream.Length) throw new InvalidDataException("Font blob has trailing compressed data.");
+ } catch (Exception ex) when (ex is InvalidDataException or IOException or InvalidOperationException) { throw new InvalidDataException("Font blob compression is invalid.", ex); }
+ var sourceHash = SHA256.HashData(File.ReadAllBytes(source));
+ if (!CryptographicOperations.FixedTimeEquals(expectedHash, sourceHash)) throw new InvalidDataException("Font source SHA-256 mismatch.");
+ if (!CryptographicOperations.FixedTimeEquals(payloadHash, SHA256.HashData(payload))) throw new InvalidDataException("Font blob payload SHA-256 mismatch.");
+ if (input.Position != input.Length) throw new InvalidDataException("Font blob has trailing data.");
+ return payload;
+ }
+
+ // Keep only deterministic SFNT table records used by CSharpMath. GSUB/GPOS are
+ // deliberately omitted because the renderer does not perform OpenType shaping.
+ internal static byte[] BuildRequiredPayload(byte[] source) {
+ if (source.Length < 12) throw new InvalidDataException("Font SFNT header is truncated.");
+ var count = (source[4] << 8) | source[5];
+ using var output = new MemoryStream();
+ output.Write(new byte[] { (byte)'T', (byte)'B', (byte)'L', 1 });
+ var kept = new List<(string Tag, int Offset, int Length)>();
+ for (var i = 0; i < count; i++) {
+ var p = 12 + i * 16;
+ if (p + 16 > source.Length) throw new InvalidDataException("Font table directory is truncated.");
+ var tag = Encoding.ASCII.GetString(source, p, 4);
+ var offset = (int)BinaryPrimitives.ReadUInt32BigEndian(source.AsSpan(p + 8));
+ var length = (int)BinaryPrimitives.ReadUInt32BigEndian(source.AsSpan(p + 12));
+ if (tag is "GSUB" or "GPOS") continue;
+ if (offset < 0 || length < 0 || offset > source.Length - length) throw new InvalidDataException("Font table range is invalid.");
+ kept.Add((tag, offset, length));
+ }
+ Span number = stackalloc byte[4];
+ BinaryPrimitives.WriteUInt32LittleEndian(number, (uint)kept.Count); output.Write(number);
+ foreach (var table in kept.OrderBy(t => t.Tag, StringComparer.Ordinal)) {
+ output.Write(Encoding.ASCII.GetBytes(table.Tag));
+ BinaryPrimitives.WriteUInt32LittleEndian(number, (uint)table.Length); output.Write(number);
+ output.Write(source, table.Offset, table.Length);
+ }
+ return output.ToArray();
+ }
+}
+
+internal static class Program {
+ private static readonly string[] Bundled = { "latinmodern-math.otf", "AMS-Capital-Blackboard-Bold.otf", "cyrillic-modern-nmr10.otf" };
+
+ public static int Main(string[] args) {
+ if ((args.Length is < 3 or > 4) || (!string.Equals(args[0], "generate", StringComparison.OrdinalIgnoreCase) && !string.Equals(args[0], "verify", StringComparison.OrdinalIgnoreCase))) {
+ Console.Error.WriteLine("Usage: CSharpMath.FontGenerator generate [prototype-directory]");
+ return 2;
+ }
+ var sourceDirectory = Path.GetFullPath(args[1]);
+ var outputDirectory = Path.GetFullPath(args[2]);
+ if (string.Equals(args[0], "verify", StringComparison.OrdinalIgnoreCase)) return Verify(sourceDirectory, outputDirectory);
+ Generate(sourceDirectory, outputDirectory, args.Length == 4 ? Path.GetFullPath(args[3]) : null);
+ return 0;
+ }
+
+ private static int Verify(string sourceDirectory, string outputDirectory) {
+ var temporary = Directory.CreateTempSubdirectory("csmf-verify-");
+ try {
+ Generate(sourceDirectory, temporary.FullName, null);
+ var expected = Directory.GetFiles(temporary.FullName, "*", SearchOption.AllDirectories).Select(p => Path.GetRelativePath(temporary.FullName, p)).OrderBy(p => p, StringComparer.Ordinal).ToArray();
+ var actual = Directory.GetFiles(outputDirectory, "*", SearchOption.AllDirectories).Select(p => Path.GetRelativePath(outputDirectory, p)).OrderBy(p => p, StringComparer.Ordinal).ToArray();
+ if (!expected.SequenceEqual(actual, StringComparer.Ordinal)) throw new InvalidDataException("Generated font artifact set differs from deterministic output.");
+ foreach (var relative in expected) {
+ var a = File.ReadAllBytes(Path.Combine(outputDirectory, relative));
+ var b = File.ReadAllBytes(Path.Combine(temporary.FullName, relative));
+ if (!a.AsSpan().SequenceEqual(b)) throw new InvalidDataException("Generated font artifact differs: " + relative);
+ }
+ Console.WriteLine("CSMF1 artifacts verified against source SHA-256.");
+ return 0;
+ } finally { temporary.Delete(true); }
+ }
+
+ private static void Generate(string sourceDirectory, string outputDirectory, string? prototypeDirectory) {
+ Directory.CreateDirectory(outputDirectory);
+ var entries = new List();
+ foreach (var fileName in Bundled.OrderBy(x => x, StringComparer.Ordinal)) {
+ var source = Path.Combine(sourceDirectory, fileName);
+ if (!File.Exists(source)) throw new FileNotFoundException("Bundled source font is missing", source);
+ var outputName = Path.ChangeExtension(fileName, ".csmfont");
+ FontBlob.Write(source, Path.Combine(outputDirectory, outputName));
+ var bytes = File.ReadAllBytes(source);
+ entries.Add(new ManifestEntry(fileName, outputName, Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), bytes.Length, new FileInfo(Path.Combine(outputDirectory, outputName)).Length));
+ }
+ var manifest = new { schema = FontBlob.Schema, byteOrder = "little", format = "CSMF1", shaping = "GSUB/GPOS intentionally excluded; CSharpMath does not shape text with them.", fonts = entries };
+ File.WriteAllText(Path.Combine(outputDirectory, "manifest.json"), JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }) + Environment.NewLine, new UTF8Encoding(false));
+ if (prototypeDirectory != null) WritePrototype(outputDirectory, prototypeDirectory, entries);
+ }
+
+ private static void WritePrototype(string outputDirectory, string prototypeDirectory, IReadOnlyList entries) {
+ Directory.CreateDirectory(prototypeDirectory);
+ var sb = new StringBuilder("// Generated by CSharpMath.FontGenerator. Do not edit.\nnamespace CSharpMath.Generated;\n\ninternal static class BundledFontPrototype\n{\n");
+ foreach (var entry in entries) {
+ var bytes = File.ReadAllBytes(Path.Combine(outputDirectory, entry.Output));
+ sb.Append(" internal static readonly byte[] ").Append(Path.GetFileNameWithoutExtension(entry.Output).Replace('-', '_')).Append(" = new byte[] { ");
+ sb.Append(string.Join(", ", bytes.Select(b => b.ToString()))).AppendLine(" };\n");
+ }
+ sb.AppendLine("}");
+ File.WriteAllText(Path.Combine(prototypeDirectory, "BundledFontPrototype.g.cs"), sb.ToString(), new UTF8Encoding(false));
+ }
+
+ private sealed record ManifestEntry(string Source, string Output, string SourceSha256, int SourceBytes, long GeneratedBytes);
+}
diff --git a/CSharpMath.FontGenerator/Properties/AssemblyInfo.cs b/CSharpMath.FontGenerator/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..30d64262
--- /dev/null
+++ b/CSharpMath.FontGenerator/Properties/AssemblyInfo.cs
@@ -0,0 +1,2 @@
+using System.Runtime.CompilerServices;
+[assembly: InternalsVisibleTo("CSharpMath.FontGenerator.Tests")]
diff --git a/CSharpMath.FontGenerator/README.md b/CSharpMath.FontGenerator/README.md
new file mode 100644
index 00000000..0c29140e
--- /dev/null
+++ b/CSharpMath.FontGenerator/README.md
@@ -0,0 +1,28 @@
+# CSharpMath bundled-font generator
+
+This build-only tool turns the three checked-in reference OTFs into deterministic
+`CSMF1` blobs and a stable `manifest.json`. The blob records magic, schema,
+endianness, source SHA-256, source name, and payload length. `ReadAndValidate`
+rejects a changed source, tampered payload, unknown schema, wrong byte order, or
+truncated input before any data is consumed.
+
+The optional third argument emits a C# byte-array representation. It is a
+prototype used for the #191 size/startup/allocation/AOT comparison; production
+uses the binary representation. This envelope intentionally does not serialize
+GSUB or GPOS: CSharpMath's typesetter does not perform OpenType shaping.
+
+Current checked-in size evidence (2026-08-31): raw OTF total 786,744 bytes;
+CSMF1/TBL1 total 415,661 bytes (47.2% smaller). The optional C# prototype is
+generated in a temporary directory for measurement only (1,900,468 bytes
+total), and is not checked in. Fixed-width records and no reflection or
+dynamic serialization keep the binary representation suitable for
+AOT/trimming. This artifact phase is **not runtime-consumed yet** and changes
+no startup behavior. The #293 cold-process methodology (fresh process,
+first-render timing, allocated/retained managed bytes) carries to #321 for the
+runtime decoder comparison.
+
+Example:
+
+```text
+dotnet run --project CSharpMath.FontGenerator -- generate "CSharpMath.Rendering/Reference Fonts" generated generated-csharp
+```
diff --git a/CSharpMath.Rendering/Generated Fonts/AMS-Capital-Blackboard-Bold.csmfont b/CSharpMath.Rendering/Generated Fonts/AMS-Capital-Blackboard-Bold.csmfont
new file mode 100644
index 00000000..be2bc290
Binary files /dev/null and b/CSharpMath.Rendering/Generated Fonts/AMS-Capital-Blackboard-Bold.csmfont differ
diff --git a/CSharpMath.Rendering/Generated Fonts/cyrillic-modern-nmr10.csmfont b/CSharpMath.Rendering/Generated Fonts/cyrillic-modern-nmr10.csmfont
new file mode 100644
index 00000000..5d27ab4a
Binary files /dev/null and b/CSharpMath.Rendering/Generated Fonts/cyrillic-modern-nmr10.csmfont differ
diff --git a/CSharpMath.Rendering/Generated Fonts/latinmodern-math.csmfont b/CSharpMath.Rendering/Generated Fonts/latinmodern-math.csmfont
new file mode 100644
index 00000000..3224e014
Binary files /dev/null and b/CSharpMath.Rendering/Generated Fonts/latinmodern-math.csmfont differ
diff --git a/CSharpMath.Rendering/Generated Fonts/manifest.json b/CSharpMath.Rendering/Generated Fonts/manifest.json
new file mode 100644
index 00000000..81ff0455
--- /dev/null
+++ b/CSharpMath.Rendering/Generated Fonts/manifest.json
@@ -0,0 +1,29 @@
+{
+ "schema": 1,
+ "byteOrder": "little",
+ "format": "CSMF1",
+ "shaping": "GSUB/GPOS intentionally excluded; CSharpMath does not shape text with them.",
+ "fonts": [
+ {
+ "Source": "AMS-Capital-Blackboard-Bold.otf",
+ "Output": "AMS-Capital-Blackboard-Bold.csmfont",
+ "SourceSha256": "9578b5b9c86e6ab03846080b9d6fa4f7bc6b3044ac15604b8e7bfd4330295dda",
+ "SourceBytes": 8716,
+ "GeneratedBytes": 5417
+ },
+ {
+ "Source": "cyrillic-modern-nmr10.otf",
+ "Output": "cyrillic-modern-nmr10.csmfont",
+ "SourceSha256": "5b8e360154685a1117e7f93542b89d5263db58f41a40d6df8f131c5fe5032c0c",
+ "SourceBytes": 44292,
+ "GeneratedBytes": 23348
+ },
+ {
+ "Source": "latinmodern-math.otf",
+ "Output": "latinmodern-math.csmfont",
+ "SourceSha256": "6075562b771f8b82f0c179e363389684f2dd09de30038269e2628e504bd7be0f",
+ "SourceBytes": 733736,
+ "GeneratedBytes": 386896
+ }
+ ]
+}
diff --git a/CSharpMath.slnx b/CSharpMath.slnx
index 305791b5..58578246 100644
--- a/CSharpMath.slnx
+++ b/CSharpMath.slnx
@@ -96,6 +96,12 @@
+
+
+
+
+
+