package main import ( "embed" "flag" "image/png" "log" "github.com/gdamore/tcell/v2" "golang.org/x/text/encoding" "golang.org/x/text/encoding/japanese" ) const ( // Bit width of half-width characters. bitsHW = 32 // Glyph width of half-width characters. glyphWidthHW = 4 // Glyph height of half-width characters. glyphHeightHW = 8 // Bit width of full-width characters. bitsFW = 64 // Glyph width of full-width characters. glyphWidthFW = 8 // Glyph height of full-width characters. glyphHeightFW = 8 fw1FirstByteStart = 0x81 fw1FirstByteEnd = 0x9F fw2FirstByteStart = 0xE0 fw2FirstByteEnd = 0xEF fwSecondByteStart = 0x40 fwSecondByteEnd = 0xFC // There is no character whose 2nd byte is 0x7F. fwSecondUnmapped = 0x7F ) //go:embed assets/*.png var fontFiles embed.FS // One glyph for half-width characters. type GlyphHW uint32 // One glyph for full-width characters. type GlyphFW uint64 type Font struct { // Half-width glyphs. It is keyed by a raw character code. glyphsHW *[256]GlyphHW // Full-width glyphs. glyphsFW1 *[31][189]GlyphFW // Full-width glyphs. glyphsFW2 *[16][189]GlyphFW } type CharClass uint8 const ( charClassHW = iota charClassFW1 charClassFW2 ) // Get character class. func getCharClass(b byte) CharClass { if fw1FirstByteStart <= b && b <= fw1FirstByteEnd { return charClassFW1 } else if fw2FirstByteStart <= b && b <= fw2FirstByteEnd { return charClassFW2 } else { return charClassHW } } func glyphHWToglyphFW(gHW GlyphHW) GlyphFW { gFW := GlyphFW(0) for i := 0; i < bitsHW; i++ { if gHW&(1< squareH*8 { squareW = squareH * 8 } if squareH > squareW { squareH = squareW } xOffsets := make([]int, len(banner)) for i, gridWidth := range gridWidths { xOffsets[i] = (scrW/squareW - gridWidth) / 2 } yOffset := (scrH/squareH - gridHeight) / 2 return squareW, squareH, xOffsets, yOffset } func drawOneLine(r *Renderer, s string, xOffset, yOffset int, font *Font) { for i := 0; i < len(s); i++ { b := s[i] x := xOffset + i*glyphWidthHW y := yOffset var g GlyphFW switch getCharClass(b) { case charClassHW: g = glyphHWToglyphFW(font.glyphsHW[b]) case charClassFW1: b2 := s[i+1] g = font.glyphsFW1[b-fw1FirstByteStart][b2-fwSecondByteStart] i++ case charClassFW2: b2 := s[i+1] g = font.glyphsFW1[b-fw2FirstByteStart][b2-fwSecondByteStart] i++ } drawGlyph(r, g, x, y) } } func drawBanner(r *Renderer, banner Banner, font *Font) { r.ClearScreen() sw, sh, xOffsets, yOffset := calcSquareSizeAndOffset(r, banner) r.SetSquareSize(sw, sh) for i, line := range banner { drawOneLine(r, line, xOffsets[i], yOffset+i*glyphHeightFW, font) } } func parseGlyphsHW(filePath string) (*[256]GlyphHW, error) { fp, err := fontFiles.Open(filePath) if err != nil { return nil, err } defer fp.Close() img, err := png.Decode(fp) if err != nil { return nil, err } gs := [256]GlyphHW{} for dy := 0; dy < 16; dy++ { for dx := 0; dx < 16; dx++ { glyph := GlyphHW(0) for i := 0; i < bitsHW; i++ { x := dx*glyphWidthHW + i%glyphWidthHW y := dy*glyphHeightHW + i/glyphWidthHW r, g, b, _ := img.At(x, y).RGBA() if r == 0 && b == 0 && g == 0 { glyph |= 1 << i } } c := dy*16 + dx gs[c] = glyph } } return &gs, nil } func parseGlyphsFW(filePath string) (*[31][189]GlyphFW, *[16][189]GlyphFW, error) { fp, err := fontFiles.Open(filePath) if err != nil { return nil, nil, err } defer fp.Close() img, err := png.Decode(fp) if err != nil { return nil, nil, err } gs1 := [31][189]GlyphFW{} for dy := 0; dy < 62; dy++ { for dx := 0; dx < 94; dx++ { glyph := GlyphFW(0) for i := 0; i < bitsFW; i++ { x := dx*glyphWidthFW + i%glyphWidthFW y := dy*glyphHeightFW + i/glyphWidthFW r, g, b, _ := img.At(x, y).RGBA() if r == 0 && b == 0 && g == 0 { glyph |= 1 << i } } c1 := dy / 2 c2 := dx + (dy%2)*(fwSecondByteEnd-fwSecondByteStart)/2 if c2 >= fwSecondUnmapped-fwSecondByteStart { c2 += 1 } gs1[c1][c2] = glyph } } yOffset := 31 * glyphHeightFW gs2 := [16][189]GlyphFW{} for dy := 0; dy < 16; dy++ { for dx := 0; dx < 94; dx++ { glyph := GlyphFW(0) for i := 0; i < bitsFW; i++ { x := dx*glyphWidthFW + i%glyphWidthFW y := dy*glyphHeightFW + i/glyphWidthFW + yOffset r, g, b, _ := img.At(x, y).RGBA() if r == 0 && b == 0 && g == 0 { glyph |= 1 << i } } c1 := dy / 2 c2 := dx + (dy%2)*(fwSecondByteEnd-fwSecondByteStart)/2 if c2 >= fwSecondUnmapped-fwSecondByteStart { c2 += 1 } gs2[c1][c2] = glyph } } return &gs1, &gs2, nil } func prepareFont(fileHW, fileFW string) (*Font, error) { glyphsHW, err := parseGlyphsHW(fileHW) if err != nil { return nil, err } glyphsFW1, glyphsFW2, err := parseGlyphsFW(fileFW) if err != nil { return nil, err } return &Font{glyphsHW, glyphsFW1, glyphsFW2}, nil } func main() { var fontType = flag.String("f", "mincho", "Font (mincho or gothic)") flag.Parse() var fontFileHW string var fontFileFW string if *fontType == "mincho" { fontFileHW = "assets/misaki_gothic_2nd_4x8.png" fontFileFW = "assets/misaki_mincho.png" } else if *fontType == "gothic" { fontFileHW = "assets/misaki_gothic_2nd_4x8.png" fontFileFW = "assets/misaki_gothic_2nd.png" } else { log.Fatalf("Unknown font: %s", *fontType) } if flag.NArg() == 0 { return } font, err := prepareFont(fontFileHW, fontFileFW) if err != nil { log.Fatalf("%+v", err) } r, err := NewRenderer( tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset), tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorOlive), ) if err != nil { log.Fatalf("%+v", err) } defer r.Fini() banner, err := NewBanner(flag.Args()) if err != nil { log.Fatalf("%+v", err) } drawBanner(r, banner, font) for { r.Show() ev := r.PollEvent() switch ev := ev.(type) { case *tcell.EventResize: drawBanner(r, banner, font) r.Sync() case *tcell.EventKey: if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC || ev.Rune() == 'q' { return } } } }