...

Source file src/oss.terrastruct.com/d2/d2renderers/d2fonts/d2fonts.go

Documentation: oss.terrastruct.com/d2/d2renderers/d2fonts

     1  // d2fonts holds fonts for renderings
     2  
     3  // TODO write a script to do this as part of CI
     4  // Currently using an online converter: https://dopiaza.org/tools/datauri/index.php
     5  package d2fonts
     6  
     7  import (
     8  	"embed"
     9  	"encoding/base64"
    10  	"fmt"
    11  	"strings"
    12  	"sync"
    13  
    14  	"oss.terrastruct.com/d2/lib/font"
    15  	fontlib "oss.terrastruct.com/d2/lib/font"
    16  	"oss.terrastruct.com/d2/lib/syncmap"
    17  )
    18  
    19  type FontFamily string
    20  type FontStyle string
    21  
    22  type Font struct {
    23  	Family FontFamily
    24  	Style  FontStyle
    25  	Size   int
    26  }
    27  
    28  func (f FontFamily) Font(size int, style FontStyle) Font {
    29  	return Font{
    30  		Family: f,
    31  		Style:  style,
    32  		Size:   size,
    33  	}
    34  }
    35  
    36  func (f Font) GetEncodedSubset(corpus string) string {
    37  	var uniqueChars string
    38  	uniqueMap := make(map[rune]bool)
    39  	for _, char := range corpus {
    40  		if _, exists := uniqueMap[char]; !exists {
    41  			uniqueMap[char] = true
    42  			uniqueChars = uniqueChars + string(char)
    43  		}
    44  	}
    45  
    46  	FontFamiliesMu.Lock()
    47  	defer FontFamiliesMu.Unlock()
    48  	face := FontFaces.Get(f)
    49  	fontBuf := make([]byte, len(face))
    50  	copy(fontBuf, face)
    51  	fontBuf = font.UTF8CutFont(fontBuf, uniqueChars)
    52  
    53  	fontBuf, err := fontlib.Sfnt2Woff(fontBuf)
    54  	if err != nil {
    55  		// If subset fails, return full encoding
    56  		return FontEncodings.Get(f)
    57  	}
    58  
    59  	return fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(fontBuf))
    60  }
    61  
    62  const (
    63  	FONT_SIZE_XS   = 13
    64  	FONT_SIZE_S    = 14
    65  	FONT_SIZE_M    = 16
    66  	FONT_SIZE_L    = 20
    67  	FONT_SIZE_XL   = 24
    68  	FONT_SIZE_XXL  = 28
    69  	FONT_SIZE_XXXL = 32
    70  
    71  	FONT_STYLE_REGULAR  FontStyle = "regular"
    72  	FONT_STYLE_BOLD     FontStyle = "bold"
    73  	FONT_STYLE_SEMIBOLD FontStyle = "semibold"
    74  	FONT_STYLE_ITALIC   FontStyle = "italic"
    75  
    76  	SourceSansPro FontFamily = "SourceSansPro"
    77  	SourceCodePro FontFamily = "SourceCodePro"
    78  	HandDrawn     FontFamily = "HandDrawn"
    79  )
    80  
    81  var FontSizes = []int{
    82  	FONT_SIZE_XS,
    83  	FONT_SIZE_S,
    84  	FONT_SIZE_M,
    85  	FONT_SIZE_L,
    86  	FONT_SIZE_XL,
    87  	FONT_SIZE_XXL,
    88  	FONT_SIZE_XXXL,
    89  }
    90  
    91  var FontStyles = []FontStyle{
    92  	FONT_STYLE_REGULAR,
    93  	FONT_STYLE_BOLD,
    94  	FONT_STYLE_SEMIBOLD,
    95  	FONT_STYLE_ITALIC,
    96  }
    97  
    98  var FontFamilies = []FontFamily{
    99  	SourceSansPro,
   100  	SourceCodePro,
   101  	HandDrawn,
   102  }
   103  
   104  var FontFamiliesMu sync.Mutex
   105  
   106  //go:embed encoded/SourceSansPro-Regular.txt
   107  var sourceSansProRegularBase64 string
   108  
   109  //go:embed encoded/SourceSansPro-Bold.txt
   110  var sourceSansProBoldBase64 string
   111  
   112  //go:embed encoded/SourceSansPro-Semibold.txt
   113  var sourceSansProSemiboldBase64 string
   114  
   115  //go:embed encoded/SourceSansPro-Italic.txt
   116  var sourceSansProItalicBase64 string
   117  
   118  //go:embed encoded/SourceCodePro-Regular.txt
   119  var sourceCodeProRegularBase64 string
   120  
   121  //go:embed encoded/SourceCodePro-Bold.txt
   122  var sourceCodeProBoldBase64 string
   123  
   124  //go:embed encoded/SourceCodePro-Semibold.txt
   125  var sourceCodeProSemiboldBase64 string
   126  
   127  //go:embed encoded/SourceCodePro-Italic.txt
   128  var sourceCodeProItalicBase64 string
   129  
   130  //go:embed encoded/FuzzyBubbles-Regular.txt
   131  var fuzzyBubblesRegularBase64 string
   132  
   133  //go:embed encoded/FuzzyBubbles-Bold.txt
   134  var fuzzyBubblesBoldBase64 string
   135  
   136  //go:embed ttf/*
   137  var fontFacesFS embed.FS
   138  
   139  var FontEncodings syncmap.SyncMap[Font, string]
   140  var FontFaces syncmap.SyncMap[Font, []byte]
   141  
   142  func init() {
   143  	FontEncodings = syncmap.New[Font, string]()
   144  
   145  	FontEncodings.Set(
   146  		Font{
   147  			Family: SourceSansPro,
   148  			Style:  FONT_STYLE_REGULAR,
   149  		},
   150  		sourceSansProRegularBase64)
   151  
   152  	FontEncodings.Set(
   153  		Font{
   154  			Family: SourceSansPro,
   155  			Style:  FONT_STYLE_BOLD,
   156  		},
   157  		sourceSansProBoldBase64)
   158  
   159  	FontEncodings.Set(
   160  		Font{
   161  			Family: SourceSansPro,
   162  			Style:  FONT_STYLE_SEMIBOLD,
   163  		},
   164  		sourceSansProSemiboldBase64)
   165  
   166  	FontEncodings.Set(
   167  		Font{
   168  			Family: SourceSansPro,
   169  			Style:  FONT_STYLE_ITALIC,
   170  		},
   171  		sourceSansProItalicBase64)
   172  
   173  	FontEncodings.Set(
   174  		Font{
   175  			Family: SourceCodePro,
   176  			Style:  FONT_STYLE_REGULAR,
   177  		},
   178  		sourceCodeProRegularBase64)
   179  
   180  	FontEncodings.Set(
   181  		Font{
   182  			Family: SourceCodePro,
   183  			Style:  FONT_STYLE_BOLD,
   184  		},
   185  		sourceCodeProBoldBase64)
   186  
   187  	FontEncodings.Set(
   188  		Font{
   189  			Family: SourceCodePro,
   190  			Style:  FONT_STYLE_SEMIBOLD,
   191  		},
   192  		sourceCodeProSemiboldBase64)
   193  
   194  	FontEncodings.Set(
   195  		Font{
   196  			Family: SourceCodePro,
   197  			Style:  FONT_STYLE_ITALIC,
   198  		},
   199  		sourceCodeProItalicBase64)
   200  
   201  	FontEncodings.Set(
   202  		Font{
   203  			Family: HandDrawn,
   204  			Style:  FONT_STYLE_REGULAR,
   205  		},
   206  		fuzzyBubblesRegularBase64)
   207  
   208  	FontEncodings.Set(
   209  		Font{
   210  			Family: HandDrawn,
   211  			Style:  FONT_STYLE_ITALIC,
   212  			// This font has no italic, so just reuse regular
   213  		}, fuzzyBubblesRegularBase64)
   214  	FontEncodings.Set(
   215  		Font{
   216  			Family: HandDrawn,
   217  			Style:  FONT_STYLE_BOLD,
   218  		}, fuzzyBubblesBoldBase64)
   219  	FontEncodings.Set(
   220  		Font{
   221  			Family: HandDrawn,
   222  			Style:  FONT_STYLE_SEMIBOLD,
   223  			// This font has no semibold, so just reuse bold
   224  		}, fuzzyBubblesBoldBase64)
   225  
   226  	FontEncodings.Range(func(k Font, v string) bool {
   227  		FontEncodings.Set(k, strings.TrimSuffix(v, "\n"))
   228  		return true
   229  	})
   230  
   231  	FontFaces = syncmap.New[Font, []byte]()
   232  
   233  	b, err := fontFacesFS.ReadFile("ttf/SourceSansPro-Regular.ttf")
   234  	if err != nil {
   235  		panic(err)
   236  	}
   237  	FontFaces.Set(Font{
   238  		Family: SourceSansPro,
   239  		Style:  FONT_STYLE_REGULAR,
   240  	}, b)
   241  
   242  	b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Regular.ttf")
   243  	if err != nil {
   244  		panic(err)
   245  	}
   246  	FontFaces.Set(Font{
   247  		Family: SourceCodePro,
   248  		Style:  FONT_STYLE_REGULAR,
   249  	}, b)
   250  
   251  	b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Bold.ttf")
   252  	if err != nil {
   253  		panic(err)
   254  	}
   255  	FontFaces.Set(Font{
   256  		Family: SourceCodePro,
   257  		Style:  FONT_STYLE_BOLD,
   258  	}, b)
   259  
   260  	b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Semibold.ttf")
   261  	if err != nil {
   262  		panic(err)
   263  	}
   264  	FontFaces.Set(Font{
   265  		Family: SourceCodePro,
   266  		Style:  FONT_STYLE_SEMIBOLD,
   267  	}, b)
   268  
   269  	b, err = fontFacesFS.ReadFile("ttf/SourceCodePro-Italic.ttf")
   270  	if err != nil {
   271  		panic(err)
   272  	}
   273  	FontFaces.Set(Font{
   274  		Family: SourceCodePro,
   275  		Style:  FONT_STYLE_ITALIC,
   276  	}, b)
   277  
   278  	b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Bold.ttf")
   279  	if err != nil {
   280  		panic(err)
   281  	}
   282  	FontFaces.Set(Font{
   283  		Family: SourceSansPro,
   284  		Style:  FONT_STYLE_BOLD,
   285  	}, b)
   286  
   287  	b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Semibold.ttf")
   288  	if err != nil {
   289  		panic(err)
   290  	}
   291  	FontFaces.Set(Font{
   292  		Family: SourceSansPro,
   293  		Style:  FONT_STYLE_SEMIBOLD,
   294  	}, b)
   295  
   296  	b, err = fontFacesFS.ReadFile("ttf/SourceSansPro-Italic.ttf")
   297  	if err != nil {
   298  		panic(err)
   299  	}
   300  	FontFaces.Set(Font{
   301  		Family: SourceSansPro,
   302  		Style:  FONT_STYLE_ITALIC,
   303  	}, b)
   304  
   305  	b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Regular.ttf")
   306  	if err != nil {
   307  		panic(err)
   308  	}
   309  	FontFaces.Set(Font{
   310  		Family: HandDrawn,
   311  		Style:  FONT_STYLE_REGULAR,
   312  	}, b)
   313  	FontFaces.Set(Font{
   314  		Family: HandDrawn,
   315  		Style:  FONT_STYLE_ITALIC,
   316  	}, b)
   317  
   318  	b, err = fontFacesFS.ReadFile("ttf/FuzzyBubbles-Bold.ttf")
   319  	if err != nil {
   320  		panic(err)
   321  	}
   322  	FontFaces.Set(Font{
   323  		Family: HandDrawn,
   324  		Style:  FONT_STYLE_BOLD,
   325  	}, b)
   326  	FontFaces.Set(Font{
   327  		Family: HandDrawn,
   328  		Style:  FONT_STYLE_SEMIBOLD,
   329  	}, b)
   330  }
   331  
   332  var D2_FONT_TO_FAMILY = map[string]FontFamily{
   333  	"default": SourceSansPro,
   334  	"mono":    SourceCodePro,
   335  }
   336  
   337  func AddFontStyle(font Font, style FontStyle, ttf []byte) error {
   338  	FontFaces.Set(font, ttf)
   339  
   340  	woff, err := fontlib.Sfnt2Woff(ttf)
   341  	if err != nil {
   342  		return fmt.Errorf("failed to encode ttf to woff: %v", err)
   343  	}
   344  	encodedWoff := fmt.Sprintf("data:application/font-woff;base64,%v", base64.StdEncoding.EncodeToString(woff))
   345  	FontEncodings.Set(font, encodedWoff)
   346  
   347  	return nil
   348  }
   349  
   350  func AddFontFamily(name string, regularTTF, italicTTF, boldTTF, semiboldTTF []byte) (*FontFamily, error) {
   351  	FontFamiliesMu.Lock()
   352  	defer FontFamiliesMu.Unlock()
   353  	customFontFamily := FontFamily(name)
   354  
   355  	regularFont := Font{
   356  		Family: customFontFamily,
   357  		Style:  FONT_STYLE_REGULAR,
   358  	}
   359  	if regularTTF != nil {
   360  		err := AddFontStyle(regularFont, FONT_STYLE_REGULAR, regularTTF)
   361  		if err != nil {
   362  			return nil, err
   363  		}
   364  	} else {
   365  		fallbackFont := Font{
   366  			Family: SourceSansPro,
   367  			Style:  FONT_STYLE_REGULAR,
   368  		}
   369  		FontFaces.Set(regularFont, FontFaces.Get(fallbackFont))
   370  		FontEncodings.Set(regularFont, FontEncodings.Get(fallbackFont))
   371  	}
   372  
   373  	italicFont := Font{
   374  		Family: customFontFamily,
   375  		Style:  FONT_STYLE_ITALIC,
   376  	}
   377  	if italicTTF != nil {
   378  		err := AddFontStyle(italicFont, FONT_STYLE_ITALIC, italicTTF)
   379  		if err != nil {
   380  			return nil, err
   381  		}
   382  	} else {
   383  		fallbackFont := Font{
   384  			Family: SourceSansPro,
   385  			Style:  FONT_STYLE_ITALIC,
   386  		}
   387  		FontFaces.Set(italicFont, FontFaces.Get(fallbackFont))
   388  		FontEncodings.Set(italicFont, FontEncodings.Get(fallbackFont))
   389  	}
   390  
   391  	boldFont := Font{
   392  		Family: customFontFamily,
   393  		Style:  FONT_STYLE_BOLD,
   394  	}
   395  	if boldTTF != nil {
   396  		err := AddFontStyle(boldFont, FONT_STYLE_BOLD, boldTTF)
   397  		if err != nil {
   398  			return nil, err
   399  		}
   400  	} else {
   401  		fallbackFont := Font{
   402  			Family: SourceSansPro,
   403  			Style:  FONT_STYLE_BOLD,
   404  		}
   405  		FontFaces.Set(boldFont, FontFaces.Get(fallbackFont))
   406  		FontEncodings.Set(boldFont, FontEncodings.Get(fallbackFont))
   407  	}
   408  
   409  	semiboldFont := Font{
   410  		Family: customFontFamily,
   411  		Style:  FONT_STYLE_SEMIBOLD,
   412  	}
   413  	if semiboldTTF != nil {
   414  		err := AddFontStyle(semiboldFont, FONT_STYLE_SEMIBOLD, semiboldTTF)
   415  		if err != nil {
   416  			return nil, err
   417  		}
   418  	} else {
   419  		fallbackFont := Font{
   420  			Family: SourceSansPro,
   421  			Style:  FONT_STYLE_SEMIBOLD,
   422  		}
   423  		FontFaces.Set(semiboldFont, FontFaces.Get(fallbackFont))
   424  		FontEncodings.Set(semiboldFont, FontEncodings.Get(fallbackFont))
   425  	}
   426  
   427  	FontFamilies = append(FontFamilies, customFontFamily)
   428  
   429  	return &customFontFamily, nil
   430  }
   431  

View as plain text