shithub: neatroff

Download patch

ref: 7a7228955eeb53ce7adfb4e11fa1c500be35d616
parent: 9ea76ed72cf02518746e3b0ea9ae29570e19ce2e
author: Ali Gholami Rudi <ali@rudi.ir>
date: Sat Aug 2 13:42:57 EDT 2014

font: support advanced font features

Now neatroff supports advanced font substition and positioning
features.  Font descriptions can now include lines starting
with gpos and gsub:

  gsub feat -f -f -i +ffi
  gpos feat A:+0+0+5+0 V

The length of the patterns can be longer.

--- a/font.c
+++ b/font.c
@@ -4,6 +4,23 @@
 #include <string.h>
 #include "roff.h"
 
+#define GHASH(g1, g2)		((((g2) + 1) << 16) | ((g1) + 1))
+
+/* glyph pattern for gsub and gpos tables; each grule has some gpats */
+struct gpat {
+	short g;		/* glyph index */
+	short rep;		/* ignore this glyph; gsub replacement */
+	short x, y, xadv, yadv;	/* gpos data */
+};
+
+/* glyph substitution and positioning rules */
+struct grule {
+	struct gpat *pats;
+	short len;	/* pats[] length */
+	short feat;	/* feature owning this rule */
+	int hash;	/* hash of this rule for sorting and comparison */
+};
+
 struct font {
 	char name[FNLEN];
 	char fontname[FNLEN];
@@ -19,15 +36,17 @@
 	struct glyph *g_map[NGLYPHS];	/* character remapped via font_map() */
 	int n;				/* number of characters in charset */
 	struct dict cdict;		/* mapping from c[i] to i */
-	/* font ligatures (lg*) */
-	char lg[NLIGS][LIGLEN * GNLEN];	/* ligatures */
-	int lgn;			/* number of ligatures in lg[] */
-	/* kerning pair table per glyph (kn*) */
-	int knhead[NGLYPHS];		/* kerning pairs of glyphs[] */
-	int knnext[NKERNS];		/* next item in knhead[] list */
-	int knpair[NKERNS];		/* kerning pair 2nd glyphs */
-	int knval[NKERNS];		/* font pairwise kerning value */
-	int knn;			/* number of kerning pairs */
+	/* font features */
+	char feat_name[NFEATS][8];	/* feature names */
+	int feat_set[NFEATS];		/* feature enabled */
+	int feat_n;
+	/* glyph substitution and positioning */
+	struct gpat pats[NGPATS];	/* glyph pattern space */
+	int pats_pos;			/* current position in pats[] */
+	struct grule gsub[NGRULES];	/* glyph substitution rules */
+	int gsub_n;
+	struct grule gpos[NGRULES];	/* glyph positioning rules */
+	int gpos_n;
 };
 
 /* find a glyph by its name */
@@ -89,59 +108,113 @@
 	return g ? g - fn->glyphs : -1;
 }
 
-/*
- * If the first m characters of src form a ligature, return n and
- * copy the ligature to lig.
- */
-int font_lig(struct font *fn, char *lig, char src[][GNLEN], int n)
+static int grulecmp(void *v1, void *v2)
 {
-	char cat[GNLEN * 2] = "";
-	int cmap[GNLEN * 2] = {0};
-	int i, l;
-	for (i = 0; i < n && i < LIGLEN && strlen(cat) < GNLEN; i++) {
-		strcat(cat, src[i]);
-		cmap[strlen(cat)] = i;
-	}
-	for (i = 0; i < fn->lgn; i++) {
-		l = strlen(fn->lg[i]);
-		if (cmap[l] && !strncmp(cat, fn->lg[i], l)) {
-			if (font_find(fn, fn->lg[i])) {
-				memcpy(lig, cat, l);
-				lig[l] = '\0';
-				return cmap[l] + 1;
-			}
-		}
-	}
-	return 0;
+	return ((struct grule *) v1)->hash - ((struct grule *) v2)->hash;
 }
 
