Re: groff performance and a critique of mandoc(1)'s PDF generation (was: Git snapshot tarballs, multiple-pass formatting, and pdfmom)

John Gardner <[email protected]> Sat, 1 Aug 2026 15:05:41 +1000
Newsgroups gmane.comp.printing.groff.general
Message-ID <CAGcdajeSrVmDW9pHdt83aTu+aGdooxXUe8HrWh62sjU=KWKcCg@mail.gmail.com>
At 2026-07-26T16:40:39-0500, G. Branden Robinson wrote:

> mandoc(1), unsurprisingly, doesn't typeset pic(1) diagrams (p. 117).
> Unfortunately because it also doesn't set a man(7) `EX`/`EE` display in a
> monospaced typeface, the fallback ASCII art is mostly unintelligible.


I'm surprised mandoc(1) hasn't adopted Pikchr
<https://pikchr.org/home/doc/trunk/doc/differences.md> as a
\[open-shock-quote]=E2=9C=8C=EF=B8=8F*mOdERn*=E2=9C=8C=EF=B8=8F\[closed-sho=
ck-quote] alternative to pic(1).
They're even more security- and performance-conscious than mandoc(1)'s devs
are (because it does, after all, have to run client-side in browsers to
generate some potentially very complex diagrams). Pikchr is designed to be
embedded in Markdown code blocks, like this:[1]

``` pikchr
arrow right 200% "Markdown" "Source"
box rad 10px "Markdown" "Formatter" "(markdown.c)" fit
arrow right 200% "HTML+SVG" "Output"
arrow <-> down 70% from last box.s
box same "Pikchr" "Formatter" "(pikchr.c)" fit
```

which from a preprocessing perspective, isn't much different than a
code-block delimited by .PS and .PE macros.

Pikchr is a project closely connected to SQLite, and it's used to render
railroad diagrams of SQL clause syntax. It's quite efficient, though their
disparaging remarks about Troff being a historically significant but now
obsolete markup language low-key pisses me off, especially knowing they
implicitly mean Markdown is Troff's "modern" replacement.

But hey, a tone of similarly-undeserved arrogance can be felt reading
mandoc(1)'s description of the Troff typesetting system or the Roff
language in general (it's more subdued, but you damn well know they wish
request-free mdoc(7) was the only macro package the world uses and cares
about).

Anyway, Mandoc isn't a typesetting system, and it makes no allusions to
being one. I'm not even sure why they bothered with PDF output (because it
really *is* shit), and even its HTML output could do with some sanding, so
to speak. It's great at what it does: render manual pages for terminal
display quickly and competently, where there isn't much wiggle-room for the
type of typography bikeshedding that leads to people arguing about the use
of fixed-pitch fonts for certain (inline) manual page elements (which
happens to be exactly what led to the creation of mono.tmac
<https://github.com/Alhadis/Mono/blob/80e8d1df113c6718406ef8a137827301ed7e1=
cb2/ono.tmac#L49-L79>'s
original macros for controlling how constant-width gets rendered in
terminal, if it does at all).

Sorry for yet another long-winded post,
=E2=80=94 Alhadis

*Footnotes:*

   1. PIC[2] source taken from Pikchr's homepage
   <https://pikchr.org/home/doc/trunk/homepage.md>.
   2. I adamantly refuse to call this the "Pikchr" language when it's
   really just a subset of the pic(1) language with extra features thrown i=
n.
   Not unlike how Markdown is just HTML with a niceties used wherever contr=
ol
   over the layout of the output or better styling control isn't necessary =
or
   desired, at which point I just write in the subset of HTML markup permit=
ted
   in Markdown comments.
   3. There is no third footnote, I just ramble too much shit.


On Mon, 27 Jul 2026 at 07:42, G. Branden Robinson <
[email protected]> wrote:

> Hi Larry,
>
> At 2026-07-26T00:50:49-0400, Larry Kollar wrote:
> > > G. Branden Robinson <[email protected]> wrote:
> > > I'd like to avoid doing what TeX does (or what its usual front-end
> > > does), and do what appears to be multi-pass formatting even when
> > > it's not necessary.  Or maybe it _doesn't_ apply multiple passes
> > > unnecessarily, but its ordinary operation is so garrulous that I
> > > struggle to tell the difference.
> >
> > In the years around 2004-2010, I maintained a pretty large[1] firmware
> > guide in Groff. Standard procedure was a two-pass format: build the
> > table of contents during the first pass, along with all forward
> > references, then the second pass resolved all the page numbers.
> > Rarely, maybe once or twice a year, I needed a third pass when
> > pagination interfered with cross-references. But even on a blue Mac G3
> > (1999 model), typing =E2=80=9Cmake=E2=80=9D and walking to the bathroom=
 gave me a
