{"record":{"id":"6e34314b2ed31b3f","repo":"d2lang/d2","slug":"expected-ttf-file-but-s-has-extension-s","errorCode":null,"errorMessage":"expected .ttf file but %s has extension %s","messagePattern":"expected \\.ttf file but (.+?) has extension (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"d2cli/main.go","lineNumber":1319,"sourceCode":"\t\tf.AddToOpts(ms.Opts)\n\t\t// Don't pollute the main d2 flagset with these. It'll be a lot\n\t\tms.Opts.Flags.MarkHidden(f.Name)\n\t}\n\n\treturn nil\n}\n\nfunc initPlaywright() error {\n\tpw, err := png.InitPlaywrightWithPrompt()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn pw.Cleanup()\n}\n\nfunc loadFont(ms *xmain.State, path string) ([]byte, error) {\n\tif filepath.Ext(path) != \".ttf\" {\n\t\treturn nil, fmt.Errorf(\"expected .ttf file but %s has extension %s\", path, filepath.Ext(path))\n\t}\n\tttf, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read font at %s: %v\", path, err)\n\t}\n\tms.Log.Info.Printf(\"font %s loaded\", filepath.Base(path))\n\treturn ttf, nil\n}\n\nfunc loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToSemibold, pathToMono, pathToMonoBold, pathToMonoItalic, pathToMonoSemibold string) (*d2fonts.FontFamily, *d2fonts.FontFamily, error) {\n\tif pathToRegular == \"\" && pathToItalic == \"\" && pathToBold == \"\" && pathToSemibold == \"\" &&\n\t\tpathToMono == \"\" && pathToMonoBold == \"\" && pathToMonoItalic == \"\" && pathToMonoSemibold == \"\" {\n\t\treturn nil, nil, nil\n\t}\n\n\tvar regularTTF []byte\n\tvar italicTTF []byte\n\tvar boldTTF []byte","sourceCodeStart":1301,"sourceCodeEnd":1337,"githubUrl":"https://github.com/d2lang/d2/blob/0d69dca6f532ceaeacd615d35d1eaa41a238ffdb/d2cli/main.go#L1301-L1337","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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`)"],"exampleFix":"// before\nd2 --font-regular=Inter.otf sketch.d2\n// after\nd2 --font-regular=Inter-Regular.ttf sketch.d2","handlingStrategy":"validation","validationCode":"func validFont(path string) bool {\n\treturn filepath.Ext(strings.ToLower(path)) == \".ttf\"\n}\n// check before invoking: if !validFont(p) { convert or error }","typeGuard":"func isTTFPath(path string) bool {\n\treturn strings.EqualFold(filepath.Ext(path), \".ttf\")\n}","tryCatchPattern":"ttf, err := loadFont(ms, path)\nif err != nil {\n\tvar extErr = err // inspect message for extension mismatch\n\tms.Log.Error.Print(err)\n\treturn fmt.Errorf(\"convert %s to TTF first\", path)\n}","preventionTips":["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)"],"tags":["cli","fonts","validation"],"backgroundTag":"unsupported-font-format","analyzedSha":"0d69dca6f532ceaeacd615d35d1eaa41a238ffdb","analyzedAt":"2026-08-31T12:19:21.182Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}