fix: embed portable static fonts for CV preview and PDF fidelity
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace JobTrackerApi.Services;
|
||||
|
||||
// Self-contained document fonts keep local-file PDF export and srcDoc preview on the same metrics.
|
||||
internal static class CvDocumentFonts
|
||||
{
|
||||
private static readonly Dictionary<string, string> Styles = new()
|
||||
{
|
||||
["Selawik"] = Faces("selawik", ("selawk.ttf", "400", "normal"), ("selawksb.ttf", "600", "normal"), ("selawkb.ttf", "700", "normal")),
|
||||
["Arimo"] = Faces("arimo", ("Arimo-Regular.ttf", "400", "normal"), ("Arimo-Italic.ttf", "400", "italic"), ("Arimo-Bold.ttf", "700", "normal"), ("Arimo-BoldItalic.ttf", "700", "italic")),
|
||||
["Gelasio"] = Faces("gelasio", ("Gelasio-Regular.ttf", "400", "normal"), ("Gelasio-Italic.ttf", "400", "italic"), ("Gelasio-SemiBold.ttf", "600", "normal"), ("Gelasio-SemiBoldItalic.ttf", "600", "italic"), ("Gelasio-Bold.ttf", "700", "normal"), ("Gelasio-BoldItalic.ttf", "700", "italic")),
|
||||
["Roboto"] = Faces("roboto", ("Roboto-Regular.ttf", "400", "normal"), ("Roboto-Italic.ttf", "400", "italic"), ("Roboto-Medium.ttf", "500", "normal"), ("Roboto-MediumItalic.ttf", "500", "italic"), ("Roboto-Bold.ttf", "700", "normal"), ("Roboto-BoldItalic.ttf", "700", "italic")),
|
||||
["Poppins"] = Faces("poppins", ("Poppins-Regular.ttf", "400", "normal"), ("Poppins-Italic.ttf", "400", "italic"), ("Poppins-SemiBold.ttf", "600", "normal"), ("Poppins-SemiBoldItalic.ttf", "600", "italic"), ("Poppins-Bold.ttf", "700", "normal"), ("Poppins-BoldItalic.ttf", "700", "italic")),
|
||||
["Roboto Mono"] = Faces("robotomono", ("RobotoMono-Regular.ttf", "400", "normal"), ("RobotoMono-Italic.ttf", "400", "italic"), ("RobotoMono-Medium.ttf", "500", "normal"), ("RobotoMono-MediumItalic.ttf", "500", "italic"), ("RobotoMono-Bold.ttf", "700", "normal"), ("RobotoMono-BoldItalic.ttf", "700", "italic")),
|
||||
};
|
||||
|
||||
internal static string Family(string stack) => stack.TrimStart() switch
|
||||
{
|
||||
var s when s.StartsWith("Georgia", StringComparison.Ordinal) => "Gelasio",
|
||||
var s when s.StartsWith("'Roboto Mono'", StringComparison.Ordinal) || s.StartsWith("Consolas", StringComparison.Ordinal) => "Roboto Mono",
|
||||
var s when s.StartsWith("'Roboto'", StringComparison.Ordinal) => "Roboto",
|
||||
var s when s.StartsWith("'Poppins'", StringComparison.Ordinal) => "Poppins",
|
||||
var s when s.StartsWith("'Segoe UI'", StringComparison.Ordinal) => "Selawik",
|
||||
_ => "Arimo",
|
||||
};
|
||||
|
||||
internal static string Stack(string original) => $"'{Family(original)}', {original}";
|
||||
|
||||
internal static string Embed(params string[] stacks) => string.Concat(stacks.Select(Family).Distinct().Select(family => Styles[family]));
|
||||
|
||||
private static string Faces(string family, params (string File, string Weight, string Style)[] faces)
|
||||
{
|
||||
var names = new Dictionary<string, string> { ["selawik"] = "Selawik", ["arimo"] = "Arimo", ["gelasio"] = "Gelasio", ["roboto"] = "Roboto", ["poppins"] = "Poppins", ["robotomono"] = "Roboto Mono" };
|
||||
var css = new StringBuilder();
|
||||
using var license = new StreamReader(Resource(family + ".OFL.txt"));
|
||||
css.Append("/*\n").Append(license.ReadToEnd()).Append("\n*/\n");
|
||||
foreach (var face in faces)
|
||||
{
|
||||
using var stream = Resource(face.File);
|
||||
using var bytes = new MemoryStream();
|
||||
stream.CopyTo(bytes);
|
||||
css.Append($"@font-face{{font-family:'{names[family]}';font-style:{face.Style};font-weight:{face.Weight};font-display:block;src:url(data:font/ttf;base64,{Convert.ToBase64String(bytes.ToArray())}) format('truetype');}}\n");
|
||||
}
|
||||
return css.ToString();
|
||||
}
|
||||
|
||||
private static Stream Resource(string name) => typeof(CvDocumentFonts).Assembly.GetManifestResourceStream("CvFonts." + name)
|
||||
?? throw new InvalidOperationException("Required document font resource is missing: " + name);
|
||||
}
|
||||
@@ -43,7 +43,8 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
|
||||
? SplitColumns(model, theme, sidebarSections, settings)
|
||||
: (string.Empty, RenderSections(model.Sections, theme, settings));
|
||||
|
||||
var css = BuildCss(theme, settings, accent, headerInk, ink, muted, paper, headingColor, headingFont, bodyFont, headingStyle, density, pageDims, layout, twoColumn);
|
||||
var css = CvDocumentFonts.Embed(headingFont, bodyFont, theme.UtilityFont)
|
||||
+ BuildCss(theme, settings, accent, headerInk, ink, muted, paper, headingColor, CvDocumentFonts.Stack(headingFont), CvDocumentFonts.Stack(bodyFont), headingStyle, density, pageDims, layout, twoColumn);
|
||||
var header = RenderHeader(model, theme, showIcons, twoColumn, headerStyle);
|
||||
var body = layout switch
|
||||
{
|
||||
@@ -255,7 +256,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
|
||||
.header-plain{{border-bottom:2px solid {accent};}}";
|
||||
|
||||
var numberedSections = t.NumberSections
|
||||
? $@".main{{counter-reset:cv-section;}} .main>.section>.section-title{{counter-increment:cv-section;font-family:{t.UtilityFont};font-weight:600;}} .main>.section>.section-title::before{{content:counter(cv-section,decimal-leading-zero) ' — ';color:var(--cv-accent-color);}}"
|
||||
? $@".main{{counter-reset:cv-section;}} .main>.section>.section-title{{counter-increment:cv-section;font-family:{CvDocumentFonts.Stack(t.UtilityFont)};font-weight:600;}} .main>.section>.section-title::before{{content:counter(cv-section,decimal-leading-zero) ' — ';color:var(--cv-accent-color);}}"
|
||||
: string.Empty;
|
||||
|
||||
return $@"
|
||||
@@ -304,7 +305,7 @@ h1,h2{{font-family:{headingFont};}}
|
||||
.paragraphs p{{margin:0 0 {F(1.8 * density)}mm;white-space:pre-line;}}
|
||||
.skill-groups{{display:grid;gap:{F(1.7 * density)}mm;}}
|
||||
.skill-group{{display:grid;grid-template-columns:minmax(24mm,.3fr) minmax(0,1fr);gap:3mm;break-inside:avoid-page;page-break-inside:avoid;}}
|
||||
.skill-group-name{{font-family:{t.UtilityFont};font-weight:700;color:var(--cv-accent-color);overflow-wrap:anywhere;}}
|
||||
.skill-group-name{{font-family:{CvDocumentFonts.Stack(t.UtilityFont)};font-weight:700;color:var(--cv-accent-color);overflow-wrap:anywhere;}}
|
||||
.skill-group-items{{display:flex;flex-wrap:wrap;gap:.8mm 3mm;list-style:none;margin:0;padding:0;}}
|
||||
.skill-group-items li{{overflow-wrap:anywhere;}}
|
||||
.entry{{margin-bottom:{entryGap}mm;}}
|
||||
|
||||
Reference in New Issue
Block a user