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 []byte

View on GitHub (pinned to 0d69dca6f5)

Solutions

  1. Convert the font to TTF format (e.g. with fontforge or an online otf-to-ttf converter) and pass the .ttf file
  2. Download the TTF variant of the font instead of otf/woff
  3. 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

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


AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31). Data as JSON: /api/errors/6e34314b2ed31b3f. Report an issue: GitHub.