-/* return nonzero if s is a ligature */
-int font_islig(struct font *fn, char *s)
+/* the hashing function for grule structs, based on their first two glyphs */
+static int grule_hash(struct grule *rule)
 {
-	int i;
-	for (i = 0; i < fn->lgn; i++)
-		if (!strcmp(s, fn->lg[i]))
-			return 1;
-	return 0;
+	int g1 = -1, g2 = -1;
+	int i = 0;
+	while (i < rule->len && rule->pats[i].rep)
+		i++;
+	g1 = i < rule->len ? rule->pats[i++].g : -1;
+	while (i < rule->len && rule->pats[i].rep)
+		i++;
+	g2 = i < rule->len ? rule->pats[i++].g : -1;
+	return GHASH(g1, g2);
 }
 
-/* return pairwise kerning value between c1 and c2 */
-int font_kern(struct font *fn, char *c1, char *c2)
+static int grule_find(struct grule *rules, int n, int hash)
 {
-	int i1, i2, i;
-	i1 = font_idx(fn, font_find(fn, c1));
-	i2 = font_idx(fn, font_find(fn, c2));
-	if (i1 < 0 || i2 < 0)
+	int l = 0;
+	int h = n;
+	while (l < h) {
+		int m = (l + h) >> 1;
+		if (rules[m].hash >= hash)
+			h = m;
+		else
+			l = m + 1;
+	}
+	return rules[l].hash == hash ? l : -1;
+}
+
+static int font_rulematches(struct font *fn, struct grule *rule, int *src, int len)
+{
+	int j, sidx = 0;
+	if (!fn->feat_set[rule->feat])
 		return 0;
-	i = fn->knhead[i1];
-	while (i >= 0) {
-		if (fn->knpair[i] == i2)
-			return fn->knval[i];
-		i = fn->knnext[i];
+	for (j = 0; j < rule->len; j++)
+		if (!rule->pats[j].rep)
+			if (sidx >= len || rule->pats[j].g != src[sidx++])
+				return 0;
+	return j == rule->len;
+}
+
+static struct grule *font_findrule(struct font *fn, struct grule *rules,
+			int n, int *src, int len)
+{
+	int i, j;
+	for (j = 0; j < 2 && j < len; j++) {
+		int hash = GHASH(src[0], j == 0 ? -1 : src[1]);
+		int idx = grule_find(rules, n, hash);
+		if (idx < 0)
+			continue;
+		for (i = idx; i < n && rules[i].hash == hash; i++)
+			if (font_rulematches(fn, rules + i, src, len))
+				return rules + i;
 	}
-	return 0;
+	return NULL;
 }
 
+int font_layout(struct font *fn, struct glyph **gsrc, int nsrc, int sz,
+		struct glyph **gdst, int *dmap,
+		int *x, int *y, int *xadv, int *yadv)
+{
+	int src[WORDLEN], dst[WORDLEN];
+	int ndst = 0;
+	int i, j;
+	for (i = 0; i < nsrc; i++)
+		src[i] = font_idx(fn, gsrc[i]);
+	for (i = 0; i < nsrc; i++) {
+		struct grule *rule = font_findrule(fn, fn->gsub,
+					fn->gsub_n, src + i, nsrc - i);
+		dmap[ndst] = i;
+		if (rule) {
+			for (j = 0; j < rule->len; j++) {
+				if (rule->pats[j].rep)
+					dst[ndst++] = rule->pats[j].g;
+				else
+					i++;
+			}
+			i--;
+		} else {
+			dst[ndst++] = src[i];
+		}
+	}
+	memset(x, 0, ndst * sizeof(x[0]));
+	memset(y, 0, ndst * sizeof(y[0]));
+	memset(xadv, 0, ndst * sizeof(xadv[0]));
+	memset(yadv, 0, ndst * sizeof(yadv[0]));
+	for (i = 0; i < ndst; i++)
+		gdst[i] = fn->glyphs + dst[i];
+	for (i = 0; i < ndst; i++) {
+		struct grule *rule = font_findrule(fn, fn->gpos,
+					fn->gpos_n, dst + i, ndst - i);
+		if (!rule)
+			continue;
+		for (j = 0; j < rule->len; j++) {
+			x[i + j] = rule->pats[j].x;
+			y[i + j] = rule->pats[j].y;
+			xadv[i + j] = rule->pats[j].xadv;
+			yadv[i + j] = rule->pats[j].yadv;
+		}
+	}
+	return ndst;
+}
+
 static int font_readchar(struct font *fn, FILE *fin)
 {
 	char tok[ILNLEN];
@@ -149,8 +222,10 @@
 	char id[ILNLEN];
 	struct glyph *glyph = NULL;
 	int type;
+	if (fn->n + 1 == NGLYPHS)
+		errmsg("neatroff: NGLYPHS too low\n");
 	if (fn->n >= NGLYPHS)
-		return 1;
+		return 0;
 	if (fscanf(fin, "%s %s", name, tok) != 2)
 		return 1;
 	if (!strcmp("---", name))
@@ -163,8 +238,9 @@
 		glyph = font_glyph(fn, id);
 		if (!glyph) {
 			glyph = font_glyphput(fn, id, name, type);
-			sscanf(tok, "%d,%d,%d,%d,%d", &glyph->wid,
-				&glyph->llx, &glyph->lly, &glyph->urx, &glyph->ury);
+			sscanf(tok, "%hd,%hd,%hd,%hd,%hd", &glyph->wid,
+				&glyph->llx, &glyph->lly,
+				&glyph->urx, &glyph->ury);
 		}
 	}
 	strcpy(fn->c[fn->n], name);
@@ -174,24 +250,140 @@
 	return 0;
 }
 
