diff --git a/internal/modules/misc/fonts/dejavu-sans-license.txt b/internal/modules/misc/fonts/dejavu-sans-license.txt new file mode 100644 index 0000000..ecf22a0 --- /dev/null +++ b/internal/modules/misc/fonts/dejavu-sans-license.txt @@ -0,0 +1,47 @@ +DejaVu Sans font license + +Source: https://dejavu-fonts.github.io/ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. +Bitstream Vera is a trademark of Bitstream, Inc. +DejaVu changes are in public domain. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and distributed under the "Bitstream Vera" +names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. diff --git a/internal/modules/misc/fonts/dejavu-sans.ttf b/internal/modules/misc/fonts/dejavu-sans.ttf new file mode 100644 index 0000000..fb0bd94 Binary files /dev/null and b/internal/modules/misc/fonts/dejavu-sans.ttf differ diff --git a/internal/modules/misc/handlers_test.go b/internal/modules/misc/handlers_test.go index ff6899e..aacc065 100644 --- a/internal/modules/misc/handlers_test.go +++ b/internal/modules/misc/handlers_test.go @@ -425,6 +425,32 @@ func TestWheelOfNamesBeta_RotatesOptionLabelsWithSlices(t *testing.T) { } } +func TestWheelOfNamesBeta_DisplayTextPreservesVietnamese(t *testing.T) { + input := "Tiếng Việt Đặng" + got := wheelBetaDisplayText(input, 32) + if got != input { + t.Fatalf("wheelBetaDisplayText() = %q, want %q", got, input) + } + if strings.Contains(got, "?") { + t.Fatalf("wheelBetaDisplayText() replaced Vietnamese with ?: %q", got) + } +} + +func TestWheelOfNamesBeta_FontSupportsVietnameseGlyphs(t *testing.T) { + for _, r := range "Tiếng Việt Đặng Ơ Ư ấ ệ" { + if r == ' ' { + continue + } + glyphIndex, err := wheelBetaTextFont.GlyphIndex(nil, r) + if err != nil { + t.Fatalf("GlyphIndex(%q): %v", r, err) + } + if glyphIndex == 0 { + t.Fatalf("GlyphIndex(%q) = 0, want Vietnamese glyph support", r) + } + } +} + func countColorIndex(img *image.Paletted, bounds image.Rectangle, colorIndex byte) int { bounds = bounds.Intersect(img.Bounds()) count := 0 diff --git a/internal/modules/misc/wheelofnames_beta.go b/internal/modules/misc/wheelofnames_beta.go index 314870e..fd0bc8a 100644 --- a/internal/modules/misc/wheelofnames_beta.go +++ b/internal/modules/misc/wheelofnames_beta.go @@ -140,10 +140,10 @@ func renderWheelBetaFrameWithCelebration(options []string, winner int, rotation if label == "" { label = "CURRENT" } - value := asciiWheelText(options[currentIndex], 28) + value := wheelBetaDisplayText(options[currentIndex], 28) if reveal { label = "RESULT" - value = asciiWheelText(options[winner], 28) + value = wheelBetaDisplayText(options[winner], 28) } drawStatusBand(img, label, value) return img diff --git a/internal/modules/misc/wheelofnames_beta_drawing.go b/internal/modules/misc/wheelofnames_beta_drawing.go index 93cd74e..78e9e8a 100644 --- a/internal/modules/misc/wheelofnames_beta_drawing.go +++ b/internal/modules/misc/wheelofnames_beta_drawing.go @@ -1,20 +1,49 @@ package misc import ( + _ "embed" "image" "image/color" "math" "strings" + "unicode" "golang.org/x/image/font" - "golang.org/x/image/font/basicfont" + "golang.org/x/image/font/opentype" "golang.org/x/image/math/fixed" ) const ( wheelBetaSliceLabelRadius = 64 + wheelBetaFontSize = 10 + wheelBetaFontDPI = 72 ) +//go:embed fonts/dejavu-sans.ttf +var wheelBetaTextFontBytes []byte + +var wheelBetaTextFont = mustWheelBetaTextFont() + +func mustWheelBetaTextFont() *opentype.Font { + parsed, err := opentype.Parse(wheelBetaTextFontBytes) + if err != nil { + panic(err) + } + return parsed +} + +func newWheelBetaTextFace() font.Face { + face, err := opentype.NewFace(wheelBetaTextFont, &opentype.FaceOptions{ + Size: wheelBetaFontSize, + DPI: wheelBetaFontDPI, + Hinting: font.HintingFull, + }) + if err != nil { + panic(err) + } + return face +} + func drawWheelSliceLabels(img *image.Paletted, options []string, rotation float64) { if len(options) == 0 { return @@ -26,7 +55,7 @@ func drawWheelSliceLabels(img *image.Paletted, options []string, rotation float6 angle := rotation + (float64(idx)+0.5)*segment centerX := cx + int(math.Round(math.Cos(angle)*float64(wheelBetaSliceLabelRadius))) centerY := cy + int(math.Round(math.Sin(angle)*float64(wheelBetaSliceLabelRadius))) - text := asciiWheelText(option, limit) + text := wheelBetaDisplayText(option, limit) drawRotatedCenteredTextAt(img, text, centerX+1, centerY+1, angle, wheelBetaPaperColorIndex) drawRotatedCenteredTextAt(img, text, centerX, centerY, angle, wheelBetaInkColorIndex) } @@ -203,7 +232,10 @@ func drawCenteredText(img *image.Paletted, text string, baselineY int, colorInde } func drawCenteredTextAt(img *image.Paletted, text string, centerX, baselineY int, colorIndex byte) { - face := basicfont.Face7x13 + face := newWheelBetaTextFace() + defer func() { + _ = face.Close() + }() width := font.MeasureString(face, text).Ceil() x := centerX - width/2 drawer := font.Drawer{ @@ -216,7 +248,10 @@ func drawCenteredTextAt(img *image.Paletted, text string, centerX, baselineY int } func drawRotatedCenteredTextAt(img *image.Paletted, text string, centerX, centerY int, angle float64, colorIndex byte) { - face := basicfont.Face7x13 + face := newWheelBetaTextFace() + defer func() { + _ = face.Close() + }() padding := 2 metrics := face.Metrics() textWidth := font.MeasureString(face, text).Ceil() @@ -247,17 +282,20 @@ func drawRotatedCenteredTextAt(img *image.Paletted, text string, centerX, center } } -func asciiWheelText(s string, limit int) string { +func wheelBetaDisplayText(s string, limit int) string { var b strings.Builder + count := 0 for _, r := range s { - if b.Len() >= limit { + if count >= limit { break } - if r < 32 || r > 126 { + if unicode.IsControl(r) { b.WriteByte('?') + count++ continue } b.WriteRune(r) + count++ } out := strings.TrimSpace(b.String()) if out == "" {