d2lang/d2 · error
expected .ttf file but %s has extension %s
Error message
expected .ttf file but %s has extension %s
What it means
loadFont validates that a font path points to a .ttf (TrueType) file before reading it. D2's renderer only supports TTF font files, so any other extension (otf, woff, woff2, etc.) is rejected up front with the path and its actual extension in the message.
Source
Thrown at d2cli/main.go:1319
f.AddToOpts(ms.Opts)
// Don't pollute the main d2 flagset with these. It'll be a lot
ms.Opts.Flags.MarkHidden(f.Name)
}
return nil
}
func initPlaywright() error {
pw, err := png.InitPlaywrightWithPrompt()
if err != nil {
return err
}
return pw.Cleanup()
}
func loadFont(ms *xmain.State, path string) ([]byte, error) {
if filepath.Ext(path) != ".ttf" {
return nil, fmt.Errorf("expected .ttf file but %s has extension %s", path, filepath.Ext(path))
}
ttf, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read font at %s: %v", path, err)
}
ms.Log.Info.Printf("font %s loaded", filepath.Base(path))
return ttf, nil
}
func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToSemibold, pathToMono, pathToMonoBold, pathToMonoItalic, pathToMonoSemibold string) (*d2fonts.FontFamily, *d2fonts.FontFamily, error) {
if pathToRegular == "" && pathToItalic == "" && pathToBold == "" && pathToSemibold == "" &&
pathToMono == "" && pathToMonoBold == "" && pathToMonoItalic == "" && pathToMonoSemibold == "" {
return nil, nil, nil
}
var regularTTF []byte
var italicTTF []byte
var boldTTF []byteView on GitHub (pinned to 0d69dca6f5)
Solutions
- Convert the font to TTF format (e.g. with fontforge or an online otf-to-ttf converter) and pass the .ttf file
- Download the TTF variant of the font instead of otf/woff
- Rename the file to have a lowercase .ttf extension only if it truly is TTF content (check with `file font.ttf`)
Example fix
// before d2 --font-regular=Inter.otf sketch.d2 // after d2 --font-regular=Inter-Regular.ttf sketch.d2
Defensive patterns
Strategy: validation
Validate before calling
func validFont(path string) bool {
return filepath.Ext(strings.ToLower(path)) == ".ttf"
}
// check before invoking: if !validFont(p) { convert or error } Type guard
func isTTFPath(path string) bool {
return strings.EqualFold(filepath.Ext(path), ".ttf")
} Try / catch
ttf, err := loadFont(ms, path)
if err != nil {
var extErr = err // inspect message for extension mismatch
ms.Log.Error.Print(err)
return fmt.Errorf("convert %s to TTF first", path)
} Prevention
- Only pass .ttf files to the --font-* flags
- Convert otf/woff fonts to ttf ahead of time
- Use lowercase .ttf extensions (extension check is case-sensitive)
When it happens
Trigger: Calling loadFont (via loadFonts, e.g. from --font-regular and related CLI flags) with a path whose filepath.Ext() is not exactly ".ttf" — e.g. an .otf font, a .woff2 web font, or a path with no extension.
Common situations: Users pass a system font like Arial.otf or a downloaded web font (woff/woff2) to the font family flags; also happens with font files downloaded without extensions or with uppercase extensions like .TTF (filepath.Ext comparison is case-sensitive).
Related errors
- %s is not a supported format. Supported formats are: %s
- render target "%s" not found
- failed to read font at %s: %v
- expected "opacity" to be a number between 0.0 and 1.0
- expected "stroke" to be a valid named color ("orange"), a he
AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31).
Data as JSON: /api/errors/6e34314b2ed31b3f.
Report an issue: GitHub.