+static int font_findfeat(struct font *fn, char *feat, int mk)
+{
+	int i;
+	for (i = 0; i < fn->feat_n; i++)
+		if (!strcmp(feat, fn->feat_name[i]))
+			return i;
+	if (mk)
+		strcpy(fn->feat_name[fn->feat_n], feat);
+	return mk ? fn->feat_n++ : -1;
+}
+
+static struct gpat *font_gpat(struct font *fn, int len)
+{
+	int pos = fn->pats_pos;
+	if (pos + 9 > LEN(fn->pats) && pos + 6 < LEN(fn->pats))
+		errmsg("neatroff: NGPATS too low\n");
+	if (pos + len > LEN(fn->pats))
+		return NULL;
+	memset(fn->pats + pos, 0, sizeof(fn->pats[0]) * len);
+	fn->pats_pos += len;
+	return fn->pats + pos;
+}
+
+static struct grule *font_gsub(struct font *fn, char *feat, int len)
+{
+	struct grule *rule;
+	struct gpat *pats = font_gpat(fn, len);
+	if (fn->gsub_n + 1 == LEN(fn->gsub))
+		errmsg("neatroff: NGRULES too low\n");
+	if (fn->gsub_n >= LEN(fn->gsub) || !pats)
+		return NULL;
+	rule = &fn->gsub[fn->gsub_n++];
+	rule->pats = pats;
+	rule->len = len;
+	rule->feat = font_findfeat(fn, feat, 1);
+	return rule;
+}
+
+static struct grule *font_gpos(struct font *fn, char *feat, int len)
+{
+	struct grule *rule;
+	struct gpat *pats = font_gpat(fn, len);
+	if (fn->gpos_n + 1 == LEN(fn->gpos))
+		errmsg("neatroff: NGRULES too low\n");
+	if (fn->gpos_n >= LEN(fn->gpos) || !pats)
+		return NULL;
+	rule = &fn->gpos[fn->gpos_n++];
+	rule->pats = pats;
+	rule->len = len;
+	rule->feat = font_findfeat(fn, feat, 1);
+	return rule;
+}
+
+static int font_readgsub(struct font *fn, FILE *fin)
+{
+	char tok[128];
+	struct grule *rule;
+	struct glyph *g;
+	int i, n;
+	if (fscanf(fin, "%s %d", tok, &n) != 2)
+		return 1;
+	if (!(rule = font_gsub(fn, tok, n)))
+		return 0;
+	for (i = 0; i < n; i++) {
+		if (fscanf(fin, "%s", tok) != 1)
+			return 1;
+		if (!tok[0] || !(g = font_glyph(fn, tok + 1)))
+			return 0;
+		rule->pats[i].g = font_idx(fn, g);
+		rule->pats[i].rep = tok[0] == '+';
+	}
+	return 0;
+}
+
+static int font_readgpos(struct font *fn, FILE *fin)
+{
+	char tok[128];
+	char *col;
+	struct grule *rule;
+	struct glyph *g;
+	int i, n;
+	if (fscanf(fin, "%s %d", tok, &n) != 2)
+		return 1;
+	if (!(rule = font_gpos(fn, tok, n)))
+		return 0;
+	for (i = 0; i < n; i++) {
+		if (fscanf(fin, "%s", tok) != 1)
+			return 1;
+		col = strchr(tok, ':');
+		if (col)
+			*col = '\0';
+		if (!(g = font_glyph(fn, tok)))
+			return 0;
+		rule->pats[i].g = font_idx(fn, g);
+		if (col)
+			sscanf(col + 1, "%hd%hd%hd%hd",
+				&rule->pats[i].x, &rule->pats[i].y,
+				&rule->pats[i].xadv, &rule->pats[i].yadv);
+	}
+	return 0;
+}
+
 static int font_readkern(struct font *fn, FILE *fin)
 {
 	char c1[ILNLEN], c2[ILNLEN];
-	int i1, i2, val;
+	struct grule *rule;
+	int val;
 	if (fscanf(fin, "%s %s %d", c1, c2, &val) != 3)
 		return 1;
-	i1 = font_idx(fn, font_glyph(fn, c1));
-	i2 = font_idx(fn, font_glyph(fn, c2));
-	if (fn->knn < NKERNS && i1 >= 0 && i2 >= 0) {
-		fn->knnext[fn->knn] = fn->knhead[i1];
-		fn->knhead[i1] = fn->knn;
-		fn->knval[fn->knn] = val;
-		fn->knpair[fn->knn] = i2;
-		fn->knn++;
-	}
+	if (!(rule = font_gpos(fn, "kern", 2)))
+		return 0;
+	rule->pats[0].g = font_idx(fn, font_glyph(fn, c1));
+	rule->pats[1].g = font_idx(fn, font_glyph(fn, c2));
+	rule->pats[0].xadv = val;
 	return 0;
 }
 
