add tool to store a BDF font in a more terse format and edit a font
authorThorsten Glaser <tg@mirbsd.org>
Sun, 2 Sep 2012 00:35:42 +0000 (00:35 +0000)
committerThorsten Glaser <tg@mirbsd.org>
Sun, 2 Sep 2012 00:35:42 +0000 (00:35 +0000)
with manpage; as used in the MirBSD XFree86® build process

mksh/bdfctool.1 [new file with mode: 0644]
mksh/bdfctool.sh [new file with mode: 0644]

diff --git a/mksh/bdfctool.1 b/mksh/bdfctool.1
new file mode 100644 (file)
index 0000000..6bceeeb
--- /dev/null
@@ -0,0 +1,393 @@
+.\" $MirOS: X11/extras/bdfctool/bdfctool.1,v 1.9 2012/09/01 21:15:29 tg Exp $
+.\"-
+.\" Copyright © 2012
+.\"    Thorsten “mirabilos” Glaser <tg@mirbsd.org>
+.\"-
+.\" Try to make GNU groff and AT&T nroff more compatible
+.\" * ` generates ‘ in gnroff, so use \`
+.\" * ' generates ’ in gnroff, \' generates ´, so use \*(aq
+.\" * - generates ‐ in gnroff, \- generates −, so .tr it to -
+.\"   thus use - for hyphens and \- for minus signs and option dashes
+.\" * ~ is size-reduced and placed atop in groff, so use \*(TI
+.\" * ^ is size-reduced and placed atop in groff, so use \*(ha
+.\" * \(en does not work in nroff, so use \*(en
+.\" * <>| are problematic, so redefine and use \*(Lt\*(Gt\*(Ba
+.\" Also make sure to use \& especially with two-letter words.
+.\" The section after the "doc" macropackage has been loaded contains
+.\" additional code to convene between the UCB mdoc macropackage (and
+.\" its variant as BSD mdoc in groff) and the GNU mdoc macropackage.
+.\"
+.ie \n(.g \{\
+.      if \ 1\*[.T]\ 1ascii\ 1 .tr \-\N'45'
+.      if \ 1\*[.T]\ 1latin1\ 1 .tr \-\N'45'
+.      if \ 1\*[.T]\ 1utf8\ 1 .tr \-\N'45'
+.      ds <= \[<=]
+.      ds >= \[>=]
+.      ds Rq \[rq]
+.      ds Lq \[lq]
+.      ds sL \(aq
+.      ds sR \(aq
+.      if \ 1\*[.T]\ 1utf8\ 1 .ds sL `
+.      if \ 1\*[.T]\ 1ps\ 1 .ds sL `
+.      if \ 1\*[.T]\ 1utf8\ 1 .ds sR '
+.      if \ 1\*[.T]\ 1ps\ 1 .ds sR '
+.      ds aq \(aq
+.      ds TI \(ti
+.      ds ha \(ha
+.      ds en \(en
+.\}
+.el \{\
+.      ds aq '
+.      ds TI ~
+.      ds ha ^
+.      ds en \(em
+.\}
+.\"
+.\" Implement .Dd with the Mdocdate RCS keyword
+.\"
+.rn Dd xD
+.de Dd
+.ie \a\\$1\a$Mdocdate:\a \{\
+.      xD \\$2 \\$3, \\$4
+.\}
+.el .xD \\$1 \\$2 \\$3 \\$4 \\$5 \\$6 \\$7 \\$8
+..
+.\"
+.\" .Dd must come before definition of .Mx, because when called
+.\" with -mandoc, it might implement .Mx itself, but we want to
+.\" use our own definition. And .Dd must come *first*, always.
+.\"
+.Dd $Mdocdate: September 1 2012 $
+.\"
+.\" Check which macro package we use, and do other -mdoc setup.
+.\"
+.ie \n(.g \{\
+.      if \ 1\*[.T]\ 1utf8\ 1 .tr \[la]\*(Lt
+.      if \ 1\*[.T]\ 1utf8\ 1 .tr \[ra]\*(Gt
+.      ie d volume-ds-1 .ds tT gnu
+.      el .ds tT bsd
+.\}
+.el .ds tT ucb
+.\"
+.\" Implement .Mx (MirBSD)
+.\"
+.ie "\*(tT"gnu" \{\
+.      eo
+.      de Mx
+.      nr curr-font \n[.f]
+.      nr curr-size \n[.ps]
+.      ds str-Mx \f[\n[curr-font]]\s[\n[curr-size]u]
+.      ds str-Mx1 \*[Tn-font-size]\%MirOS\*[str-Mx]
+.      if !\n[arg-limit] \
+.      if \n[.$] \{\
+.      ds macro-name Mx
+.      parse-args \$@
+.      \}
+.      if (\n[arg-limit] > \n[arg-ptr]) \{\
+.      nr arg-ptr +1
+.      ie (\n[type\n[arg-ptr]] == 2) \
+.      as str-Mx1 \~\*[arg\n[arg-ptr]]
+.      el \
+.      nr arg-ptr -1
+.      \}
+.      ds arg\n[arg-ptr] "\*[str-Mx1]
+.      nr type\n[arg-ptr] 2
+.      ds space\n[arg-ptr] "\*[space]
+.      nr num-args (\n[arg-limit] - \n[arg-ptr])
+.      nr arg-limit \n[arg-ptr]
+.      if \n[num-args] \
+.      parse-space-vector
+.      print-recursive
+..
+.      ec
+.      ds sP \s0
+.      ds tN \*[Tn-font-size]
+.\}
+.el \{\
+.      de Mx
+.      nr cF \\n(.f
+.      nr cZ \\n(.s
+.      ds aa \&\f\\n(cF\s\\n(cZ
+.      if \\n(aC==0 \{\
+.              ie \\n(.$==0 \&MirOS\\*(aa
+.              el .aV \\$1 \\$2 \\$3 \\$4 \\$5 \\$6 \\$7 \\$8 \\$9
+.      \}
+.      if \\n(aC>\\n(aP \{\
+.              nr aP \\n(aP+1
+.              ie \\n(C\\n(aP==2 \{\
+.                      as b1 \&MirOS\ #\&\\*(A\\n(aP\\*(aa
+.                      ie \\n(aC>\\n(aP \{\
+.                              nr aP \\n(aP+1
+.                              nR
+.                      \}
+.                      el .aZ
+.              \}
+.              el \{\
+.                      as b1 \&MirOS\\*(aa
+.                      nR
+.              \}
+.      \}
+..
+.\}
+.\"-
+.Dt BDFCTOOL 1
+.Os MirBSD
+.Sh NAME
+.Nm bdfctool
+.Nd convert BDF and bdfc font files
+.Sh SYNOPSIS
+.Nm
+.Fl c
+.Nm
+.Fl d
+.Op Fl F
+.Nm
+.Fl e
+.Op Fl a
+.Sh DESCRIPTION
+The
+.Nm
+utility converts (mostly) fixed-width bitmap fonts between the
+.Tn BDF
+file format as used by
+.Tn XFree86\(rg
+and the
+.Ic bdfc
+format as specified below.
+It operates as a filter, i.e. takes its input from the standard
+input stream and writes data to standard output.
+.Pp
+The options are as follows:
+.Bl -tag -width XXX
+.It Fl a
+In edit mode, emit ASCII (1:2) encoding for an unset bit
+.Pq Sq Li \&. ,
+a set bit
+.Pq Sq Li \&#
+and the line end separator
+.Pq Sq Li \&\*(Ba .
+.It Ic +a
+In edit mode, emit Unicode (1:1) encoding (default).
+.It Fl d
+Decompress the font from bdfc into
+.Tn BDF .
+.It Fl c
+Compress the font from
+.Tn BDF
+or the bdfc edit form to bdfc, also sorting and weeding out
+any duplicates (later occurrence wins).
+.It Fl e
+Expand selected glyphs inside the bdfc file into the edit form,
+which uses U+3000 and U+4DC0 to represent unset and set bits,
+respectively, so they can be visually edited.
+This mode operates on glyphs and does not need to be passed the
+whole file, e.g. using \*(haK/ in the jupp text editor.
+.It Fl F
+Do a fast decompression with no error checking.
+Run this on files passed through
+.Nm
+.Fl c
+.Em only .
+Used by the
+.Mx
+.Tn XFree86\(rg
+build process.
+.El
+.Sh BDFC FORMAT DESCRIPTION
+A
+.Ic \&.bdfc
+file is a compressed, editable representation of a subset of the
+.Ic Bitmap Distribution Format Pq BDF
+as used for fixed-width fonts in the
+.Tn XFree86\(rg
+windowing system.
+.Pp
+Every file starts with a line consisting of
+.Dq Li "=bdfc 1" ,
+where
+.Ql \&1
+is the version number.
+The format is line-oriented and only somewhat stateful.
+It is optimised for being operated on using the jupp text editor and
+.Nm mksh
+shell scripts.
+Lines starting with an apostrophe U+0027 and a space U+0020, or
+consisting of only an apostrophe before the newline, can be
+used anywhere inside the file, except within the trailing-data lines
+of an edit block (see below), to denote a comment, which is retained
+(tacked on to the following character).
+.Pp
+Next comes a block of font header information that are just
+passed through, prefixed with a
+.Dq Li h .
+After that, list the font properties, prefixed with a
+.Dq Li p
+each, and followed by a
+.Dq Li C
+on a line by itself, which will deal with emitting the
+.Li STARTPROPERTIES
+number, the properties and
+.Li ENDPROPERTIES
+and marks the place where
+.Li CHARS
+is put in
+.Tn BDF .
+.Pp
+Finally, there is the character block, which is somewhat stateless.
+There are two types of entries for that block, glyph defaults and glyph data.
+The block is ended with a period
+.Pq Dq Li .\&
+on a line by itself.
+.Pp
+Glyphs are sorted by their font encoding / Unicode code point, and each
+glyph occurs only once, although the
+.Nm
+tool in the
+.Fl c
+operation mode is able to take glyphs in any order and weed out duplicates.
+The character name can be omitted if it matches the form
+.Dq Li uni Ns Ar 20AC
+where
+.Dq Ar 20AC
+is the four-nibble uppercase Unicode codepoint of the glyph, in this
+example the Euro sign.
+.Pp
+Glyph defaults are lines in the format
+.Dl d 540 0 9 0 0 \-4
+where the first
+.Dq Li d
+is the line type, and the next values are, in order, the arguments to the
+.Li SWIDTH
+and
+.Li DWIDTH
+and the third and fourth argument to the
+.Li BBX
+.Tn BDF
+commands.
+(The first and second arguments of
+.Li BBX
+are derived from the glyph data line instead.)
+.Pp
+The glyph defaults are used in encoding every subsequent glyph for
+.Tn BDF
+and are valid until the next glyph default line, which means that
+a character block must start with one, and that sorting may need
+to duplicate or move such lines, as handled by
+.Nm
+.Fl c .
+.Pp
+Finally, let's talk about the glyph data lines.
+The standard (condensed) form looks like
+.Dl c 0020 6 00:00:00:00:00:00:00:00 space
+which are, in this order, the type of the line, the encoding of
+the glyph, the width (in bit) of the glyph (first argument of
+.Li BBX ) ,
+the glyph data (in whole bytes, uppercase nibbles, as in
+.Tn BDF ,
+but colon-separated; the number of which yields the second argument to
+.Li BBX )
+and the glyph name (which, as explained above, is optional)
+consisting of up to 14 alphanumeric characters.
+.Pp
+The editing form is a multi-line form and
+.Em must not
+be used in persistent storage, revision control or transmission.
+Its first line looks like
+.Dl e 0020 6 8 space
+which is basically the same as the standard form, except that the
+number of lines replaces the bitmap data.
+This is followed by (in this case eight) lines that comprise of
+(in this case six) occurrences of either U+3000 (to denote an unset
+pixel) or U+4DC0 (to denote a set pixel), followed by U+258C (to
+denote, as a visual help, the next character).
+The compression script also accepts a dot U+002E or a space U+0020
+as null-bit, a hash U+0023 or an asterisk U+002A as set bit, and a
+pipe sign / bar U+007C as line end marker.
+You should use the regular form if your display font has an 1:2
+ratio (e.g. 8x16, 9x18) and the alternative form if it has an 1:1
+ratio (e.g. 8x8 pixels), and switch fonts if it has a different
+ratio altogether.
+.Pp
+The trailing dot does not denote the end of file for the
+.Fl c
+operation, as it can handle concatenated files, but is used
+to have an easy way to switch between the file and glyph sections,
+since the former does not use a structured line format.
+.Sh RETURN VALUES
+The
+.Nm
+utility exits with one of the following values:
+.Pp
+.Bl -tag -width XXX -compact
+.It Li 0
+No error occurred.
+.It Li 1
+Wrong usage.
+.It Li 2
+An error during processing occurred, e.g. invalid input.
+.It Li 3
+A strict mode
+.Pq Fl d
+error occurred, e.g. invalid input.
+.It Li 4
+An error in an external program, such as
+.Xr mktemp 1 ,
+occurred.
+.El
+.Sh EXAMPLES
+The following example should be a minimal valid font demonstrating
+all features of the bdfc format:
+.Bd -literal
+=bdfc 1
+\&' $ucs\-fonts: 4x6.bdf,v 1.5 2002\-08\-26 18:05:49+01 mgk25 Rel $
+hFONT \-Misc\-Fixed\-Medium\-R\-Normal\-\-6\-60\-75\-75\-C\-40\-ISO10646\-1
+hSIZE 6 75 75
+hFONTBOUNDINGBOX 4 6 0 \-1
+pFONT_ASCENT 5
+pFONT_DESCENT 1
+pDEFAULT_CHAR 0
+C
+d 640 0 4 0 0 \-1
+e 0000 4 6 char0
+#.#.\*(Ba
+\&....\*(Ba
+#.#.\*(Ba
+\&....\*(Ba
+#.#.\*(Ba
+\&....\*(Ba
+c 0020 4 00:00:00:00:00:00 space
+c 018F 4 00:C0:60:A0:40:00
+\&.
+.Ed
+.Sh SEE ALSO
+.Xr bdftopcf 1 ,
+.Xr fstobdf 1
+.Pp
+The
+.Tn XFree86\(rg
+.Ic Bitmap Distribution Format ,
+version 2.1, specification
+.Sh AUTHORS
+.An Thorsten Glaser Aq tg@mirbsd.org
+wrote this tool because
+.Xr cvs 1
+does not scale for multi-thousand-line files,
+to have a one-line-per-glyph format that matches
+.Tn BDF .
+.Sh CAVEATS
+.Nm
+has its own ideas of how a
+.Tn BDF
+font file should look like, and if you deviate from that,
+you might get an error; although, support for more features
+can surely be added.
+.Pp
+.Dq Li ENCODING \-1
+support is missing.
+The glyph encoding is currently treated as the primary key;
+values from 0000 to FFFF inclusive are valid, the zero-padding
+is mandatory.
+.Pp
+The current practical limit on glyph width is 32.
+0-bit wide glyphs cause an error; those with height 0 are
+silently converted to an unset 1x1 bitmap.
diff --git a/mksh/bdfctool.sh b/mksh/bdfctool.sh
new file mode 100644 (file)
index 0000000..d134ef2
--- /dev/null
@@ -0,0 +1,679 @@
+#!/bin/mksh
+# $MirOS: X11/extras/bdfctool/bdfctool.sh,v 1.11 2012/09/01 19:00:01 tg Exp $
+#-
+# Copyright © 2012
+#      Thorsten Glaser <tg@mirbsd.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+set -o noglob
+
+uascii=-1
+ufast=0
+while getopts "acdeFh" ch; do
+       case $ch {
+       (a) uascii=1 ;;
+       (+a) uascii=0 ;;
+       (c|d|e) mode=$ch ;;
+       (F) ufast=1 ;;
+       (+F) ufast=0 ;;
+       (h) mode=$ch ;;
+       (*) mode= ;;
+       }
+done
+shift $((OPTIND - 1))
+(( $# )) && mode=
+
+if [[ $mode = ?(h) ]] || [[ $mode != e && $uascii != -1 ]] || \
+    [[ $mode != d && $ufast != 0 ]]; then
+       print -ru2 "Usage: ${0##*/} -c | -d [-F] | -e [-a]"
+       [[ $mode = h ]]; exit $?
+fi
+
+lno=0
+if [[ $mode = e ]]; then
+       if (( uascii == 1 )); then
+               set -A BITv -- '.' '#' '|'
+       else
+               set -A BITv -- ' ' '䷀' '▌'
+       fi
+       while IFS= read -r line; do
+               (( ++lno ))
+               if [[ $line = 'e '* ]]; then
+                       set -A f -- $line
+                       i=${f[3]}
+                       print -r -- "$line"
+                       while (( i-- )); do
+                               if IFS= read -r line; then
+                                       print -r -- "$line"
+                                       continue
+                               fi
+                               print -ru2 "E: Unexpected end of 'e' command" \
+                                   "at line $lno"
+                               exit 2
+                       done
+                       (( lno += f[3] ))
+                       continue
+               fi
+               if [[ $line != 'c '* ]]; then
+                       print -r -- "$line"
+                       continue
+               fi
+               set -A f -- $line
+               if (( (w = f[2]) > 32 || w < 1 )); then
+                       print -ru2 "E: width ${f[2]} not in 1‥32 at line $lno"
+                       exit 2
+               fi
+               if (( w <= 8 )); then
+                       adds=000000
+               elif (( w <= 16 )); then
+                       adds=0000
+               elif (( w <= 24 )); then
+                       adds=00
+               else
+                       adds=
+               fi
+               (( shiftbits = 32 - w ))
+               (( uw = 2 + w ))
+               IFS=:
+               set -A bmp -- ${f[3]}
+               IFS=$' \t\n'
+               f[0]=e
+               f[3]=${#bmp[*]}
+               print -r -- "${f[*]}"
+               chl=0
+               for ch in "${bmp[@]}"; do
+                       (( ++chl ))
+                       if [[ $ch != +([0-9A-F]) ]]; then
+                               print -ru2 "E: char '$ch' at #$chl in line $lno not hex"
+                               exit 2
+                       fi
+                       ch=$ch$adds
+                       if (( ${#ch} != 8 )); then
+                               print -ru2 "E: char '$ch' at #$chl in line $lno not valid"
+                               exit 2
+                       fi
+                       typeset -Uui2 -Z$uw bbin=16#$ch
+                       (( bbin >>= shiftbits ))
+                       b=${bbin#2#}
+                       b=${b//0/${BITv[0]}}
+                       b=${b//1/${BITv[1]}}
+                       print -r -- $b${BITv[2]}
+               done
+       done
+       exit 0
+fi
+
+Fdef=          # currently valid 'd' line
+set -A Fhead   # lines of file header, including comments intersparsed
+set -A Fprop   # lines of file properties, same
+set -A Gprop   # glyph property line (from Fdef), per glyph
+set -A Gdata   # glyph data line, per glyph
+set -A Gcomm   # glyph comments (if any) as string, per glyph
+set -A Fcomm   # lines of comments at end of file
+
+state=0
+
+function parse_bdfc_file {
+       local last
+
+       set -A last
+       while IFS= read -r line; do
+               (( ++lno ))
+               if [[ $line = C ]]; then
+                       Fprop+=("${last[@]}")
+                       state=1
+                       return
+               elif [[ $line = '=bdfc 1' ]]; then
+                       continue
+               fi
+               last+=("$line")
+               [[ $line = \' || $line = "' "* ]] && continue
+               if [[ $line = h* ]]; then
+                       Fhead+=("${last[@]}")
+               elif [[ $line = p* ]]; then
+                       Fprop+=("${last[@]}")
+               else
+                       print -ru2 "E: invalid line #$lno: '$line'"
+                       exit 2
+               fi
+               set -A last
+       done
+       Fprop+=("${last[@]}")
+       state=2
+}
+
+function parse_bdfc_edit {
+       local w shiftbits uw line r i
+
+       if (( (w = f[2]) <= 8 )); then
+               (( shiftbits = 8 - w ))
+               (( uw = 5 ))
+       elif (( w <= 16 )); then
+               (( shiftbits = 16 - w ))
+               (( uw = 7 ))
+       elif (( w <= 24 )); then
+               (( shiftbits = 24 - w ))
+               (( uw = 9 ))
+       else
+               (( shiftbits = 32 - w ))
+               (( uw = 11 ))
+       fi
+
+       if (( (i = f[3]) < 1 || i > 999 )); then
+               print -ru2 "E: nonsensical number of lines '${f[3]}' in" \
+                   "line $lno, U+${ch#16#}"
+               exit 2
+       fi
+
+       while (( i-- )); do
+               if ! IFS= read -r line; then
+                       print -ru2 "E: Unexpected end of 'e' command" \
+                           "at line $lno, U+${ch#16#}"
+                       exit 2
+               fi
+               (( ++lno ))
+               linx=${line// /.}
+               linx=${linx//䷀/#}
+               linx=${linx//▌/|}
+               linx=${linx//[ .]/0}
+               linx=${linx//[#*]/1}
+               if [[ $linx != +([01])'|' || ${#linx} != $((w + 1)) ]]; then
+                       print -ru2 "E: U+${ch#16#} (line #$lno) bitmap line" \
+                           $((f[3] - i)) "invalid: '$line'"
+                       exit 2
+               fi
+               linx=${linx%'|'}
+               typeset -Uui16 -Z$uw bhex=2#$linx
+               (( bhex <<= shiftbits ))
+               r+=${bhex#16#}:
+       done
+       f[3]=${r%:}
+       f[0]=c
+}
+
+function parse_bdfc_glyph {
+       local last
+
+       set -A last
+       while IFS= read -r line; do
+               (( ++lno ))
+               if [[ $line = . ]]; then
+                       Fcomm+=("${last[@]}")
+                       state=0
+                       return
+               fi
+               if [[ $line = \' || $line = "' "* ]]; then
+                       last+=("$line")
+                       continue
+               fi
+               set -A f -- $line
+               if [[ ${f[0]} = d ]]; then
+                       Fdef="${f[*]}"
+                       continue
+               fi
+               if [[ ${f[0]} != [ce] ]]; then
+                       print -ru2 "E: invalid line #$lno: '$line'"
+                       exit 2
+               fi
+               if [[ $Fdef != 'd '* ]]; then
+                       print -ru2 "E: char at line $lno without defaults set"
+                       exit 2
+               fi
+               if [[ ${f[1]} != [0-9A-F][0-9A-F][0-9A-F][0-9A-F] ]]; then
+                       print -ru2 "E: invalid encoding '${f[1]}' at line $lno"
+                       exit 2
+               fi
+               typeset -Uui16 -Z7 ch=16#${f[1]}
+               if (( ${#f[*]} < 4 || ${#f[*]} > 5 )); then
+                       print -ru2 "E: invalid number of fields on line $lno" \
+                           "at U+${ch#16#}: ${#f[*]}: '$line'"
+                       exit 2
+               fi
+               if (( f[2] < 1 || f[2] > 32 )); then
+                       print -ru2 "E: width ${f[2]} not in 1‥32 at line $lno"
+                       exit 2
+               fi
+               [[ ${f[4]} = "uni${ch#16#}" ]] && unset f[4]
+               if [[ ${f[0]} = e ]]; then
+                       parse_bdfc_edit
+               else
+                       if (( f[2] <= 8 )); then
+                               x='+([0-9A-F][0-9A-F]:)'
+                       elif (( f[2] <= 16 )); then
+                               x='+([0-9A-F][0-9A-F][0-9A-F][0-9A-F]:)'
+                       elif (( f[2] <= 24 )); then
+                               x='+([0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]:)'
+                       else
+                               x='+([0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]:)'
+                       fi
+                       if eval [[ '${f[3]}:' != "$x" ]]; then
+                               print -ru2 "E: invalid hex encoding for" \
+                                   "U+${ch#16#}, line $lno: '${f[3]}'"
+                               exit 2
+                       fi
+               fi
+               Gdata[ch]="${f[*]}"
+               for line in "${last[@]}"; do
+                       Gcomm[ch]+=$line$'\n'
+               done
+               set -A last
+               Gprop[ch]=$Fdef
+       done
+       Fcomm+=("${last[@]}")
+       state=2
+}
+
+function parse_bdfc {
+       while :; do
+               case $state {
+               (0) parse_bdfc_file ;;
+               (1) parse_bdfc_glyph ;;
+               (2) return 0 ;;
+               }
+       done
+       print -ru2 "E: internal error (at line $lno), shouldn't happen"
+       exit 255
+}
+
+function parse_bdf {
+       while IFS= read -r line; do
+               (( ++lno ))
+               case $line {
+               (COMMENT)
+                       Fhead+=("'")
+                       ;;
+               (COMMENT@([      ])*)
+                       Fhead+=("' ${line#COMMENT[       ]}")
+                       ;;
+               (STARTPROPERTIES\ +([0-9]))
+                       break
+                       ;;
+               (*)
+                       Fhead+=("h$line")
+                       ;;
+               }
+       done
+       set -A f -- $line
+       numprop=${f[1]}
+       while IFS= read -r line; do
+               (( ++lno ))
+               case $line {
+               (COMMENT)
+                       Fprop+=("'")
+                       ;;
+               (COMMENT@([      ])*)
+                       Fprop+=("' ${line#COMMENT[       ]}")
+                       ;;
+               (ENDPROPERTIES)
+                       break
+                       ;;
+               (*)
+                       Fprop+=("p$line")
+                       let --numprop
+                       ;;
+               }
+       done
+       if (( numprop )); then
+               print -ru2 "E: expected ${f[1]} properties, got" \
+                   "$((f[1] - numprop)) in line $lno"
+               exit 2
+       fi
+       while IFS= read -r line; do
+               (( ++lno ))
+               case $line {
+               (COMMENT)
+                       Fprop+=("'")
+                       ;;
+               (COMMENT@([      ])*)
+                       Fprop+=("' ${line#COMMENT[       ]}")
+                       ;;
+               (CHARS\ +([0-9]))
+                       break
+                       ;;
+               (*)
+                       print -ru2 "E: expected CHARS not '$line' in line $lno"
+                       exit 2
+                       ;;
+               }
+       done
+       set -A f -- $line
+       numchar=${f[1]}
+       set -A cc
+       set -A cn
+       set -A ce
+       set -A cs
+       set -A cd
+       set -A cb
+       while IFS= read -r line; do
+               (( ++lno ))
+               case $line {
+               (COMMENT)
+                       cc+=("'")
+                       ;;
+               (COMMENT@([      ])*)
+                       cc+=("' ${line#COMMENT[  ]}")
+                       ;;
+               (STARTCHAR\ *)
+                       set -A cn -- $line
+                       ;;
+               (ENCODING\ +([0-9]))
+                       set -A ce -- $line
+                       ;;
+               (SWIDTH\ +([0-9-])\ +([0-9-]))
+                       set -A cs -- $line
+                       ;;
+               (DWIDTH\ +([0-9-])\ +([0-9-]))
+                       set -A cd -- $line
+                       ;;
+               (BBX\ +([0-9])\ +([0-9])\ +([0-9-])\ +([0-9-]))
+                       set -A cb -- $line
+                       ;;
+               (BITMAP)
+                       if [[ -z $cn ]]; then
+                               print -ru2 "E: missing STARTCHAR in line $lno"
+                               exit 2
+                       fi
+                       if [[ -z $ce ]]; then
+                               print -ru2 "E: missing ENCODING in line $lno"
+                               exit 2
+                       fi
+                       if [[ -z $cs ]]; then
+                               print -ru2 "E: missing SWIDTH in line $lno"
+                               exit 2
+                       fi
+                       if [[ -z $cd ]]; then
+                               print -ru2 "E: missing DWIDTH in line $lno"
+                               exit 2
+                       fi
+                       if [[ -z $cb ]]; then
+                               print -ru2 "E: missing BBX in line $lno"
+                               exit 2
+                       fi
+                       typeset -Uui16 -Z7 ch=10#${ce[1]}
+                       if (( ch < 0 || ch > 0xFFFF )); then
+                               print -ru2 "E: encoding ${ce[1]} out of" \
+                                   "bounds in line $lno"
+                               exit 2
+                       fi
+                       Gprop[ch]="d ${cs[1]} ${cs[2]} ${cd[1]} ${cd[2]} ${cb[3]} ${cb[4]}"
+                       set -A f c ${ch#16#} ${cb[1]} - ${cn[1]}
+                       [[ ${f[4]} = "uni${ch#16#}" ]] && unset f[4]
+                       if (( f[2] <= 8 )); then
+                               ck='[0-9A-F][0-9A-F]'
+                       elif (( f[2] <= 16 )); then
+                               ck='[0-9A-F][0-9A-F][0-9A-F][0-9A-F]'
+                       elif (( f[2] <= 24 )); then
+                               ck='[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]'
+                       else
+                               ck='[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]'
+                       fi
+                       if (( (numlines = cb[2]) )); then
+                               bmps=
+                               typeset -u linu
+                               while IFS= read -r linu; do
+                                       (( ++lno ))
+                                       if eval [[ '$linu' != "$ck" ]]; then
+                                               print -ru2 "E: invalid hex encoding" \
+                                                   "for U+${ch#16#} (dec. $((ch)))" \
+                                                   "on line $lno: '$linu'"
+                                               exit 2
+                                       fi
+                                       bmps+=$linu:
+                                       (( --numlines )) || break
+                               done
+                               f[3]=${bmps%:}
+                       else
+                               f[2]=1
+                               f[3]=00
+                       fi
+                       if ! IFS= read -r line || [[ $line != ENDCHAR ]]; then
+                               print -ru2 "E: expected ENDCHAR after line $lno"
+                               exit 2
+                       fi
+                       (( ++lno ))
+                       Gdata[ch]="${f[*]}"
+                       [[ -n $cc ]] && for line in "${cc[@]}"; do
+                               Gcomm[ch]+=$line$'\n'
+                       done
+                       set -A cc
+                       set -A cn
+                       set -A ce
+                       set -A cs
+                       set -A cd
+                       set -A cb
+                       ;;
+               (ENDFONT)
+                       break
+                       ;;
+               (*)
+                       print -ru2 "E: unexpected '$line' in line $lno"
+                       exit 2
+                       ;;
+               }
+       done
+       Fcomm+=("${cc[@]}")
+       for line in "${cn[*]}" "${ce[*]}" "${cs[*]}" "${cd[*]}" "${cb[*]}"; do
+               [[ -n $line ]] || continue
+               print -ru2 "E: unexpected '$line' between last char and ENDFONT"
+               exit 2
+       done
+       if (( numchar != ${#Gdata[*]} )); then
+               print -ru2 "E: expected $numchar glyphs, got ${#Gdata[*]}"
+               exit 2
+       fi
+       while IFS= read -r line; do
+               (( ++lno ))
+               case $line {
+               (COMMENT)
+                       Fcomm+=("'")
+                       ;;
+               (COMMENT@([      ])*)
+                       Fcomm+=("' ${line#COMMENT[       ]}")
+                       ;;
+               (*)
+                       print -ru2 "E: unexpected '$line' past ENDFONT" \
+                           "in line $lno"
+                       exit 2
+                       ;;
+               }
+       done
+}
+
+if [[ $mode = c ]]; then
+       if ! IFS= read -r line; then
+               print -ru2 "E: read error at BOF"
+               exit 2
+       fi
+       lno=1
+       if [[ $line = 'STARTFONT 2.1' ]]; then
+               parse_bdf
+       elif [[ $line = '=bdfc 1' ]]; then
+               parse_bdfc
+       else
+               print -ru2 "E: not BDF or bdfc at BOF: '$line'"
+               exit 2
+       fi
+
+       # write .bdfc stream
+
+       for line in '=bdfc 1' "${Fhead[@]}" "${Fprop[@]}"; do
+               print -r -- "$line"
+       done
+       print C
+       Fdef=
+       for x in ${!Gdata[*]}; do
+               if [[ ${Gprop[x]} != "$Fdef" ]]; then
+                       Fdef=${Gprop[x]}
+                       print -r -- $Fdef
+               fi
+               print -r -- "${Gcomm[x]}${Gdata[x]}"
+       done
+       for line in "${Fcomm[@]}"; do
+               print -r -- "$line"
+       done
+       print .
+       exit 0
+fi
+
+if [[ $mode != d ]]; then
+       print -ru2 "E: cannot happen (control flow issue in ${0##*/}:$LINENO)"
+       exit 255
+fi
+
+if ! IFS= read -r line; then
+       print -ru2 "E: read error at BOF"
+       exit 2
+fi
+lno=1
+if [[ $line != '=bdfc 1' ]]; then
+       print -ru2 "E: not bdfc at BOF: '$line'"
+       exit 2
+fi
+
+if (( ufast )); then
+       if ! T=$(mktemp /tmp/bdfctool.XXXXXXXXXX); then
+               print -u2 E: cannot make temporary file
+               exit 4
+       fi
+       # quickly parse bdfc header
+       set -A last
+       while IFS= read -r line; do
+               [[ $line = C ]] && break
+               last+=("$line")
+               [[ $line = \' || $line = "' "* ]] && continue
+               if [[ $line = h* ]]; then
+                       Fhead+=("${last[@]}")
+               else
+                       Fprop+=("${last[@]}")
+               fi
+               set -A last
+       done
+       Fprop+=("${last[@]}")
+else
+       # parse entire bdfc file into memory
+       parse_bdfc
+fi
+
+# analyse data for BDF
+numprop=0
+for line in "${Fprop[@]}"; do
+       [[ $line = p* ]] && let ++numprop
+done
+(( ufast )) || numchar=${#Gdata[*]}
+
+# write BDF stream
+print 'STARTFONT 2.1'
+for line in "${Fhead[@]}"; do
+       if [[ $line = h* ]]; then
+               print -r -- "${line#h}"
+       else
+               print -r -- "COMMENT${line#\'}"
+       fi
+done
+set -A last
+print STARTPROPERTIES $((numprop))
+for line in "${Fprop[@]}"; do
+       if [[ $line = p* ]]; then
+               last+=("${line#p}")
+       else
+               last+=("COMMENT${line#\'}")
+               continue
+       fi
+       for line in "${last[@]}"; do
+               print -r -- "$line"
+       done
+       set -A last
+done
+print ENDPROPERTIES
+for line in "${last[@]}"; do
+       print -r -- "$line"
+done
+if (( ufast )); then
+       numchar=0
+       # directly transform font data
+       set -A last
+       while IFS= read -r line; do
+               [[ $line = . ]] && break
+               if [[ $line = \' || $line = "' "* ]]; then
+                       last+=("$line")
+                       continue
+               fi
+               set -A f -- $line
+               if [[ ${f[0]} = d ]]; then
+                       set -A xprop -- $line
+                       continue
+               fi
+               typeset -Uui16 -Z7 ch=16#${f[1]}
+               for line in "${last[@]}"; do
+                       print -r -- "COMMENT${line#\'}"
+               done
+               set -A last
+               IFS=:
+               set -A bmp -- ${f[3]}
+               IFS=$' \t\n'
+               cat <<-EOF
+                       STARTCHAR ${f[4]:-uni${ch#16#}}
+                       ENCODING $((ch))
+                       SWIDTH ${xprop[1]} ${xprop[2]}
+                       DWIDTH ${xprop[3]} ${xprop[4]}
+                       BBX ${f[2]} ${#bmp[*]} ${xprop[5]} ${xprop[6]}
+                       BITMAP
+               EOF
+               for line in "${bmp[@]}"; do
+                       print $line
+               done
+               print ENDCHAR
+               let ++numchar
+       done >"$T"
+       Fcomm+=("${last[@]}")
+       print CHARS $((numchar))
+       cat "$T"
+       rm -f "$T"
+else
+       print CHARS $((numchar))
+       for x in ${!Gdata[*]}; do
+               IFS=$'\n'
+               set -A xcomm -- ${Gcomm[x]}
+               IFS=$' \t\n'
+               for line in "${xcomm[@]}"; do
+                       print -r -- "COMMENT${line#\'}"
+               done
+               set -A xprop -- ${Gprop[x]}
+               set -A f -- ${Gdata[x]}
+               IFS=:
+               set -A bmp -- ${f[3]}
+               IFS=$' \t\n'
+               typeset -Uui16 -Z7 ch=16#${f[1]}
+               cat <<-EOF
+                       STARTCHAR ${f[4]:-uni${ch#16#}}
+                       ENCODING $((ch))
+                       SWIDTH ${xprop[1]} ${xprop[2]}
+                       DWIDTH ${xprop[3]} ${xprop[4]}
+                       BBX ${f[2]} ${#bmp[*]} ${xprop[5]} ${xprop[6]}
+                       BITMAP
+               EOF
+               for line in "${bmp[@]}"; do
+                       print $line
+               done
+               print ENDCHAR
+       done
+fi
+for line in "${Fcomm[@]}"; do
+       print -r -- "COMMENT${line#\'}"
+done
+print ENDFONT
+exit 0