diff --git a/internal/card/card_test.go b/internal/card/card_test.go index dc770f93..baee9681 100644 --- a/internal/card/card_test.go +++ b/internal/card/card_test.go @@ -290,6 +290,8 @@ func adversarialProfile() *github.Profile { p := &github.Profile{ Login: "user-with-a-very-long-login-name", Name: "A Very Long Display Name That Keeps Going", + UTCOffsetLabel: "UTC+12.75", // half-hour / quarter-hour zones widen the title + Company: "A-Company-With-An-Unusually-Long-Name Pty Ltd", Location: "A Place With A Name That Is Way Too Long To Fit", Website: "https://example-with-a-very-long-domain.example.com/profile", diff --git a/internal/card/svg.go b/internal/card/svg.go index ff0dbeee..dcd3ec63 100644 --- a/internal/card/svg.go +++ b/internal/card/svg.go @@ -3,6 +3,7 @@ package card import ( "fmt" "strings" + "unicode/utf8" ) // escapeXML replaces the five XML-significant characters so user-controlled @@ -65,14 +66,43 @@ func formatInt(n int) string { return out } -// header returns the opening tag + background rect + title text. +// header returns the opening tag + background rect + title text. The +// title font-size is auto-shrunk if the string wouldn't fit on one line at +// the default 15 px — productive-time and productive-weekday append the +// timezone label ("UTC+7.00") which can push the title to ~40 chars, past +// the 340 px frame at 15 px. Floor at 11 px so small zones stay readable. func header(width, height int, bg, stroke string, strokeOpacity float64, titleColor, title string) string { + fontSize := fitTitleFontSize(title, width) return fmt.Sprintf(` - %s`, + %s`, width, height, width, height, width-1, height-1, bg, stroke, strokeOpacity, - titleColor, escapeXML(title)) + fontSize, titleColor, escapeXML(title)) +} + +// fitTitleFontSize picks the largest title font (between 11 and 15 px) at +// which the title still fits in width − leftInset − rightSafety. Uses the +// same 0.6 × font-size char-width estimate as the fit-the-frame test. +func fitTitleFontSize(title string, width int) int { + const ( + leftInset = 20 + rightSafety = 4 + minFont = 11 + maxFont = 15 + avgCharRatio = 0.6 + ) + budget := float64(width - leftInset - rightSafety) + chars := utf8.RuneCountInString(title) + if chars == 0 { + return maxFont + } + for fs := maxFont; fs >= minFont; fs-- { + if float64(chars)*float64(fs)*avgCharRatio <= budget { + return fs + } + } + return minFont } const footer = `