+static void font_lig(struct font *fn, char *lig)
+{
+	char c[GNLEN];
+	int g[WORDLEN];
+	struct grule *rule;
+	char *s = lig;
+	int j, n = 0;
+	while (utf8read(&s, c) > 0)
+		g[n++] = font_idx(fn, font_find(fn, c));
+	if (!(rule = font_gsub(fn, "liga", n + 1)))
+		return;
+	for (j = 0; j < n; j++)
+		rule->pats[j].g = g[j];
+	rule->pats[n].g = font_idx(fn, font_glyph(fn, lig));
+	rule->pats[n].rep = 1;
+}
+
 static void skipline(FILE* filp)
 {
 	int c;
@@ -205,6 +397,8 @@
 	struct font *fn;
 	char tok[ILNLEN];
 	FILE *fin;
+	char ligs[512][GNLEN];
+	int ligs_n = 0;
 	int i;
 	fin = fopen(path, "r");
 	if (!fin)
@@ -217,13 +411,22 @@
 	memset(fn, 0, sizeof(*fn));
 	dict_init(&fn->gdict, NGLYPHS, -1, 0, 0);
 	dict_init(&fn->cdict, NGLYPHS, -1, 0, 0);
-	for (i = 0; i < LEN(fn->knhead); i++)
-		fn->knhead[i] = -1;
 	while (fscanf(fin, "%s", tok) == 1) {
 		if (!strcmp("char", tok)) {
 			font_readchar(fn, fin);
 		} else if (!strcmp("kern", tok)) {
 			font_readkern(fn, fin);
+		} else if (!strcmp("ligatures", tok)) {
+			while (fscanf(fin, "%s", ligs[ligs_n]) == 1) {
+				if (!strcmp("0", ligs[ligs_n]))
+					break;
+				if (ligs_n < LEN(ligs))
+					ligs_n++;
+			}
+		} else if (!strcmp("gsub", tok)) {
+			font_readgsub(fn, fin);
+		} else if (!strcmp("gpos", tok)) {
+			font_readgpos(fn, fin);
 		} else if (!strcmp("spacewidth", tok)) {
 			fscanf(fin, "%d", &fn->spacewid);
 		} else if (!strcmp("special", tok)) {
@@ -232,13 +435,6 @@
 			fscanf(fin, "%s", fn->name);
 		} else if (!strcmp("fontname", tok)) {
 			fscanf(fin, "%s", fn->fontname);
-		} else if (!strcmp("ligatures", tok)) {
-			while (fscanf(fin, "%s", tok) == 1) {
-				if (!strcmp("0", tok))
-					break;
-				if (fn->lgn < NLIGS)
-					strcpy(fn->lg[fn->lgn++], tok);
-			}
 		} else if (!strcmp("charset", tok)) {
 			while (!font_readchar(fn, fin))
 				;
@@ -246,7 +442,15 @@
 		}
 		skipline(fin);
 	}
+	for (i = 0; i < ligs_n; i++)
+		font_lig(fn, ligs[i]);
 	fclose(fin);
+	for (i = 0; i < fn->gsub_n; i++)
+		fn->gsub[i].hash = grule_hash(&fn->gsub[i]);
+	for (i = 0; i < fn->gpos_n; i++)
+		fn->gpos[i].hash = grule_hash(&fn->gpos[i]);
+	qsort(fn->gsub, fn->gsub_n, sizeof(fn->gsub[0]), (void *) grulecmp);
+	qsort(fn->gpos, fn->gpos_n, sizeof(fn->gpos[0]), (void *) grulecmp);
 	return fn;
 }
 
