ref: b548687a8ed1d0a159c9d3f3f921d93bbb56908e
dir: /appl/cmd/ttffs.b/
implement Ttffs; # serve (over styx) ttf's as inferno/plan 9 (sub)fonts in arbitrary sizes. # fonts and subfonts are not listed in the directory, but can be walked to. # the font and subfont files are generated on the fly. # subfonts contain at most 128 glyphs. # at first read of a font, it is parsed and its glyph ranges determined. # # for each font file (fontpath/*.ttf) the following files are served: # <name>.<style>.<size>.font # <name>.<style>.<size>.<index> # # the second form are subfonts, index starts at 1. index 1 always has the single char 0. include "sys.m"; sys: Sys; sprint: import sys; include "draw.m"; draw: Draw; Display, Rect, Point, Image: import draw; include "arg.m"; include "bufio.m"; bufio: Bufio; Iobuf: import bufio; include "string.m"; str: String; include "styx.m"; styx: Styx; Tmsg, Rmsg: import styx; include "styxservers.m"; styxservers: Styxservers; Styxserver, Fid, Navigator, Navop: import styxservers; nametree: Nametree; Tree: import nametree; Enotfound: import styxservers; include "freetype.m"; ft: Freetype; Face, Glyph: import ft; include "readdir.m"; readdir: Readdir; include "tables.m"; tables: Tables; Table, Strhash: import tables; Ttffs: module { init: fn(nil: ref Draw->Context, args: list of string); }; dflag: int; fontpath := "/fonts/ttf"; mtpt: con "/mnt/ft"; disp: ref Display; srv: ref Styxserver; styles := array[] of {"r", "i", "b", "ib"}; Qroot, Qindex: con iota; idgen := 2; Font: adt { sane, family: string; ranges: array of ref (int, int); # sorted, start-end inclusive styles: cyclic array of ref Style; # indexed by Face.style }; Style: adt { f: ref Font; dir: ref Sys->Dir; path: string; fc: ref Face; sizes: cyclic ref Table[cyclic ref Size]; # size }; Size: adt { id: int; # qid.path. subfonts are id+Size.range st: cyclic ref Style; range: int; # index for Font.ranges. 0 is .font, 1 is first in range size: int; dat: array of byte; nuse: int; }; fonts: ref Strhash[ref Font]; # family sanefonts: ref Strhash[ref Font]; # sane name sizes: ref Table[ref Size]; # qid.path init(ctxt: ref Draw->Context, args: list of string) { sys = load Sys Sys->PATH; if(ctxt == nil || (disp = ctxt.display) == nil) fail("no display"); draw = load Draw Draw->PATH; arg := load Arg Arg->PATH; bufio = load Bufio Bufio->PATH; str = load String String->PATH; ft = load Freetype Freetype->PATH; styx = load Styx Styx->PATH; styx->init(); styxservers = load Styxservers Styxservers->PATH; styxservers->init(styx); nametree = load Nametree Nametree->PATH; nametree->init(); readdir = load Readdir Readdir->PATH; tables = load Tables Tables->PATH; sys->pctl(Sys->NEWPGRP, nil); Mflag := 0; arg->init(args); arg->setusage(arg->progname()+" [-dM] [-p fontpath]"); while((c := arg->opt()) != 0) case c { 'd' => dflag++; 'p' => fontpath = arg->arg(); 'M' => Mflag++; * => arg->usage(); } args = arg->argv(); if(args != nil) arg->usage(); fonts = fonts.new(11, nil); sanefonts = sanefonts.new(11, nil); sizes = sizes.new(11, nil); fds := array[2] of ref Sys->FD; fd := sys->fildes(0); if(!Mflag) { if(sys->pipe(fds) < 0) fail(sprint("pipe: %r")); fd = fds[0]; } navc := chan of ref Navop; spawn navigator(navc); msgc: chan of ref Tmsg; (msgc, srv) = Styxserver.new(fd, Navigator.new(navc), big Qroot); if(Mflag) return main(msgc); spawn main(msgc); if(sys->mount(fds[1], nil, mtpt, sys->MAFTER, nil) < 0) fail(sprint("mount: %r")); return; } xwarn(s: string): array of ref (int, int) { warn(s); return nil; } # read cached glyph ranges available in the font readranges(path: string, mtime: int): array of ref (int, int) { fd := sys->open(path, Sys->OREAD); if(fd == nil) return xwarn(sprint("open %q: %r", path)); (ok, d) := sys->fstat(fd); if(ok != 0) return xwarn(sprint("fstat %q: %r", path)); if(d.mtime <= mtime) return xwarn(sprint("%q: older than ttf, ignoring", path)); if(sys->readn(fd, buf := array[int d.length] of byte, len buf) != len buf) return xwarn(sprint("readn %q: %r", path)); s := string buf; r: list of ref (int, int); for(l := sys->tokenize(s, "\n").t1; l != nil; l = tl l) { x := sys->tokenize(hd l, " ").t1; if(len x != 2) return xwarn(sprint("%q: bad glyph range line: %q", path, hd l)); (a, rem0) := str->toint(hd x, 10); (b, rem1) := str->toint(hd tl x, 10); if(rem0 != nil || rem1 != nil || b < a || b > 64*1024 || r != nil && a <= (hd r).t1) return xwarn(sprint("%q: invalid glyph range: %q", path, hd l)); r = ref (a, b)::r; } return l2a(rev(r)); } genranges(f: ref Face): array of ref (int, int) { r := list of {ref (0, 0)}; max := 64*1024; i := 1; while(i < max) { for(; i < max && !f.haschar(i); i++) {} s := i; for(; i < max && f.haschar(i); i++) {} e := i; while(s < e) { n := e-s; if(n > 128) n = 128; if(dflag > 1) say(sprint("range %d-%d", s, s+n-1)); r = ref (s, s+n-1)::r; s += n; } } return l2a(rev(r)); } indexdat: array of byte; indexmtime: int; mkindex(): array of byte { say("mkindex0"); (ok, dir) := sys->stat(fontpath); if(ok != 0) { warn(sprint("stat %q: %r", fontpath)); return nil; } if(indexdat != nil && indexmtime == dir.mtime) return indexdat; say("mkindex1"); nfonts := fonts.new(11, nil); nsanefonts := sanefonts.new(11, nil); nsizes := sizes.new(11, nil); (a, n) := readdir->init(fontpath, Readdir->NONE); if(n < 0) return nil; for(i := 0; i < len a; i++) { if(!suffix(".ttf", a[i].name) && !suffix(".otf", a[i].name)) continue; name := a[i].name; name = name[:len name-len ".ttf"]; path := sprint("%s/%s", fontpath, a[i].name); fc := ft->newface(path, 0); if(fc == nil) { warn(sprint("newface %q: %r", path)); continue; } if(dflag) say(sprint("have face, nfaces=%d index=%d style=%d height=%d ascent=%d familyname=%q stylename=%q", fc.nfaces, fc.index, fc.style, fc.height, fc.ascent, fc.familyname, fc.stylename)); rpath := sprint("%s/ranges.%s", fontpath, name); ranges := readranges(rpath, a[i].mtime); if(ranges == nil) { ranges = genranges(fc); s := ""; for(j := 0; j < len ranges; j++) s += sprint("%d %d\n", ranges[j].t0, ranges[j].t1); fd := sys->create(rpath, Sys->OWRITE, 8r666); if(fd == nil || sys->write(fd, buf := array of byte s, len buf) != len buf) warn(sprint("create or write %q: %r", rpath)); } f := nfonts.find(fc.familyname); if(f == nil) { sane := sanitize(fc.familyname); while(nsanefonts.find(sane) != nil) sane += "0"; f = ref Font(sane, fc.familyname, ranges, array[len styles] of ref Style); nfonts.add(f.family, f); nsanefonts.add(f.sane, f); } else if(f.styles[fc.style] != nil) { warn(sprint("duplicate style %#q", styles[fc.style])); continue; } st := ref Style(f, ref dir, path, fc, nil); st.sizes = st.sizes.new(11, nil); f.styles[st.fc.style] = st; for(l := tabitems(st.sizes); l != nil; l = tl l) { (nil, size) := *hd l; nsizes.add(size.id, size); } } s := ""; for(l := strtabitems(nfonts); l != nil; l = tl l) { f := (hd l).t1; st := ""; for(i = 0; i < len styles; i++) if(f.styles[i] != nil) st += ","+styles[i]; s += sprint("%q %q\n", f.sane, sprint(".%s.%s.2-", f.family, st[1:])); } # ensure we don't mkindex immediately after writing ranges files (ok, dir) = sys->stat(fontpath); if(ok != 0) { warn(sprint("stat: %q: %r", fontpath)); return nil; } fonts = nfonts; sanefonts = nsanefonts; sizes = nsizes; indexdat = array of byte s; indexmtime = dir.mtime; return indexdat; } sanitize(s: string): string { s = str->tolower(s); r: string; for(i := 0; i < len s; i++) case c := s[i] { ' ' or '\t' or '-' => if(r != nil && r[len r-1] != '-') r[len r] = '-'; '.' => {} * => r[len r] = c; } return r; } mkname(s: ref Size): string { st := s.st; fc := st.fc; f := st.f; if(s.range == 0) return sprint("%s.%s.%d.font", f.sane, styles[fc.style], s.size); return sprint("%s.%s.%d.%d", f.sane, styles[fc.style], s.size, s.range); } mkdat(f: ref Size): array of byte { if(f.dat == nil) { if(f.range == 0) f.dat = mkfont(f); else f.dat = mksubfont(f); } return f.dat; } mkfont(sz: ref Size): array of byte { f := sz.st.f; fc := sz.st.fc; fc.setcharsize(sz.size<<6, 0, 0); s := sprint("%d %d\n", fc.height, fc.ascent); for(i := 0; i < len f.ranges; i++) { (a, b) := *f.ranges[i]; s += sprint("0x%04x\t0x%04x\t%q\n", a, b, sprint("%s.%s.%d.%d", f.sane, styles[fc.style], sz.size, i+1)); } return array of byte s; } mksubfont(sz: ref Size): array of byte { (s, l) := *sz.st.f.ranges[sz.range-1]; fc := sz.st.fc; fc.setcharsize(sz.size<<6, 0, 0); imgs := array[l+1-s] of ref Image; n := l+1-s; width := 0; left := array[len imgs+1] of {* => 0}; advance := array[len imgs+1] of {* => 0}; for(i := 0; i < n; i++) { c := s+i; g := fc.loadglyph(c); if(g == nil) fail(sprint("no glyph for %c (%#x)", c, c)); if(dflag) say(sprint("glyph %#x, width=%d height=%d top=%d left=%d advance=%d,%d", c, g.width, g.height, g.top, g.left, g.advance.x>>6, g.advance.y>>6)); r := Rect((0,0), (g.width, fc.height)); img := imgs[i] = disp.newimage(r, Draw->GREY8, 0, Draw->Black); gr: Rect; gr.min = (0,fc.ascent-g.top); gr.max = gr.min.add((g.width, g.height)); img.writepixels(gr, g.bitmap); width += g.width; left[i] = g.left; advance[i] = g.advance.x>>6; } oimghdr := 0; obuf := oimghdr + 5*12; osubfhdr := obuf + fc.height*width; ochars := osubfhdr + 3*12; oend := ochars + (len imgs+1)*6; buf := array[oend] of byte; fontr := Rect((0,0), (width,fc.height)); fontimg := disp.newimage(fontr, Draw->GREY8, 0, Draw->Black); buf[oimghdr:] = sys->aprint("%11s %11d %11d %11d %11d ", "k8", 0, 0, fontr.max.x, fontr.max.y); x := 0; buf[osubfhdr:] = sys->aprint("%11d %11d %11d ", len imgs, fc.height, fc.ascent); o := ochars; for(i = 0; i < len imgs+1; i++) { if(i < len imgs) img := imgs[i]; buf[o++] = byte (x>>0); buf[o++] = byte (x>>8); buf[o++] = byte 0; # top buf[o++] = byte fc.height; # bottom buf[o++] = byte left[i]; # left if(img == nil) { buf[o++] = byte 0; # width break; } buf[o++] = byte advance[i]; # width r := fontr; r.min.x = x; fontimg.draw(r, disp.white, img, Point(0,0)); x += img.r.dx(); } if(o != len buf) raise "bad pack"; r := fontimg.readpixels(fontimg.r, buf[obuf:]); if(r != osubfhdr-obuf) fail(sprint("readpixels, got %d, expected %d: %r", r, osubfhdr-obuf)); return buf; } main(msgc: chan of ref Tmsg) { sys->pctl(Sys->FORKNS, nil); more: for(;;) { mm := <-msgc; if(mm == nil) break more; pick m := mm { Readerror => warn("read: "+m.error); break more; * => handle(mm); pick x := mm { Clunk or Remove => cacheclean(); } } } killgrp(pid()); } cacheclean() { for(k := tabitems(sizes); k != nil; k = tl k) (hd k).t1.nuse = 0; for(l := srv.allfids(); l != nil; l = tl l) { fid := hd l; f := sizes.find(int fid.path); if(fid.isopen) f.nuse++; } for(k = tabitems(sizes); k != nil; k = tl k) { sz := (hd k).t1; if(sz.nuse == 0 && sz.dat != nil) { if(dflag) say(sprint("freeing %s.%s.%d.%d", sz.st.f.sane, styles[sz.st.fc.style], sz.size, sz.range)); sz.dat = nil; } } } navigator(navc: chan of ref Navop) { for(;;) navigate(<-navc); } navreply(op: ref Navop, d: ref Sys->Dir, err: string) { op.reply <-= (d, err); } navigate(op: ref Navop) { pick x := op { Stat => case int x.path { Qroot => navreply(x, ref dirroot(), nil); Qindex => navreply(x, ref dirindex(), nil); * => mkindex(); # ensure up to date index f := sizes.find(int x.path); if(f == nil) return navreply(x, nil, sprint("missing Size for qid.path %bd/%#bx", x.path, x.path)); d := ref dir(mkname(f), int x.path, 8r444, len mkdat(f), 0); navreply(x, d, nil); } Walk => if(x.name == "..") return navreply(x, ref dirroot(), nil); if(x.path != big Qroot) return navreply(x, nil, Enotfound); if(x.name == "index") return navreply(x, ref dirindex(), nil); mkindex(); # ensure up to date index name, style, size, suf: string; s := x.name; (s, suf) = str->splitstrr(s, "."); if(s != nil) (s, size) = str->splitstrr(s[:len s-1], "."); if(s != nil) (name, style) = str->splitstrr(s[:len s-1], "."); if(name == nil) return navreply(x, nil, Enotfound); name = name[:len name-1]; if(dflag) say(sprint("walk, name %q, style %q, size %q, suf %q", name, style, size, suf)); # format is good f := sanefonts.find(name); if(f == nil) return navreply(x, nil, "no such font"); sti := find(styles, style); if(sti < 0 || f.styles[sti] == nil) return navreply(x, nil, "no such style"); (szs, rem) := str->toint(size, 10); if(rem != nil) return navreply(x, nil, Enotfound); if(szs <= 1) return navreply(x, nil, "no such size"); r := 0; if(suf != "font") { (r, rem) = str->toint(suf, 10); if(rem != nil || r <= 0 || r > len f.ranges) return navreply(x, nil, "no such range"); } st := f.styles[sti]; xsz := st.sizes.find(szs); if(xsz == nil) { xsz = ref Size(idgen++, st, 0, szs, nil, 0); sizes.add(xsz.id, xsz); for(i := 0; i < len f.ranges; i++) { ssz := ref Size(idgen++, st, 1+i, szs, nil, 0); sizes.add(ssz.id, ssz); } st.sizes.add(xsz.size, xsz); } sz := sizes.find(xsz.id+r); navreply(x, ref dir(x.name, sz.id, 8r444, len mkdat(sz), 0), nil); Readdir => dirs := array[] of {ref dirindex()}; s := x.offset; if(s > len dirs) s = len dirs; e := x.offset+x.count; if(e > len dirs) e = len dirs; while(s < e) navreply(x, dirs[s++], nil); navreply(x, nil, nil); } } handle(mm: ref Tmsg) { pick m := mm { Read => ff := srv.getfid(m.fid); if(ff == nil || ff.path == big Qroot || !ff.isopen) break; if(ff.path == big Qindex) dat := mkindex(); else { f := sizes.find(int ff.path); if(f == nil) { srv.reply(ref Rmsg.Error(m.tag, "size not found?")); return; } dat = mkdat(f); } srv.reply(styxservers->readbytes(m, dat)); return; } srv.default(mm); } dirroot(): Sys->Dir { return dir(".", Qroot, 8r555|Sys->DMDIR, 0, 0); } dirindex(): Sys->Dir { mtime := 0; (ok, d) := sys->stat(fontpath); if(ok == 0) mtime = d.mtime; return dir("index", Qindex, 8r444, 0, mtime); } dir(name: string, path, mode, length, mtime: int): Sys->Dir { d := sys->zerodir; d.name = name; d.uid = d.gid = "ttffs"; d.qid.path = big path; d.qid.qtype = Sys->QTFILE; if(mode&Sys->DMDIR) d.qid.qtype = Sys->QTDIR; d.mtime = d.atime = mtime; d.mode = mode; d.length = big length; return d; } strtabitems[T](t: ref Strhash[T]): list of ref (string, T) { r: list of ref (string, T); for(i := 0; i < len t.items; i++) for(l := t.items[i]; l != nil; l = tl l) r = ref hd l::r; return r; } tabitems[T](t: ref Table[T]): list of ref (int, T) { r: list of ref (int, T); for(i := 0; i < len t.items; i++) for(l := t.items[i]; l != nil; l = tl l) r = ref hd l::r; return r; } find(a: array of string, s: string): int { for(i := 0; i < len a; i++) if(a[i] == s) return i; return -1; } suffix(suf, s: string): int { return len s >= len suf && suf == s[len s-len suf:]; } pid(): int { return sys->pctl(0, nil); } killgrp(pid: int) { sys->fprint(sys->open(sprint("/prog/%d/ctl", pid), Sys->OWRITE), "killgrp"); } rev[T](l: list of T): list of T { r: list of T; for(; l != nil; l = tl l) r = hd l::r; return r; } l2a[T](l: list of T): array of T { a := array[len l] of T; i := 0; for(; l != nil; l = tl l) a[i++] = hd l; return a; } fd2: ref Sys->FD; warn(s: string) { if(fd2 == nil) fd2 = sys->fildes(2); sys->fprint(fd2, "%s\n", s); } say(s: string) { if(dflag) warn(s); } fail(s: string) { warn(s); killgrp(pid()); raise "fail:"+s; }