From: ben Date: Sat, 3 Feb 2007 14:08:09 +0000 (+0000) Subject: When adding a new file, it helps to "svn add" it. X-Git-Url: https://git.distorted.org.uk/~mdw/sgt/halibut/commitdiff_plain/2729eafc11bc11588764711335273a15d8cc226c When adding a new file, it helps to "svn add" it. git-svn-id: svn://svn.tartarus.org/sgt/halibut@7200 cda61777-01e9-0310-a592-d414129be87e --- diff --git a/in_sfnt.c b/in_sfnt.c new file mode 100644 index 0000000..43580e6 --- /dev/null +++ b/in_sfnt.c @@ -0,0 +1,678 @@ +/* + * Support for sfnt-housed fonts for Halibut + * + * sfnt-housed fonts include TrueType, OpenType, sfnt-housed Type 1 + * fonts and a couple of bitmap formats. + * + * The various tables that can appear in sfnt-housed fonts are defined + * in several places. These include: + * + * The OpenType Specification: + * + * + * The TrueType Reference Manual: + * + */ + +#include +#include +#include +#include +#include "halibut.h" +#include "paper.h" + +typedef struct sfnt_decode_Tag sfnt_decode; +struct sfnt_decode_Tag { + void (*decoder)(void *src, void *dest); + size_t src_len; + size_t dest_offset; +}; + +static void decode_uint8(void *src, void *dest) { + *(unsigned int *)dest = *(unsigned char *)src; +} +#define d_uint8 decode_uint8, 1 + +static void decode_int8(void *src, void *dest) { + *(int *)dest = *(signed char *)src; +} +#define d_int8 decode_int8, 1 + +static void decode_uint16(void *src, void *dest) { + unsigned char *cp = src; + *(unsigned int *)dest = (cp[0] << 8) + cp[1]; +} +#define d_uint16 decode_uint16, 2 + +static void decode_int16(void *src, void *dest) { + signed char *cp = src; + unsigned char *ucp = src; + *(int *)dest = (cp[0] << 8) + ucp[1]; +} +#define d_int16 decode_int16, 2 + +static void decode_uint32(void *src, void *dest) { + unsigned char *cp = src; + *(unsigned int *)dest = + (cp[0] << 24) + (cp[1] << 16) + (cp[2] << 8) + cp[3]; +} +#define d_uint32 decode_uint32, 4 + +static void decode_int32(void *src, void *dest) { + signed char *cp = src; + unsigned char *ucp = src; + *(int *)dest = (cp[0] << 24) + (ucp[1] << 16) + (ucp[2] << 8) + ucp[3]; +} +#define d_int32 decode_int32, 4 + +static void decode_skip(void *src, void *dest) { + /* do nothing */ +} +#define d_skip(n) decode_skip, (n), 0 + +static void decode_end(void *src, void *dest) { + /* never called */ +} +#define d_end decode_end, 0, 0 + +static void *decode(sfnt_decode *dec, void *src, void *end, void *dest) { + while (dec->decoder != decode_end) { + if ((char *)src + dec->src_len > (char *)end) return NULL; + dec->decoder(src, (char *)dest + dec->dest_offset); + src = (char *)src + dec->src_len; + dec++; + } + return src; +} + +static void *decoden(sfnt_decode *dec, void *src, void *end, void *dest, + size_t size, size_t n) { + while (n-- && src) { + src = decode(dec, src, end, dest); + dest = (char *)dest + size; + } + return src; +} + +/* Decoding specs for simple data types */ +sfnt_decode uint16_decode[] = { { d_uint16, 0 }, { d_end } }; +sfnt_decode int16_decode[] = { { d_int16, 0 }, { d_end } }; +sfnt_decode uint32_decode[] = { { d_uint32, 0 }, { d_end } }; + +/* Offset subdirectory -- the start of the file */ +typedef struct offsubdir_Tag offsubdir; +struct offsubdir_Tag { + unsigned scaler_type; + unsigned numTables; +}; +sfnt_decode offsubdir_decode[] = { + { d_uint32, offsetof(offsubdir, scaler_type) }, + { d_uint16, offsetof(offsubdir, numTables) }, + { d_skip(6) }, + { d_end } +}; + +#define sfnt_00010000 0x00010000 +#define TAG_cmap 0x636d6170 +#define TAG_glyf 0x676c7966 +#define TAG_head 0x68656164 +#define TAG_hhea 0x68686561 +#define TAG_hmtx 0x686d7478 +#define TAG_loca 0x6c6f6361 +#define TAG_name 0x6e616d65 +#define TAG_post 0x706f7374 +#define sfnt_true 0x74727565 + +/* Table directory */ +typedef struct tabledir_Tag tabledir; +struct tabledir_Tag { + unsigned tag; + unsigned checkSum; + unsigned offset; + unsigned length; +}; +sfnt_decode tabledir_decode[] = { + { d_uint32, offsetof(tabledir, tag) }, + { d_uint32, offsetof(tabledir, checkSum) }, + { d_uint32, offsetof(tabledir, offset) }, + { d_uint32, offsetof(tabledir, length) }, + { d_end } +}; + +/* Character to Glyph ('cmap') table */ +typedef struct t_cmap_Tag t_cmap; +struct t_cmap_Tag { + unsigned numTables; +}; +sfnt_decode t_cmap_decode[] = { + { d_skip(2) }, + { d_uint16, offsetof(t_cmap, numTables) }, + { d_end } +}; +typedef struct encodingrec_Tag encodingrec; +struct encodingrec_Tag { + unsigned platformID; + unsigned encodingID; + unsigned offset; +}; +sfnt_decode encodingrec_decode[] = { + { d_uint16, offsetof(encodingrec, platformID) }, + { d_uint16, offsetof(encodingrec, encodingID) }, + { d_uint32, offsetof(encodingrec, offset) }, + { d_end } +}; +typedef struct cmap4_Tag cmap4; +struct cmap4_Tag { + unsigned length; + unsigned segCountX2; +}; +sfnt_decode cmap4_decode[] = { + { d_skip(2) }, /* format */ + { d_uint16, offsetof(cmap4, length) }, + { d_skip(2) }, /* language */ + { d_uint16, offsetof(cmap4, segCountX2) }, + { d_skip(6) }, /* searchRange, entrySelector, rangeShift */ + { d_end } +}; + +/* Font Header ('head') table */ +typedef struct t_head_Tag t_head; +struct t_head_Tag { + unsigned version; + unsigned fontRevision; + unsigned flags; + unsigned unitsPerEm; + int xMin, yMin, xMax, yMax; + int indexToLocFormat; +}; +sfnt_decode t_head_decode[] = { + { d_uint32, offsetof(t_head, version) }, + { d_uint32, offsetof(t_head, fontRevision) }, + { d_skip(8) }, /* checkSumAdjustment, magicNumber, flags */ + { d_uint16, offsetof(t_head, flags) }, + { d_uint16, offsetof(t_head, unitsPerEm) }, + { d_skip(16) }, /* created, modified */ + { d_int16, offsetof(t_head, xMin) }, + { d_int16, offsetof(t_head, yMin) }, + { d_int16, offsetof(t_head, xMax) }, + { d_int16, offsetof(t_head, yMax) }, + { d_skip(6) }, /* macStyle, lowestRecPPEM, fontDirectionHint */ + { d_int16, offsetof(t_head, indexToLocFormat) }, + { d_skip(2) }, + { d_end } +}; + +/* Horizontal Header ('hhea') table */ +typedef struct t_hhea_Tag t_hhea; +struct t_hhea_Tag { + unsigned version; + int ascent; + int descent; + int lineGap; + int metricDataFormat; + unsigned numOfLongHorMetrics; +}; +sfnt_decode t_hhea_decode[] = { + { d_uint32, offsetof(t_hhea, version) }, + { d_int16, offsetof(t_hhea, ascent) }, + { d_int16, offsetof(t_hhea, descent) }, + { d_int16, offsetof(t_hhea, lineGap) }, + { d_skip(22) }, + { d_int16, offsetof(t_hhea, metricDataFormat) }, + { d_uint16, offsetof(t_hhea, numOfLongHorMetrics) }, + { d_end } +}; + +/* Horizontal Metrics ('hmtx') table */ +sfnt_decode longhormetric_decode[] = { + { d_uint16, 0 }, + { d_skip(2) }, + { d_end } +}; + +/* Naming ('name') table */ +typedef struct t_name_Tag t_name; +typedef struct namerecord_Tag namerecord; +struct t_name_Tag { + unsigned format; + unsigned count; + unsigned stringOffset; + namerecord *nameRecord; +}; +sfnt_decode t_name_decode[] = { + { d_uint16, offsetof(t_name, format) }, + { d_uint16, offsetof(t_name, count) }, + { d_uint16, offsetof(t_name, stringOffset) }, + { d_end } +}; +struct namerecord_Tag { + unsigned platformID; + unsigned encodingID; + unsigned languageID; + unsigned nameID; + unsigned length; + unsigned offset; +}; +sfnt_decode namerecord_decode[] = { + { d_uint16, offsetof(namerecord, platformID) }, + { d_uint16, offsetof(namerecord, encodingID) }, + { d_uint16, offsetof(namerecord, languageID) }, + { d_uint16, offsetof(namerecord, nameID) }, + { d_uint16, offsetof(namerecord, length) }, + { d_uint16, offsetof(namerecord, offset) }, + { d_end } +}; + +/* PostScript compatibility ('post') table */ +typedef struct t_post_Tag t_post; +struct t_post_Tag { + unsigned format; + unsigned italicAngle; + int underlinePosition; + int underlineThickness; + unsigned isFixedPitch; + unsigned minMemType42; + unsigned maxMemType42; +}; +sfnt_decode t_post_decode[] = { + { d_uint32, offsetof(t_post, format) }, + { d_uint32, offsetof(t_post, italicAngle) }, + { d_int16, offsetof(t_post, underlinePosition) }, + { d_int16, offsetof(t_post, underlineThickness) }, + { d_uint32, offsetof(t_post, isFixedPitch) }, + { d_uint32, offsetof(t_post, minMemType42) }, + { d_uint32, offsetof(t_post, maxMemType42) }, + { d_skip(8) }, /* minMemType1, maxMemType1 */ + { d_end } +}; + +typedef struct { + glyph name; + unsigned short index; +} glyphmap; + +typedef struct sfnt_Tag sfnt; +struct sfnt_Tag { + void *data; + size_t len; + void *end; + offsubdir osd; + tabledir *td; + t_head head; + unsigned nglyphs; + glyph *glyphsbyindex; + unsigned short *glyphsbyname; + unsigned minmem, maxmem; +}; + +static int sfnt_findtable(sfnt *sf, unsigned tag, + void **startp, void **endp) { + size_t i; + + for (i = 0; i < sf->osd.numTables; i++) { + if (sf->td[i].tag == tag) { + *startp = (char *)sf->data + sf->td[i].offset; + *endp = (char *)*startp + sf->td[i].length; + return TRUE; + } + } + return FALSE; +} + +static char *sfnt_psname(font_info *fi) { + sfnt *sf = fi->fontfile; + t_name name; + void *ptr, *end; + size_t i; + char *psname; + namerecord *nr; + + if (!sfnt_findtable(sf, TAG_name, &ptr, &end)) + abort(); + ptr = decode(t_name_decode, ptr, end, &name); + name.nameRecord = snewn(name.count, namerecord); + ptr = decoden(namerecord_decode, ptr, sf->end, name.nameRecord, + sizeof(*name.nameRecord), name.count); + for (i = 0; i < name.count; i++) { + nr = name.nameRecord + i; + if (nr->nameID == 6) { + /* PostScript name, but can we make sense of it? */ + if (nr->platformID == 1 && nr->encodingID == 0) { + /* Mac Roman, which is ASCII for our purposes */ + psname = snewn(nr->length + 1, char); + memcpy(psname, (char *)ptr + nr->offset, nr->length); + psname[nr->length] = 0; + sfree(name.nameRecord); + return psname; + } + } + } + return NULL; +} + +/* + * Extract data from the 'post' table (mostly glyph mappings) + * + * TODO: cope better with duplicated glyph names (usually .notdef) + * TODO: when presented with format 3.0, try to use 'CFF' if present. + */ +static void sfnt_mapglyphs(sfnt *sf) { + t_post post; + void *ptr, *end; + unsigned char *sptr, tmp[256]; + glyph *extraglyphs; + unsigned nextras, i, g; + + sf->glyphsbyname = sf->glyphsbyindex = NULL; + if (!sfnt_findtable(sf, TAG_post, &ptr, &end)) + abort(); + ptr = decode(t_post_decode, ptr, end, &post); + sf->minmem = post.minMemType42; + sf->maxmem = post.maxMemType42; + if (ptr == NULL) abort(); + switch (post.format) { + case 0x00010000: + sf->nglyphs = 258; + sf->glyphsbyindex = (glyph *)tt_std_glyphs; + break; + case 0x00020000: + if ((char *)ptr + 2 > end) return; + decode_uint16(ptr, &sf->nglyphs); + ptr = (char *)ptr + 2; + if ((char *)ptr + 2*sf->nglyphs > end) return; + nextras = 0; + for (sptr = (char *)ptr + 2*sf->nglyphs; sptr < end; sptr += *sptr+1) + nextras++; + extraglyphs = snewn(nextras, glyph); + i = 0; + for (sptr = (char *)ptr + 2*sf->nglyphs; sptr < end; sptr += *sptr+1) { + memcpy(tmp, sptr + 1, *sptr); + tmp[*sptr] = 0; + assert(i < nextras); + extraglyphs[i++] = glyph_intern(tmp); + } + sf->glyphsbyindex = snewn(sf->nglyphs, glyph); + for (i = 0; i < sf->nglyphs; i++) { + decode_uint16((char *)ptr + 2*i, &g); + if (g <= 257) + sf->glyphsbyindex[i] = tt_std_glyphs[g]; + else if (g < 258 + nextras) + sf->glyphsbyindex[i] = extraglyphs[g - 258]; + else + sf->glyphsbyindex[i] = NOGLYPH; + } + sfree(extraglyphs); + break; + default: + abort(); + } +} + +/* + * Get data from 'hhea' and 'hmtx' tables + */ +void sfnt_getmetrics(font_info *fi) { + sfnt *sf = fi->fontfile; + t_hhea hhea; + void *ptr, *end; + unsigned i, j; + unsigned *hmtx; + + if (!sfnt_findtable(sf, TAG_hhea, &ptr, &end)) + abort(); + if (decode(t_hhea_decode, ptr, end, &hhea) == NULL) + abort(); + if ((hhea.version & 0xffff0000) != 0x00010000) + abort(); + fi->ascent = hhea.ascent; + fi->descent = hhea.descent; + if (hhea.metricDataFormat != 0) + abort(); + if (!sfnt_findtable(sf, TAG_hmtx, &ptr, &end)) + abort(); + hmtx = snewn(hhea.numOfLongHorMetrics, unsigned); + if (decoden(longhormetric_decode, ptr, end, hmtx, sizeof(*hmtx), + hhea.numOfLongHorMetrics) == NULL) + abort(); + for (i = 0; i < sf->nglyphs; i++) { + glyph_width *w = snew(glyph_width); + w->glyph = sf->glyphsbyindex[i]; + j = i < hhea.numOfLongHorMetrics ? i : hhea.numOfLongHorMetrics - 1; + w->width = hmtx[j] * UNITS_PER_PT / sf->head.unitsPerEm; + add234(fi->widths, w); + } +} + +/* + * Get mapping data from 'cmap' table + * + * We look for either a (0, 0), (0, 1), (0, 2), (0, 3), or (3, 1) table, + * all of these being versions of UCS-2. We only handle format 4 of this + * table, since that seems to be the only one in use. + */ +void sfnt_getmap(font_info *fi) { + sfnt *sf = fi->fontfile; + t_cmap cmap; + encodingrec *esd; + void *base, *ptr, *end; + unsigned i; + unsigned format; + + + for (i = 0; i < lenof(fi->bmp); i++) + fi->bmp[i] = 0xFFFF; + if (!sfnt_findtable(sf, TAG_cmap, &ptr, &end)) + abort(); + base = ptr; + ptr = decode(t_cmap_decode, ptr, end, &cmap); + if (ptr == NULL) abort(); + esd = snewn(cmap.numTables, encodingrec); + ptr = decoden(encodingrec_decode, ptr, end, esd, sizeof(*esd), + cmap.numTables); + if (ptr == NULL) abort(); + for (i = 0; i < cmap.numTables; i++) { + if (!decode(uint16_decode, (char *)base + esd[i].offset, end, &format)) + abort(); + if ((esd[i].platformID == 0 && esd[i].encodingID == 3) || + (esd[i].platformID == 3 && esd[i].encodingID == 1)) { + /* UCS-2 encoding */ + if (!decode(uint16_decode, (char *)base + esd[i].offset, end, + &format)) + abort(); + if (format == 4) { + unsigned *data, *endCode, *startCode, *idDelta, *idRangeOffset; + unsigned *glyphIndexArray; + unsigned segcount, nword, nglyphindex, j; + cmap4 cmap4; + + ptr = decode(cmap4_decode, (char *)base + esd[i].offset, end, + &cmap4); + if (!ptr) abort(); + segcount = cmap4.segCountX2 / 2; + nword = cmap4.length / 2 - 7; + data = snewn(nword, unsigned); + if (!decoden(uint16_decode, ptr, (char *)ptr + nword * 2, + data, sizeof(*data), nword)) abort(); + endCode = data; + startCode = data + segcount + 1; + idDelta = startCode + segcount; + idRangeOffset = idDelta + segcount; + glyphIndexArray = idRangeOffset + segcount; + nglyphindex = nword - segcount * 4 - 1; + + for (j = 0; j < segcount; j++) { + unsigned k, idx; + + if (idRangeOffset[j] == 0) { + for (k = startCode[j]; k <= endCode[j]; k++) { + idx = (k + idDelta[j]) & 0xffff; + if (idx != 0) { + if (idx > sf->nglyphs) abort(); + fi->bmp[k] = sf->glyphsbyindex[idx]; + } + } + } else { + unsigned startidx = idRangeOffset[j]/2 - segcount + j; + for (k = startCode[j]; k <= endCode[j]; k++) { + idx = glyphIndexArray[startidx + k - startCode[j]]; + if (idx != 0) { + idx = (idx + idDelta[j]) & 0xffff; + if (idx > sf->nglyphs) abort(); + fi->bmp[k] = sf->glyphsbyindex[idx]; + } + } + } + } + + sfree(data); + } + } + } +} + +void read_sfnt_file(input *in) { + sfnt *sf = snew(sfnt); + size_t off = 0, got; + FILE *fp = in->currfp; + font_info *fi = snew(font_info); + size_t i; + void *ptr, *end; + + fi->name = NULL; + fi->widths = newtree234(width_cmp); + fi->kerns = newtree234(kern_cmp); + fi->ligs = newtree234(lig_cmp); + fi->fontbbox[0] = fi->fontbbox[1] = fi->fontbbox[2] = fi->fontbbox[3] = 0; + fi->capheight = fi->xheight = fi->ascent = fi->descent = 0; + fi->stemh = fi->stemv = fi->italicangle = 0; + fi->fontfile = sf; + fi->filetype = TRUETYPE; + + sf->len = 32768; + sf->data = snewn(sf->len, unsigned char); + for (;;) { + got = fread((char *)sf->data + off, 1, sf->len - off, fp); + off += got; + if (off != sf->len) break; + sf->len *= 2; + sf->data = sresize(sf->data, sf->len, unsigned char); + } + sf->len = off; + sf->data = sresize(sf->data, sf->len, unsigned char); + sf->end = (char *)sf->data + sf->len; + decode(offsubdir_decode, sf->data, sf->end, &sf->osd); +/* + fprintf(stderr, "scaler type = 0x%x; numTables = %u\n", + sf->osd.scaler_type, sf->osd.numTables); +*/ + sf->td = snewn(sf->osd.numTables, tabledir); + decoden(tabledir_decode, (char *)sf->data + 12, sf->end, + sf->td, sizeof(*sf->td), sf->osd.numTables); +/* + for (i = 0; i < sf->osd.numTables; i++) + fprintf(stderr, "table tag = '%c%c%c%c'; offset = %#10x; length = %#10x\n", + sf->td[i].tag >> 24, sf->td[i].tag >> 16, sf->td[i].tag >> 8, sf->td[i].tag, sf->td[i].offset, sf->td[i].length); +*/ + if (!sfnt_findtable(sf, TAG_head, &ptr, &end)) + abort(); + if (decode(t_head_decode, ptr, end, &sf->head) == NULL) + abort(); + if ((sf->head.version & 0xffff0000) != 0x00010000) + abort(); + fi->name = sfnt_psname(fi); + sfnt_mapglyphs(sf); + sfnt_getmetrics(fi); + sfnt_getmap(fi); + fi->next = all_fonts; + all_fonts = fi; + fclose(in->currfp); +} + +static int sizecmp(const void *a, const void *b) { + if (*(size_t *)a < *(size_t *)b) return -1; + if (*(size_t *)a > *(size_t *)b) return 1; + return 0; +} + +/* + * The format for embedding TrueType fonts in Postscript is defined in + * Adobe Technical Note #5012: The Type 42 Font Format Specification. + * + */ + +void sfnt_writeps(font_info const *fi, FILE *ofp) { + unsigned i, j, lastbreak; + sfnt *sf = fi->fontfile; + size_t *breaks, glyfoff, glyflen; + void *glyfptr, *glyfend, *locaptr, *locaend; + unsigned *loca; + + /* XXX Unclear that this is the correct format. */ + fprintf(ofp, "%%!PS-TrueTypeFont-%u-%u\n", sf->osd.scaler_type, + sf->head.fontRevision); + if (sf->minmem) + fprintf(ofp, "%%%%VMUsage: %u %u\n", sf->minmem, sf->maxmem); + fprintf(ofp, "9 dict dup begin\n"); + fprintf(ofp, "/FontType 42 def\n"); + fprintf(ofp, "/FontMatrix [1 0 0 1 0 0] def\n"); + fprintf(ofp, "/FontName /%s def\n", fi->name); + fprintf(ofp, "/Encoding StandardEncoding def\n"); + if ((sf->head.flags & 0x0003) == 0x0003) { + /* + * Sensible font with the origin in the right place, such that + * the bounding box is meaningful. + */ + fprintf(ofp, "/FontBBox [%g %g %g %g] readonly def\n", + (double)sf->head.xMin / sf->head.unitsPerEm, + (double)sf->head.yMin / sf->head.unitsPerEm, + (double)sf->head.xMax / sf->head.unitsPerEm, + (double)sf->head.yMax / sf->head.unitsPerEm); + } else { + /* Non-sensible font. */ + fprintf(ofp, "/FontBBox [0 0 0 0] readonly def"); + } + fprintf(ofp, "/PaintType 0 def\n"); + fprintf(ofp, "/CharStrings %u dict dup begin\n", sf->nglyphs); + for (i = 0; i < sf->nglyphs; i++) + fprintf(ofp, "/%s %u def\n", glyph_extern(sf->glyphsbyindex[i]), i); + fprintf(ofp, "end readonly def\n"); + fprintf(ofp, "/sfnts [<"); + breaks = snewn(sf->osd.numTables + sf->nglyphs, size_t); + for (i = 0; i < sf->osd.numTables; i++) { + breaks[i] = sf->td[i].offset; + } + if (!sfnt_findtable(sf, TAG_glyf, &glyfptr, &glyfend)) + abort(); + glyfoff = (char *)glyfptr - (char *)sf->data; + glyflen = (char *)glyfend - (char *)glyfptr; + if (!sfnt_findtable(sf, TAG_loca, &locaptr, &locaend)) + abort(); + loca = snewn(sf->nglyphs, unsigned); + if (sf->head.indexToLocFormat == 0) { + if (!decoden(uint16_decode, locaptr, locaend, loca, sizeof(*loca), + sf->nglyphs)) abort(); + for (i = 0; i < sf->nglyphs; i++) loca[i] *= 2; + } else { + if (!decoden(uint32_decode, locaptr, locaend, loca, sizeof(*loca), + sf->nglyphs)) abort(); + } + for (i = 1; i < sf->nglyphs; i++) { + if (loca[i] > glyflen) abort(); + breaks[sf->osd.numTables + i - 1] = loca[i] + glyfoff; + } + breaks[sf->osd.numTables + sf->nglyphs - 1] = sf->len; + qsort(breaks, sf->osd.numTables + sf->nglyphs, sizeof(*breaks), sizecmp); + j = lastbreak = 0; + for (i = 0; i < sf->len; i++) { + if ((i - lastbreak) % 38 == 0) fprintf(ofp, "\n"); + if (i == breaks[j]) { + while (i == breaks[j]) j++; + lastbreak = i; + fprintf(ofp, "00><\n"); + } + fprintf(ofp, "%02x", *((unsigned char *)sf->data + i)); + } + fprintf(ofp, "00>] readonly def\n"); + sfree(breaks); + fprintf(ofp, "end /%s exch definefont\n", fi->name); +}