> > clean PDF before I returned.
> >
> > I=E2=80=99m not saying there are no use cases where 2026-vintage hardwa=
re
> > needs help building groff, but I believe it=E2=80=99s true that (beside=
s
> > Deri=E2=80=99s eye-popping use case) the vast majority of groff users a=
ren=E2=80=99t
> > going to notice an extra pass when it comes to processor usage.
>
> Deri's pretty good at finding mad-scientist applications for groff.
> It's great!  :D
>
> [...]
> > [1] =E2=80=9CPretty large=E2=80=9D =3D around 1000 pages across four do=
cs. Later on, I
> > consolidated it into a single manual that ran 700-900 pages depending
> > on build-time options.
>
> On my system using my working copy, which is about 28 commits ahead of
> our master branch in Savannah, groff produces our 408-page collected man
> pages document in about 7 seconds.
>
> $ rm ./build/doc/groff-man-pages.pdf
> $ time make -C build
> make: Entering directory '/home/branden/src/GIT/groff/build'
> make  all-recursive
> make[1]: Entering directory '/home/branden/src/GIT/groff/build'
> make[2]: Entering directory '/home/branden/src/GIT/groff/build'
>   GROFF    doc/groff-man-pages.pdf
> troff: backtrace: file '<standard input>':1075
> troff:<standard input>:1075: warning: special character '.j' not defined
> [-w char]
> troff: backtrace: file '<standard input>':1481
> troff:<standard input>:1481: warning: special character 'vA' not defined
> [-w char]
> troff: backtrace: file '<standard input>':1611
> troff:<standard input>:1611: warning: special character 'bs' not defined
> [-w char]
> troff: backtrace: file '<standard input>':1784
> troff:<standard input>:1784: warning: special character '-+' not defined
> [-w char]
> troff: backtrace: file '<standard input>':1833
> troff:<standard input>:1833: warning: special character 'coproduct' not
> defined [-w char]
> troff: backtrace: file '<standard input>':1933
> troff:<standard input>:1933: warning: special character '+e' not defined
> [-w char]
> make[2]: Leaving directory '/home/branden/src/GIT/groff/build'
> make[1]: Leaving directory '/home/branden/src/GIT/groff/build'
> make: Leaving directory '/home/branden/src/GIT/groff/build'
>
> real    0m7.148s
> user    0m9.101s
> sys     0m0.140s
>
> This was likely with hot memory and disk buffer caches, though, since
> I'd just done a build for another purpose.
>
> mandoc(1) advocates frequently boast of its speed relative to groff, and
> they have a point.  I can rig up a rough apples-to-apples comparison by
> having that tool produce a PDF of all of groff's man pages.
>
> $ cat ATTIC/MANS_BUILT
> ./build/contrib/chem/chem.1
> ./build/contrib/eqn2graph/eqn2graph.1
> ./build/contrib/gdiffmk/gdiffmk.1
> ./build/contrib/glilypond/glilypond.1
> ./build/contrib/gperl/gperl.1
> ./build/contrib/gpinyin/gpinyin.1
> ./build/contrib/grap2graph/grap2graph.1
> ./build/contrib/hdtbl/groff_hdtbl.7
> ./build/contrib/mm/groff_mm.7
> ./build/contrib/mm/groff_mmse.7
> ./build/contrib/mm/mmroff.1
> ./build/contrib/mom/groff_mom.7
> ./build/contrib/pic2graph/pic2graph.1
> ./build/contrib/rfc1345/groff_rfc1345.7
> ./build/man/groff.7
> ./build/man/groff_char.7
> ./build/man/groff_diff.7
> ./build/man/groff_font.5
> ./build/man/groff_out.5
> ./build/man/groff_tmac.5
> ./build/man/roff.7
> ./build/src/devices/grodvi/grodvi.1
> ./build/src/devices/grohtml/grohtml.1
> ./build/src/devices/grolbp/grolbp.1
> ./build/src/devices/grolj4/grolj4.1
> ./build/src/devices/gropdf/gropdf.1
> ./build/src/devices/grops/grops.1
> ./build/src/devices/grotty/grotty.1
> ./build/src/devices/xditview/gxditview.1
> ./build/src/preproc/eqn/eqn.1
> ./build/src/preproc/eqn/neqn.1
> ./build/src/preproc/grn/grn.1
> ./build/src/preproc/pic/pic.1
> ./build/src/preproc/preconv/preconv.1
> ./build/src/preproc/refer/refer.1
> ./build/src/preproc/soelim/soelim.1
> ./build/src/preproc/tbl/tbl.1
> ./build/src/roff/groff/groff.1
> ./build/src/roff/nroff/nroff.1
> ./build/src/roff/troff/troff.1
> ./build/src/utils/addftinfo/addftinfo.1
> ./build/src/utils/afmtodit/afmtodit.1
> ./build/src/utils/grog/grog.1
> ./build/src/utils/hpftodit/hpftodit.1
> ./build/src/utils/indxbib/indxbib.1
> ./build/src/utils/lkbib/lkbib.1
> ./build/src/utils/lookbib/lookbib.1
> ./build/src/utils/pdfmom/pdfmom.1
> ./build/src/utils/pfbtops/pfbtops.1
> ./build/src/utils/tfmtodit/tfmtodit.1
> ./build/src/utils/xtotroff/xtotroff.1
> ./build/tmac/groff_man.7
> ./build/tmac/groff_man_style.7
> ./build/tmac/groff_mdoc.7
> ./build/tmac/groff_me.7
> ./build/tmac/groff_ms.7
> ./build/tmac/groff_trace.7
> ./build/tmac/groff_www.7
> $ MANS=3D$(< ATTIC/MANS_BUILT)
> $ time mandoc -Tpdf $MANS > groff-man-pages-generated-by-mandoc.pdf
>
> real    0m0.334s
> user    0m0.316s
> sys     0m0.019s
>
> Pretty big difference.
>
> 'Course, there are important caveats.  Browsing through the output, I
> have some observations.
>
> * mandoc(1)'s output is not hyperlinked at all, neither internally nor
>   to external URLs.
>
> * Because there are no internal hyperlinks, mandoc(1) didn't need to
>   perform multiple passes.  That virtually guarantees a win on elapsed
>   time to generate the document.
>
> * There's no navigation pane.  Wanna navigate the document?  Key in a
>   page number and play Battleship to find the material you seek, or work
>   that scroll bar, plebe.
>
> * In my opinion, mandoc(1)'s output is esthetically inferior, with the
>   possible exception of the base type size being larger and thus
>   arguably gentler on the reader.
>
> * On the other hand, that decision swells the document to 675 pages.
>
> * We can _easily_ increase the base type size from 10p to 11p or 12p if
>   we decide that's a good thing to do.  I predict the impact on
>   production time to be negligible.  (Worth testing, though!)
>
> * mandoc(1)'s bullet glyphs are ugly--they're an 'o' overstruck with a
>   '+', to me, the glyphs seem to lack a precisely identical center.
>
> * Similar ugliness afflicts groff_mmse(7) (p. 78).  Notice how =C3=B6 is
>   rendered.
>
> * Neutral apostrophes ' render incorrectly as =E2=80=99 and copy-and-past=
e
>   that way, too.
>
> * A hyphen-minus - _renders_ as a hyphen =E2=80=90, but copies and pastes
>   correctly.
>
> * =C2=B1 renders unhelpfully as "+-" (p. 144).
>
> * Between bad table layout (see below), refusal to format non-Basic
>   Latin glyphs, and 1970s line-printer-style overstriking, the entire
>   groff_char(7) document (pp. 187-222) is an utter disaster.
>
> * I see errors (missing space between words) in synopsis formatting;
>   mandoc(1)'s support for the groff man `SY` and `YS` extensions may be
>   lacking--or, since mandoc(1) hasn't had a release in a few years,
>   those flaws might be fixed in its "HEAD".[A]
>
> * To my surprise, example displays are not set in a monospaced font.
>   People have ridden our rears about failing to do that when generating
>   HTML,[B] and traditionally mdoc(7) mavens are proud to a fault of
>   spattering their documents with Courier like cream pie filling at the
>   end of a Keystone Kops short feature.
>
> * Table layout is poor.  See, e.g., page 46.
>
>   + There's no separation between the columns.
>
>   + The existence of the `BI` (bold-italic) font is ignored.
>
>   + The width of text blocks is computed strongly suboptimally.  (The
>     "x" column modifier appears to be completely ignored.)
>
>   + Even worse, line wrapping is permitted before the period at the end
>     of a sentence.  That's _really_ bad, QA-wise.  mandoc(1) doesn't
>     seem to mess this up when formatting regular text; why would it
>     here?  Ahh, it may be a parser issue.  The line that gets
>     incorrectly broken is a macro call inside a text block.
>         I@T{
>         Indent text by
>         .BR \[rs]n[Si] .
>         T}
>     I'll bet that a macro argument boundary is being misinterpreted as
>     a break opportunity.  This might be due to misinterpretation or
>     misimplementation of the `\c` escape sequence.  Or not, since
>     mandoc(1) is not a *roff, and the "macros" it recognizes are not
>     implemented atop a substrate of formatter requests and escape
>     sequences.
>
>   + In fact, macro calls within tbl(1) text blocks seem to be largely
>     ignored.  Changes of typeface that should occur, don't.  See, e.g.,
>     the table on page 47.
>
>   + Table columns are misaligned when the headings are in bold but the
>     rest of the table is not.   See, e.g., the table on page 49.
>
>   + The table spanning pp. 128-129 is so badly damaged that it's nearly
>     incomprehensible.
>
>   + mandoc(1) oversets not just the line but the page when employing its
>     "semantic" glyph fallbacks.  See p. 220.
>
> * Some of the widow/orphan management is cringe, but ours often can be
>   too--this is one reason people want Knuth-Plass.  On the other hand,
>   groff's man pages (and other documents) often mitigate this with
>   careful use of `ne` requests and I think mandoc(1) might be ignoring
>   these.  So mandoc(1) makes our documents look worse than they should.
>   (Update: Maybe not.  I checked a few cases; I might be seeing more
>   widows and orphans simply because the type size mandoc(1) uses is
>   larger, creating more _opportunities_ for widows and orphans.)
>
> * mandoc(1), unsurprisingly, doesn't typeset pic(1) diagrams (p. 117).
>   Unfortunately because it also doesn't set a man(7) `EX`/`EE` display
>   in a monospaced typeface, the fallback ASCII art is mostly
>   unintelligible.
>
> * mandoc(1) also doesn't typeset eqn(1) equations (pp. 408-409).  I
>   cannot tell if eqn production is _supposed_ to be supported or not;
>   the man page disclaims eqn(1) support for its "-T man" and "-T
>   markdown" output forms, while claiming it for "-T html" but is silent
>   on the matter for "-T pdf".
>
> * Separation of man(7) paragraph tags from the paragraph body is too
>   narrow; see, e.g., the bottom of p. 144.  To be fair, groff man(7)
>   used to have this problem, too.[C]
>
> * I observe that the mandoc(1)-generated PDF is 11 MB; ours is 1.6 MB.
>   Bad old bloated GNU stuff once again, eh?[D]
>
> I terminated my survey (except to peek ahead at eqn(1)) at page 223 of
> 675.  I think I've seen enough to produce an informed opinion.
>
> I've attached the respective PDF documents.  The different _ordering_ of
> the man page documents within is not mandoc(1)'s fault but merely an
> artifact of my laziness in employing a shell variable as shown above.
>
> I don't expect any of these observations to impact BSD/mdoc/mandoc(1)
> partisans' assessment of its superiority to groff in any way.  It:
>
> * goes fast;
> * isn't copylefted;
> * isn't in C++.
>
> That, it seems, is all they require of it.
>
> Whether mandoc(1) faithfully manifests the principle of the Unix
> philosophy holding that a tool should "do one thing and do it well", I
> leave open to consideration.
>
> Regards,
> Branden
>
> [A] https://cvsweb.bsd.lv/mandoc/?sortby=3Ddate#dirlist
> [B] Good news, though!  https://savannah.gnu.org/bugs/?68309
> [C]
> https://cgit.git.savannah.gnu.org/cgit/groff.git/tree/NEWS?h=3D1.24.1#n54=
7
> [D]
> https://www.undeadly.org/cgi?action=3Darticle&sid=3D20100604082319&mode=
=3Dexpanded
>