Personal warehouse management system
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

354 lines
8.6KB

  1. package bdf
  2. import (
  3. "bufio"
  4. "encoding/hex"
  5. "fmt"
  6. "image"
  7. "image/color"
  8. "image/draw"
  9. "io"
  10. "strconv"
  11. )
  12. // glyph is a singular bitmap glyph to be used as a mask, assumed to directly
  13. // correspond to a rune. A zero value is also valid and drawable.
  14. type glyph struct {
  15. // Coordinates are relative to the origin, on the baseline.
  16. // The ascent is thus negative, unlike the usual model.
  17. bounds image.Rectangle
  18. bitmap []byte
  19. advance int
  20. }
  21. // ColorModel implements image.Image.
  22. func (g *glyph) ColorModel() color.Model { return color.Alpha16Model }
  23. // Bounds implements image.Image.
  24. func (g *glyph) Bounds() image.Rectangle { return g.bounds }
  25. // At implements image.Image. This is going to be somewhat slow.
  26. func (g *glyph) At(x, y int) color.Color {
  27. x -= g.bounds.Min.X
  28. y -= g.bounds.Min.Y
  29. dx, dy := g.bounds.Dx(), g.bounds.Dy()
  30. if x < 0 || y < 0 || x >= dx || y >= dy {
  31. return color.Transparent
  32. }
  33. stride, offset, bit := (dx+7)/8, x/8, byte(1<<uint(7-x%8))
  34. if g.bitmap[y*stride+offset]&bit == 0 {
  35. return color.Transparent
  36. }
  37. return color.Opaque
  38. }
  39. // -----------------------------------------------------------------------------
  40. // Font represents a particular bitmap font.
  41. type Font struct {
  42. Name string
  43. glyphs map[rune]glyph
  44. fallback glyph
  45. }
  46. // FindGlyph returns the best glyph to use for the given rune.
  47. // The returned boolean indicates whether a fallback has been used.
  48. func (f *Font) FindGlyph(r rune) (glyph, bool) {
  49. if g, ok := f.glyphs[r]; ok {
  50. return g, true
  51. }
  52. return f.fallback, false
  53. }
  54. // DrawString draws the specified text string onto dst horizontally along
  55. // the baseline starting at dp, using black color.
  56. func (f *Font) DrawString(dst draw.Image, dp image.Point, s string) {
  57. for _, r := range s {
  58. g, _ := f.FindGlyph(r)
  59. draw.DrawMask(dst, g.bounds.Add(dp),
  60. image.Black, image.ZP, &g, g.bounds.Min, draw.Over)
  61. dp.X += g.advance
  62. }
  63. }
  64. // BoundString measures the text's bounds when drawn along the X axis
  65. // for the baseline. Also returns the total advance.
  66. func (f *Font) BoundString(s string) (image.Rectangle, int) {
  67. var (
  68. bounds image.Rectangle
  69. dot image.Point
  70. )
  71. for _, r := range s {
  72. g, _ := f.FindGlyph(r)
  73. bounds = bounds.Union(g.bounds.Add(dot))
  74. dot.X += g.advance
  75. }
  76. return bounds, dot.X
  77. }
  78. // -----------------------------------------------------------------------------
  79. func latin1ToUTF8(latin1 []byte) string {
  80. buf := make([]rune, len(latin1))
  81. for i, b := range latin1 {
  82. buf[i] = rune(b)
  83. }
  84. return string(buf)
  85. }
  86. // tokenize splits a BDF line into tokens. Quoted strings may start anywhere
  87. // on the line. We only enforce that they must end somewhere.
  88. func tokenize(s string) (tokens []string, err error) {
  89. token, quotes, escape := []rune{}, false, false
  90. for _, r := range s {
  91. switch {
  92. case escape:
  93. switch r {
  94. case '"':
  95. escape = false
  96. token = append(token, r)
  97. case ' ', '\t':
  98. quotes, escape = false, false
  99. tokens = append(tokens, string(token))
  100. token = nil
  101. default:
  102. quotes, escape = false, false
  103. token = append(token, r)
  104. }
  105. case quotes:
  106. switch r {
  107. case '"':
  108. escape = true
  109. default:
  110. token = append(token, r)
  111. }
  112. default:
  113. switch r {
  114. case '"':
  115. // We could also enable quote processing on demand,
  116. // so that it is only turned on in properties.
  117. if len(tokens) < 1 || tokens[0] != "COMMENT" {
  118. quotes = true
  119. } else {
  120. token = append(token, r)
  121. }
  122. case ' ', '\t':
  123. if len(token) > 0 {
  124. tokens = append(tokens, string(token))
  125. token = nil
  126. }
  127. default:
  128. token = append(token, r)
  129. }
  130. }
  131. }
  132. if quotes && !escape {
  133. return nil, fmt.Errorf("strings may not contain newlines")
  134. }
  135. if quotes || len(token) > 0 {
  136. tokens = append(tokens, string(token))
  137. }
  138. return tokens, nil
  139. }
  140. // -----------------------------------------------------------------------------
  141. // bdfParser is a basic and rather lenient parser of
  142. // Bitmap Distribution Format (BDF) files.
  143. type bdfParser struct {
  144. scanner *bufio.Scanner // input reader
  145. line int // current line number
  146. tokens []string // tokens on the current line
  147. font *Font // glyph storage
  148. defaultBounds image.Rectangle
  149. defaultAdvance int
  150. defaultChar int
  151. }
  152. // readLine reads the next line and splits it into tokens.
  153. // Panics on error, returns false if the end of file has been reached normally.
  154. func (p *bdfParser) readLine() bool {
  155. p.line++
  156. if !p.scanner.Scan() {
  157. if err := p.scanner.Err(); err != nil {
  158. panic(err)
  159. }
  160. p.line--
  161. return false
  162. }
  163. var err error
  164. if p.tokens, err = tokenize(latin1ToUTF8(p.scanner.Bytes())); err != nil {
  165. panic(err)
  166. }
  167. // Eh, it would be nicer iteratively, this may overrun the stack.
  168. if len(p.tokens) == 0 {
  169. return p.readLine()
  170. }
  171. return true
  172. }
  173. func (p *bdfParser) readCharEncoding() int {
  174. if len(p.tokens) < 2 {
  175. panic("insufficient arguments")
  176. }
  177. if i, err := strconv.Atoi(p.tokens[1]); err != nil {
  178. panic(err)
  179. } else {
  180. return i // Some fonts even use -1 for things outside the encoding.
  181. }
  182. }
  183. func (p *bdfParser) parseProperties() {
  184. // The wording in the specification suggests that the argument
  185. // with the number of properties to follow isn't reliable.
  186. for p.readLine() && p.tokens[0] != "ENDPROPERTIES" {
  187. switch p.tokens[0] {
  188. case "DEFAULT_CHAR":
  189. p.defaultChar = p.readCharEncoding()
  190. }
  191. }
  192. }
  193. // XXX: Ignoring vertical advance since we only expect purely horizontal fonts.
  194. func (p *bdfParser) readDwidth() int {
  195. if len(p.tokens) < 2 {
  196. panic("insufficient arguments")
  197. }
  198. if i, err := strconv.Atoi(p.tokens[1]); err != nil {
  199. panic(err)
  200. } else {
  201. return i
  202. }
  203. }
  204. func (p *bdfParser) readBBX() image.Rectangle {
  205. if len(p.tokens) < 5 {
  206. panic("insufficient arguments")
  207. }
  208. w, e1 := strconv.Atoi(p.tokens[1])
  209. h, e2 := strconv.Atoi(p.tokens[2])
  210. x, e3 := strconv.Atoi(p.tokens[3])
  211. y, e4 := strconv.Atoi(p.tokens[4])
  212. if e1 != nil || e2 != nil || e3 != nil || e4 != nil {
  213. panic("invalid arguments")
  214. }
  215. if w < 0 || h < 0 {
  216. panic("bounding boxes may not have negative dimensions")
  217. }
  218. return image.Rectangle{
  219. Min: image.Point{x, -(y + h)},
  220. Max: image.Point{x + w, -y},
  221. }
  222. }
  223. func (p *bdfParser) parseChar() {
  224. g := glyph{bounds: p.defaultBounds, advance: p.defaultAdvance}
  225. bitmap, rows, encoding := false, 0, -1
  226. for p.readLine() && p.tokens[0] != "ENDCHAR" {
  227. if bitmap {
  228. b, err := hex.DecodeString(p.tokens[0])
  229. if err != nil {
  230. panic(err)
  231. }
  232. if len(b) != (g.bounds.Dx()+7)/8 {
  233. panic("invalid bitmap data, width mismatch")
  234. }
  235. g.bitmap = append(g.bitmap, b...)
  236. rows++
  237. } else {
  238. switch p.tokens[0] {
  239. case "ENCODING":
  240. encoding = p.readCharEncoding()
  241. case "DWIDTH":
  242. g.advance = p.readDwidth()
  243. case "BBX":
  244. g.bounds = p.readBBX()
  245. case "BITMAP":
  246. bitmap = true
  247. }
  248. }
  249. }
  250. if rows != g.bounds.Dy() {
  251. panic("invalid bitmap data, height mismatch")
  252. }
  253. // XXX: We don't try to convert encodings, since we'd need x/text/encoding
  254. // for the conversion tables, though most fonts are at least going to use
  255. // supersets of ASCII. Use ISO10646-1 X11 fonts for proper Unicode support.
  256. if encoding >= 0 {
  257. p.font.glyphs[rune(encoding)] = g
  258. }
  259. if encoding == p.defaultChar {
  260. p.font.fallback = g
  261. }
  262. }
  263. // https://en.wikipedia.org/wiki/Glyph_Bitmap_Distribution_Format
  264. // https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5005.BDF_Spec.pdf
  265. func (p *bdfParser) parse() {
  266. if !p.readLine() || len(p.tokens) != 2 || p.tokens[0] != "STARTFONT" {
  267. panic("invalid header")
  268. }
  269. if p.tokens[1] != "2.1" && p.tokens[1] != "2.2" {
  270. panic("unsupported version number")
  271. }
  272. for p.readLine() && p.tokens[0] != "ENDFONT" {
  273. switch p.tokens[0] {
  274. case "FONT":
  275. if len(p.tokens) < 2 {
  276. panic("insufficient arguments")
  277. }
  278. p.font.Name = p.tokens[1]
  279. case "FONTBOUNDINGBOX":
  280. // There's no guarantee that this includes all BBXs.
  281. p.defaultBounds = p.readBBX()
  282. case "METRICSSET":
  283. if len(p.tokens) < 2 {
  284. panic("insufficient arguments")
  285. }
  286. if p.tokens[1] == "1" {
  287. panic("purely vertical fonts are unsupported")
  288. }
  289. case "DWIDTH":
  290. p.defaultAdvance = p.readDwidth()
  291. case "STARTPROPERTIES":
  292. p.parseProperties()
  293. case "STARTCHAR":
  294. p.parseChar()
  295. }
  296. }
  297. if p.font.Name == "" {
  298. panic("the font file doesn't contain the font's name")
  299. }
  300. if len(p.font.glyphs) == 0 {
  301. panic("the font file doesn't seem to contain any glyphs")
  302. }
  303. }
  304. func NewFromBDF(r io.Reader) (f *Font, err error) {
  305. p := bdfParser{
  306. scanner: bufio.NewScanner(r),
  307. font: &Font{glyphs: make(map[rune]glyph)},
  308. defaultChar: -1,
  309. }
  310. defer func() {
  311. if r := recover(); r != nil {
  312. var ok bool
  313. if err, ok = r.(error); !ok {
  314. err = fmt.Errorf("%v", r)
  315. }
  316. }
  317. if err != nil {
  318. err = fmt.Errorf("line %d: %s", p.line, err)
  319. }
  320. }()
  321. p.parse()
  322. return p.font, nil
  323. }