@@ -285,4 +489,14 @@
 void font_setbd(struct font *fn, int bd)
 {
 	fn->bd = bd;
+}
+
+/* enable/disable font features; returns the previous value */
+int font_feat(struct font *fn, char *name, int val)
+{
+	int idx = font_findfeat(fn, name, 0);
+	int old = idx >= 0 ? fn->feat_set[idx] : 0;
+	if (idx >= 0)
+		fn->feat_set[idx] = val;
+	return old;
 }
--- a/roff.h
+++ b/roff.h
@@ -27,8 +27,6 @@
 #define NFILES		16	/* number of input files */
 #define NFONTS		32	/* number of fonts */
 #define NGLYPHS		1024	/* glyphs in fonts */
-#define NLIGS		128	/* number of font ligatures */
-#define NKERNS		1024	/* number of font kerning pairs */
 #define FNLEN		32	/* font name length */
 #define NMLEN		32	/* macro/register/environment/glyph name length */
 #define GNLEN		NMLEN	/* glyph name length */
@@ -46,12 +44,14 @@
 #define NSSTR		32	/* number of nested sstr_push() calls */
 #define NFIELDS		32	/* number of fields */
 #define MAXFRAC		100000	/* maximum value of the fractional part */
-#define LIGLEN		4	/* length of ligatures */
 #define NCDEFS		128	/* number of character definitions (.char) */
 #define NHYPHS		16384	/* hyphenation dictionary/patterns (.hw) */
 #define NHYPHSWORD	16	/* number of hyphenations per word */
 #define NHCODES		512	/* number of .hcode characters */
 #define WORDLEN		256	/* word length (for hyph.c) */
+#define NFEATS		128	/* number of features per font */
+#define NGRULES		1024	/* number of gsub/gpos rules per font */
+#define NGPATS		4096	/* number of gsub/gpos pattern glyphs */
 
 /* converting scales */
 #define SC_IN		(dev_res)	/* inch in units */
@@ -148,9 +148,9 @@
 	char id[GNLEN];		/* device-dependent glyph identifier */
 	char name[GNLEN];	/* the first character mapped to this glyph */
 	struct font *font;	/* glyph font */
