From ab253ce768c410f6c996fd3df98292df0554c2f3 Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sat, 6 Apr 2019 21:31:11 +0200 Subject: Initial commit --- bdf/bdf.go | 353 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 bdf/bdf.go (limited to 'bdf') diff --git a/bdf/bdf.go b/bdf/bdf.go new file mode 100644 index 0000000..c02e31e --- /dev/null +++ b/bdf/bdf.go @@ -0,0 +1,353 @@ +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) readCharEncoding() 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) 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() + } + } +} + +// XXX: Ignoring vertical advance since we only expect purely horizontal fonts. +func (p *bdfParser) readDwidth() int { + if len(p.tokens) < 2 { + panic("insufficient arguments") + } + if i, err := strconv.Atoi(p.tokens[1]); err != nil { + panic(err) + } else { + return i + } +} + +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 +} -- cgit v1.2.3-70-g09d2