package bdf import ( "bufio" "encoding/hex" "fmt" "image" "image/color" "image/draw" "io" "strconv" ) // glyph is a singular bitmap glyph to be used as a mask, assumed to directly // correspond to a rune. A zero value is also valid and drawable. type glyph struct { // Coordinates are relative to the origin, on the baseline. // The ascent is thus negative, unlike the usual model. bounds image.Rectangle bitmap []byte advance int } // ColorModel implements image.Image. func (g *glyph) ColorModel() color.Model { return color.Alpha16Model } // Bounds implements image.Image. func (g *glyph) Bounds() image.Rectangle { return g.bounds } // At implements image.Image. This is going to be somewhat slow. func (g *glyph) At(x, y int) color.Color { x -= g.bounds.Min.X y -= g.bounds.Min.Y dx, dy := g.bounds.Dx(), g.bounds.Dy() if x < 0 || y < 0 || x >= dx || y >= dy { return color.Transparent } stride, offset, bit := (dx+7)/8, x/8, byte(1< 0 { tokens = append(tokens, string(token)) token = nil } default: token = append(token, r) } } } if quotes && !escape { return nil, fmt.Errorf("strings may not contain newlines") } if quotes || len(token) > 0 { tokens = append(tokens, string(token)) } return tokens, nil } // ----------------------------------------------------------------------------- // bdfParser is a basic and rather lenient parser of // Bitmap Distribution Format (BDF) files. type bdfParser struct { scanner *bufio.Scanner // input reader line int // current line number tokens []string // tokens on the current line font *Font // glyph storage defaultBounds image.Rectangle defaultAdvance int defaultChar int } // readLine reads the next line and splits it into tokens. // Panics on error, returns false if the end of file has been reached normally. func (p *bdfParser) readLine() bool { p.line++ if !p.scanner.Scan() { if err := p.scanner.Err(); err != nil { panic(err) } p.line-- return false } var err error if p.tokens, err = tokenize(latin1ToUTF8(p.scanner.Bytes())); err != nil { panic(err) } // Eh, it would be nicer iteratively, this may overrun the stack. if len(p.tokens) == 0 { return p.readLine() } return true } func (p *bdfParser) readIntegerArgument() int { if len(p.tokens) < 2 { panic("insufficient arguments") } if i, err := strconv.Atoi(p.tokens[1]); err != nil { panic(err) } else { return i } } // Some fonts even use -1 for things outside the encoding. func (p *bdfParser) readCharEncoding() int { return p.readIntegerArgument() } // XXX: Ignoring vertical advance since we only expect purely horizontal fonts. func (p *bdfParser) readDwidth() int { return p.readIntegerArgument() } func (p *bdfParser) parseProperties() { // The wording in the specification suggests that the argument // with the number of properties to follow isn't reliable. for p.readLine() && p.tokens[0] != "ENDPROPERTIES" { switch p.tokens[0] { case "DEFAULT_CHAR": p.defaultChar = p.readCharEncoding() case "FONT_ASCENT": p.font.Ascent = p.readIntegerArgument() case "FONT_DESCENT": p.font.Descent = p.readIntegerArgument() } } } func (p *bdfParser) readBBX() image.Rectangle { if len(p.tokens) < 5 { panic("insufficient arguments") } w, e1 := strconv.Atoi(p.tokens[1]) h, e2 := strconv.Atoi(p.tokens[2]) x, e3 := strconv.Atoi(p.tokens[3]) y, e4 := strconv.Atoi(p.tokens[4]) if e1 != nil || e2 != nil || e3 != nil || e4 != nil { panic("invalid arguments") } if w < 0 || h < 0 { panic("bounding boxes may not have negative dimensions") } return image.Rectangle{ Min: image.Point{x, -(y + h)}, Max: image.Point{x + w, -y}, } } func (p *bdfParser) parseChar() { g := glyph{bounds: p.defaultBounds, advance: p.defaultAdvance} bitmap, rows, encoding := false, 0, -1 for p.readLine() && p.tokens[0] != "ENDCHAR" { if bitmap { b, err := hex.DecodeString(p.tokens[0]) if err != nil { panic(err) } if len(b) != (g.bounds.Dx()+7)/8 { panic("invalid bitmap data, width mismatch") } g.bitmap = append(g.bitmap, b...) rows++ } else { switch p.tokens[0] { case "ENCODING": encoding = p.readCharEncoding() case "DWIDTH": g.advance = p.readDwidth() case "BBX": g.bounds = p.readBBX() case "BITMAP": bitmap = true } } } if rows != g.bounds.Dy() { panic("invalid bitmap data, height mismatch") } // XXX: We don't try to convert encodings, since we'd need x/text/encoding // for the conversion tables, though most fonts are at least going to use // supersets of ASCII. Use ISO10646-1 X11 fonts for proper Unicode support. if encoding >= 0 { p.font.glyphs[rune(encoding)] = g } if encoding == p.defaultChar { p.font.fallback = g } } // https://en.wikipedia.org/wiki/Glyph_Bitmap_Distribution_Format // https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5005.BDF_Spec.pdf func (p *bdfParser) parse() { if !p.readLine() || len(p.tokens) != 2 || p.tokens[0] != "STARTFONT" { panic("invalid header") } if p.tokens[1] != "2.1" && p.tokens[1] != "2.2" { panic("unsupported version number") } for p.readLine() && p.tokens[0] != "ENDFONT" { switch p.tokens[0] { case "FONT": if len(p.tokens) < 2 { panic("insufficient arguments") } p.font.Name = p.tokens[1] case "FONTBOUNDINGBOX": // There's no guarantee that this includes all BBXs. p.defaultBounds = p.readBBX() case "METRICSSET": if len(p.tokens) < 2 { panic("insufficient arguments") } if p.tokens[1] == "1" { panic("purely vertical fonts are unsupported") } case "DWIDTH": p.defaultAdvance = p.readDwidth() case "STARTPROPERTIES": p.parseProperties() case "STARTCHAR": p.parseChar() } } if p.font.Name == "" { panic("the font file doesn't contain the font's name") } if len(p.font.glyphs) == 0 { panic("the font file doesn't seem to contain any glyphs") } } func NewFromBDF(r io.Reader) (f *Font, err error) { p := bdfParser{ scanner: bufio.NewScanner(r), font: &Font{glyphs: make(map[rune]glyph)}, defaultChar: -1, } defer func() { if r := recover(); r != nil { var ok bool if err, ok = r.(error); !ok { err = fmt.Errorf("%v", r) } } if err != nil { err = fmt.Errorf("line %d: %s", p.line, err) } }() p.parse() return p.font, nil }