diff --git a/d2cli/main.go b/d2cli/main.go index dedba22b0..01bcd2522 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -89,6 +89,10 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { if err != nil { return err } + fontFlag := ms.Opts.String("FONT", "font", "", "", "the font used to render text (default \"Source Sans Pro\"") + if err != nil { + return err + } ps, err := d2plugin.ListPlugins(ctx) if err != nil { @@ -249,6 +253,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { bundle: *bundleFlag, forceAppendix: *forceAppendixFlag, pw: pw, + font: *fontFlag, }) if err != nil { return err @@ -259,7 +264,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { ctx, cancel := context.WithTimeout(ctx, time.Minute*2) defer cancel() - _, written, err := compile(ctx, ms, plugin, *sketchFlag, *centerFlag, *padFlag, *themeFlag, darkThemeFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page) + _, written, err := compile(ctx, ms, plugin, *sketchFlag, *centerFlag, *padFlag, *themeFlag, darkThemeFlag, inputPath, outputPath, *bundleFlag, *forceAppendixFlag, pw.Page, *fontFlag) if err != nil { if written { return fmt.Errorf("failed to fully compile (partial render written): %w", err) @@ -269,13 +274,19 @@ func Run(ctx context.Context, ms *xmain.State) (err error) { return nil } -func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page) (_ []byte, written bool, _ error) { +func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch, center bool, pad, themeID int64, darkThemeID *int64, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, font string) (_ []byte, written bool, _ error) { start := time.Now() input, err := ms.ReadPath(inputPath) if err != nil { return nil, false, err } + // load custom fonts before initializing ruler + fontFamily, err := d2fonts.AddFont(font) + if err != nil { + return nil, false, err + } + ruler, err := textmeasure.NewRuler() if err != nil { return nil, false, err @@ -289,6 +300,8 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc } if sketch { opts.FontFamily = go2.Pointer(d2fonts.HandDrawn) + } else if fontFamily != "" { + opts.FontFamily = go2.Pointer(fontFamily) } cancel := background.Repeat(func() { diff --git a/d2cli/watch.go b/d2cli/watch.go index f9cfb9e81..2d9207ed8 100644 --- a/d2cli/watch.go +++ b/d2cli/watch.go @@ -53,6 +53,7 @@ type watcherOpts struct { bundle bool forceAppendix bool pw png.Playwright + font string } type watcher struct { @@ -360,7 +361,7 @@ func (w *watcher) compileLoop(ctx context.Context) error { w.pw = newPW } - svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.center, w.pad, w.themeID, w.darkThemeID, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page) + svg, _, err := compile(ctx, w.ms, w.layoutPlugin, w.sketch, w.center, w.pad, w.themeID, w.darkThemeID, w.inputPath, w.outputPath, w.bundle, w.forceAppendix, w.pw.Page, w.font) errs := "" if err != nil { if len(svg) > 0 { diff --git a/d2renderers/d2fonts/d2fonts.go b/d2renderers/d2fonts/d2fonts.go index d42908a13..28ddc303b 100644 --- a/d2renderers/d2fonts/d2fonts.go +++ b/d2renderers/d2fonts/d2fonts.go @@ -6,7 +6,13 @@ package d2fonts import ( "embed" + "encoding/base64" + "fmt" + "os" + "path/filepath" "strings" + + fontlib "oss.terrastruct.com/d2/lib/font" ) type FontFamily string @@ -212,6 +218,43 @@ func init() { }] = b } +func AddFont(fontLoc string) (FontFamily, error) { + if fontLoc == "" { + return "", nil + } + fontBuf, err := os.ReadFile(fontLoc) + if err != nil { + return "", fmt.Errorf("failed to read font: %v", err) + } + + splitFont := strings.Split(fontLoc, "/") + fontFileName := splitFont[len(splitFont)-1] + fontName := strings.TrimSuffix(fontFileName, filepath.Ext(fontFileName)) + fontFamily := FontFamily(fontName) + woffFont, err := fontlib.Sfnt2Woff(fontBuf) + if err != nil { + return "", fmt.Errorf("failed to convert to woff font: %v", err) + } + + encodedWoff := fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(woffFont)) + + // Encode all styles for now + customFont := fontFamily.Font(0, FONT_STYLE_REGULAR) + FontFaces[customFont] = fontBuf + FontEncodings[customFont] = encodedWoff + + customFont = fontFamily.Font(0, FONT_STYLE_BOLD) + FontFaces[customFont] = fontBuf + FontEncodings[customFont] = encodedWoff + + customFont = fontFamily.Font(0, FONT_STYLE_ITALIC) + FontFaces[customFont] = fontBuf + FontEncodings[customFont] = encodedWoff + + FontFamilies = append(FontFamilies, fontFamily) + return fontFamily, nil +} + var D2_FONT_TO_FAMILY = map[string]FontFamily{ "default": SourceSansPro, "mono": SourceCodePro,