-	int wid;		/* character width */
-	int type;		/* character type; ascender/descender */
-	int llx, lly, urx, ury;	/* character bounding box */
+	short wid;		/* character width */
+	short llx, lly, urx, ury;	/* character bounding box */
+	short type;		/* character type; ascender/descender */
 };
 
 /* output device functions */
@@ -166,9 +166,6 @@
 void font_close(struct font *fn);
 struct glyph *font_glyph(struct font *fn, char *id);
 struct glyph *font_find(struct font *fn, char *name);
-int font_lig(struct font *fn, char *lig, char src[][GNLEN], int n);
-int font_kern(struct font *fn, char *c1, char *c2);
-int font_islig(struct font *fn, char *s);
 int font_map(struct font *fn, char *name, struct glyph *gl);
 int font_mapped(struct font *fn, char *name);
 int font_special(struct font *fn);
@@ -177,6 +174,10 @@
 int font_getcs(struct font *fn);
 void font_setbd(struct font *fn, int bd);
 int font_getbd(struct font *fn);
+int font_feat(struct font *fn, char *name, int val);
+int font_layout(struct font *fn, struct glyph **src, int nsrc, int sz,
+		struct glyph **dst, int *dmap,
+		int *x, int *y, int *xadv, int *yadv);
 
 /* glyph handling functions */
 struct glyph *dev_glyph(char *c, int fn);
--- a/tr.c
+++ b/tr.c
@@ -456,6 +456,18 @@
 	font_setcs(dev_font(dev_pos(args[1])), args[2] ? eval(args[2], 0) : 0);
 }
 
+static void tr_ff(char **args)
+{
+	struct font *fn;
+	int i;
+	if (!args[2])
+		return;
+	fn = dev_font(dev_pos(args[1]));
+	for (i = 2; i <= NARGS; i++)
+		if (args[i] && args[i][0] && args[i][1])
+			font_feat(fn, args[i] + 1, args[i][0] == '+');
+}
+
 static void tr_nm(char **args)
 {
 	if (!args[1]) {
@@ -871,6 +883,7 @@
 	{"ev", tr_ev},
 	{"ex", tr_ex},
 	{"fc", tr_fc},
+	{"ff", tr_ff},
 	{"fi", tr_fi},
 	{"fmap", tr_fmap},
 	{"fp", tr_fp},
--- a/wb.c
+++ b/wb.c
@@ -136,7 +136,8 @@
 		g = dev_glyph(c, R_F(wb));
 	}
 	if (g && !zerowidth && wb->icleft && glyph_icleft(g))
-		wb_hmov(wb, SDEVWID(R_S(wb), glyph_icleft(g)));
+		sbuf_printf(&wb->sbuf, "%ch'%du'",
+			c_ec, SDEVWID(R_S(wb), glyph_icleft(g)));
 	wb->icleft = 0;
 	if (!c[1] || c[0] == c_ec || c[0] == c_ni || utf8one(c)) {
 		if (c[0] == c_ni && c[1] == c_ec)
@@ -196,55 +197,53 @@
 	return 0;
 }
 
-static int wb_layout(char src[][GNLEN], int src_n, int fn, int sz,
-			char dst[][GNLEN], int *kern, char *hyph)
-{
-	int dmap[WORDLEN];	/* the mapping from dst[] indices to src[] */
-	char src_hyph[WORDLEN];	/* hyphenation points in src[] */
-	int n = 0;		/* number of characters in dst[] */
-	int i, l;
-	if (!n_hy || wb_hyph(src, src_n, src_hyph, n_hy))
-		memset(src_hyph, 0, sizeof(src_hyph));
-	for (i = 0; i < src_n; i++) {
-		dmap[n] = i;
-		l = n_lg ? font_lig(dev_font(fn), dst[n], src + i, src_n - i) : 0;
-		if (l > 0)
-			i += l - 1;
-		else
-			strcpy(dst[n], src[i]);
-		n++;
-	}
-	kern[0] = 0;
-	for (i = 1; i < n; i++)
-		kern[i] = n_kn ? DEVWID(sz, font_kern(dev_font(fn),
-						dst[i - 1], dst[i])) : 0;
-	for (i = 0; i < n; i++)
-		hyph[i] = src_hyph[dmap[i]];
-	return n;
-}
-
 static void wb_flushsub(struct wb *wb)
 {
-	char dest[WORDLEN][GNLEN];
-	int kern[WORDLEN];
-	char hyph[WORDLEN];
+	struct font *fn;
+	struct glyph *gsrc[WORDLEN];
+	struct glyph *gdst[WORDLEN];
+	int x[WORDLEN], y[WORDLEN], xadv[WORDLEN], yadv[WORDLEN];
+	int dmap[WORDLEN];
+	char src_hyph[WORDLEN];
 	char hc[GNLEN];
-	int n;
-	int i;
-	if (wb->sub_n) {
-		n = wb_layout(wb->sub_c, wb->sub_n, wb->f, wb->s,
-				dest, kern, hyph);
-		charnext_str(hc, c_hc);
-		wb->sub_n = 0;
-		for (i = 0; i < n; i++) {
-			if (kern[i])
-				sbuf_printf(&wb->sbuf, "%ch'%du'", c_ec, kern[i]);
-			if (hyph[i])
-				sbuf_printf(&wb->sbuf, "%s", c_hc);
-			wb_putbuf(wb, dest[i]);
-		}
+	int dst_n, i;
+	int feat_kern, feat_liga;
+	if (!wb->sub_n) {
+		wb->icleft = 0;
+		return;
 	}
+	wb->sub_collect = 0;
+	fn = dev_font(wb->f);
+	if (!n_hy || wb_hyph(wb->sub_c, wb->sub_n, src_hyph, n_hy))
+		memset(src_hyph, 0, sizeof(src_hyph));
+	for (i = 0; i < wb->sub_n; i++)
+		gsrc[i] = font_find(fn, wb->sub_c[i]);
+	feat_kern = font_feat(fn, "kern", n_kn);
+	feat_liga = font_feat(fn, "liga", n_lg);
+	dst_n = font_layout(fn, gsrc, wb->sub_n, wb->s,
+			gdst, dmap, x, y, xadv, yadv);
+	font_feat(fn, "kern", feat_kern);
+	font_feat(fn, "liga", feat_liga);
+	charnext_str(hc, c_hc);
+	for (i = 0; i < dst_n; i++) {
+		if (x[i])
+			sbuf_printf(&wb->sbuf, "%ch'%du'", c_ec, x[i]);
+		if (y[i])
+			sbuf_printf(&wb->sbuf, "%cv'%du'", c_ec, y[i]);
+		if (src_hyph[dmap[i]])
+			sbuf_printf(&wb->sbuf, "%s", hc);
+		if (gdst[i] == gsrc[dmap[i]])
+			wb_putbuf(wb, wb->sub_c[dmap[i]]);
+		else
+			wb_putbuf(wb, gdst[i]->name);
+		if (x[i] || xadv[i])
+			sbuf_printf(&wb->sbuf, "%ch'%du'", c_ec, xadv[i] - x[i]);
+		if (y[i] || yadv[i])
+			sbuf_printf(&wb->sbuf, "%cv'%du'", c_ec, yadv[i] - y[i]);
+	}
+	wb->sub_n = 0;
 	wb->icleft = 0;
+	wb->sub_collect = 1;
 }
 
 void wb_put(struct wb *wb, char *c)
@@ -260,10 +259,14 @@
 	}
 	if (wb_pendingfont(wb) || wb->sub_n == LEN(wb->sub_c))
 		wb_flush(wb);
-	if (wb->sub_collect)
-		strcpy(wb->sub_c[wb->sub_n++], c);
-	else
+	if (wb->sub_collect) {
+		if (font_find(dev_font(wb->f), c))
+			strcpy(wb->sub_c[wb->sub_n++], c);
+		else
+			wb_putraw(wb, c);
+	} else {
 		wb_putbuf(wb, c);
+	}
 }
 
 /* just like wb_put() but disable subword collection */