diff mupdf-source/scripts/mutool_draw.py @ 3:2c135c81b16c

MERGE: upstream PyMuPDF 1.26.4 with MuPDF 1.26.7
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:44:09 +0200
parents b50eed0cc0ef
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/scripts/mutool_draw.py	Mon Sep 15 11:44:09 2025 +0200
@@ -0,0 +1,1260 @@
+import getopt
+import os
+import re
+import sys
+import time
+
+if os.environ.get('MUPDF_PYTHON') in ('swig', None):
+    # PYTHONPATH should have been set up to point to a build/shared-*/
+    # directory containing mupdf.so generated by scripts/mupdfwrap.py and SWIG.
+    import mupdf
+elif os.environ.get('MUPDF_PYTHON') == 'cppyy':
+    sys.path.insert(0, os.path.abspath(f'{__file__}/../../platform/python'))
+    import mupdf_cppyy
+    del sys.path[0]
+    mupdf = mupdf_cppyy.cppyy.gbl.mupdf
+else:
+    raise Exception(f'Unrecognised $MUPDF_PYTHON: {os.environ.get("MUPDF_PYTHON")}')
+
+# Force stderr to be line-buffered - i.e. python will flush to the underlying
+# stderr stream every newline. This ensures that our output interleaves with
+# the output of mupdf C code, making it easier to compare our output with that
+# of mutool.
+#
+sys.stderr = os.fdopen( os.dup( sys.stderr.fileno()), 'w', 1)
+
+
+OUT_NONE    = 0
+OUT_PNG     = 1
+OUT_PNM     = 2
+OUT_PGM     = 3
+OUT_PPM     = 4
+OUT_PAM     = 5
+OUT_PBM     = 6
+OUT_PKM     = 7
+OUT_PWG     = 8
+OUT_PCL     = 9
+OUT_PS      = 10
+OUT_PSD     = 11
+OUT_TEXT    = 12
+OUT_HTML    = 13
+OUT_XHTML   = 14
+OUT_STEXT   = 15
+OUT_PCLM    = 16
+OUT_TRACE   = 17
+OUT_BBOX    = 18
+OUT_SVG     = 19
+OUT_XMLTEXT = 20
+
+CS_INVALID      = 0
+CS_UNSET        = 1
+CS_MONO         = 2
+CS_GRAY         = 3
+CS_GRAY_ALPHA   = 4
+CS_RGB          = 5
+CS_RGB_ALPHA    = 6
+CS_CMYK         = 7
+CS_CMYK_ALPHA   = 8
+CS_ICC          = 9
+
+CS_INVALID      = 0
+CS_UNSET        = 1
+CS_MONO         = 2
+CS_GRAY         = 3
+CS_GRAY_ALPHA   = 4
+CS_RGB          = 5
+CS_RGB_ALPHA    = 6
+CS_CMYK         = 7
+CS_CMYK_ALPHA   = 8
+CS_ICC          = 9
+
+SPOTS_NONE          = 0
+SPOTS_OVERPRINT_SIM = 1
+SPOTS_FULL          = 2
+
+
+class suffix_t:
+    def __init__( self, suffix, format_, spots):
+        self.suffix = suffix
+        self.format = format_
+        self.spots = spots
+
+suffix_table = [
+        suffix_t( ".png", OUT_PNG, 0 ),
+        suffix_t( ".pgm", OUT_PGM, 0 ),
+        suffix_t( ".ppm", OUT_PPM, 0 ),
+        suffix_t( ".pnm", OUT_PNM, 0 ),
+        suffix_t( ".pam", OUT_PAM, 0 ),
+        suffix_t( ".pbm", OUT_PBM, 0 ),
+        suffix_t( ".pkm", OUT_PKM, 0 ),
+        suffix_t( ".svg", OUT_SVG, 0 ),
+        suffix_t( ".pwg", OUT_PWG, 0 ),
+        suffix_t( ".pclm", OUT_PCLM, 0 ),
+        suffix_t( ".pcl", OUT_PCL, 0 ),
+        suffix_t( ".psd", OUT_PSD, 1 ),
+        suffix_t( ".ps", OUT_PS, 0 ),
+
+        suffix_t( ".txt", OUT_TEXT, 0 ),
+        suffix_t( ".text", OUT_TEXT, 0 ),
+        suffix_t( ".html", OUT_HTML, 0 ),
+        suffix_t( ".xhtml", OUT_XHTML, 0 ),
+        suffix_t( ".stext", OUT_STEXT, 0 ),
+
+        suffix_t( ".trace", OUT_TRACE, 0 ),
+        suffix_t( ".raw", OUT_XMLTEXT, 0 ),
+        suffix_t( ".bbox", OUT_BBOX, 0 ),
+        ]
+
+class cs_name_t:
+    def __init__( self, name, colorspace):
+        self.name = name
+        self.colorspace = colorspace
+
+cs_name_table = dict(
+        m           = CS_MONO,
+        mono        = CS_MONO,
+        g           = CS_GRAY,
+        gray        = CS_GRAY,
+        grey        = CS_GRAY,
+        ga          = CS_GRAY_ALPHA,
+        grayalpha   = CS_GRAY_ALPHA,
+        greyalpha   = CS_GRAY_ALPHA,
+        rgb         = CS_RGB,
+        rgba        = CS_RGB_ALPHA,
+        rgbalpha    = CS_RGB_ALPHA,
+        cmyk        = CS_CMYK,
+        cmyka       = CS_CMYK_ALPHA,
+        cmykalpha   = CS_CMYK_ALPHA,
+        )
+
+
+class format_cs_table_t:
+    def __init__( self, format_, default_cs, permitted_cs):
+        self.format = format_
+        self.default_cs = default_cs
+        self.permitted_cs = permitted_cs
+
+format_cs_table = [
+        format_cs_table_t( OUT_PNG, CS_RGB, [ CS_GRAY, CS_GRAY_ALPHA, CS_RGB, CS_RGB_ALPHA, CS_ICC ] ),
+        format_cs_table_t( OUT_PPM, CS_RGB, [ CS_GRAY, CS_RGB ] ),
+        format_cs_table_t( OUT_PNM, CS_GRAY, [ CS_GRAY, CS_RGB ] ),
+        format_cs_table_t( OUT_PAM, CS_RGB_ALPHA, [ CS_GRAY, CS_GRAY_ALPHA, CS_RGB, CS_RGB_ALPHA, CS_CMYK, CS_CMYK_ALPHA ] ),
+        format_cs_table_t( OUT_PGM, CS_GRAY, [ CS_GRAY, CS_RGB ] ),
+        format_cs_table_t( OUT_PBM, CS_MONO, [ CS_MONO ] ),
+        format_cs_table_t( OUT_PKM, CS_CMYK, [ CS_CMYK ] ),
+        format_cs_table_t( OUT_PWG, CS_RGB, [ CS_MONO, CS_GRAY, CS_RGB, CS_CMYK ] ),
+        format_cs_table_t( OUT_PCL, CS_MONO, [ CS_MONO, CS_RGB ] ),
+        format_cs_table_t( OUT_PCLM, CS_RGB, [ CS_RGB, CS_GRAY ] ),
+        format_cs_table_t( OUT_PS, CS_RGB, [ CS_GRAY, CS_RGB, CS_CMYK ] ),
+        format_cs_table_t( OUT_PSD, CS_CMYK, [ CS_GRAY, CS_GRAY_ALPHA, CS_RGB, CS_RGB_ALPHA, CS_CMYK, CS_CMYK_ALPHA, CS_ICC ] ),
+
+        format_cs_table_t( OUT_TRACE, CS_RGB, [ CS_RGB ] ),
+        format_cs_table_t( OUT_XMLTEXT, CS_RGB, [ CS_RGB ] ),
+        format_cs_table_t( OUT_BBOX, CS_RGB, [ CS_RGB ] ),
+        format_cs_table_t( OUT_SVG, CS_RGB, [ CS_RGB ] ),
+
+        format_cs_table_t( OUT_TEXT, CS_RGB, [ CS_RGB ] ),
+        format_cs_table_t( OUT_HTML, CS_RGB, [ CS_RGB ] ),
+        format_cs_table_t( OUT_XHTML, CS_RGB, [ CS_RGB ] ),
+        format_cs_table_t( OUT_STEXT, CS_RGB, [ CS_RGB ] ),
+        ]
+
+def stat_mtime(path):
+    try:
+        return os.path.getmtime(path)
+    except Exception:
+        return 0
+
+
+class worker_t:
+    def __init__( self):
+        self.num = 0
+        self.band = 0
+        self.list = None
+        self.ctm = None
+        self.tbounds = None
+        self.pix = None
+        self.bit = None
+        self.cookie = mupdf.FzCookie()
+
+class state:
+
+    output = None
+    out = None
+    output_pagenum = 0
+    output_file_per_page = 0
+
+    format_ = None
+    output_format = OUT_NONE
+
+    rotation = 0
+    resolution = 72
+    res_specified = 0
+    width = 0
+    height = 0
+    fit = 0
+
+    layout_w = mupdf.FZ_DEFAULT_LAYOUT_W
+    layout_h = mupdf.FZ_DEFAULT_LAYOUT_H
+    layout_em = mupdf.FZ_DEFAULT_LAYOUT_EM
+    layout_css = None
+    layout_use_doc_css = 1
+    min_line_width = 0.0
+
+    showfeatures = 0
+    showtime = 0
+    showmemory = 0
+    showmd5 = 0
+
+    no_icc = 0
+    ignore_errors = 0
+    uselist = 1
+    alphabits_text = 8
+    alphabits_graphics = 8
+
+    out_cs = CS_UNSET
+    proof_filename = None
+    proof_cs = mupdf.FzColorspace()
+    icc_filename = None
+    gamma_value = 1
+    invert = 0
+    band_height = 0
+    lowmemory = 0
+
+    quiet = 0
+    errored = 0
+    colorspace = mupdf.FzColorspace()
+    oi = None
+    spots = SPOTS_OVERPRINT_SIM
+    alpha = 0
+    useaccel = 1
+    filename = None
+    files = 0
+    num_workers = 0
+    workers = None
+    bander = None
+
+    layer_config = None
+
+
+class bgprint:
+    active = 0
+    started = 0
+    pagenum = 0
+    filename = None
+    list_ = None
+    page = None
+    interptime = 0
+    seps = None
+
+
+class timing:
+    count = 0
+    total = 0
+    min_ = 0
+    max_ = 0
+    mininterp = 0
+    maxinterp = 0
+    minpage = 0
+    maxpage = 0
+    minfilename = None
+    maxfilename = None
+    layout = 0
+    minlayout = 0
+    maxlayout = 0
+    minlayoutfilename = None
+    maxlayoutfilename = None
+
+
+
+def usage():
+    sys.stderr.write( f'''
+            mudraw version {mupdf.FZ_VERSION} "
+            Usage: mudraw [options] file [pages]
+            \t-p -\tpassword
+
+            \t-o -\toutput file name (%d for page number)
+            \t-F -\toutput format (default inferred from output file name)
+            \t\traster: png, pnm, pam, pbm, pkm, pwg, pcl, ps
+            \t\tvector: svg, pdf, trace
+            \t\ttext: txt, html, stext
+
+            \t-q\tbe quiet (don't print progress messages)
+            \t-s -\tshow extra information:
+            \t\tm - show memory use
+            \t\tt - show timings
+            \t\tf - show page features
+            \t\t5 - show md5 checksum of rendered image
+
+            \t-R -\trotate clockwise (default: 0 degrees)
+            \t-r -\tresolution in dpi (default: 72)
+            \t-w -\twidth (in pixels) (maximum width if -r is specified)
+            \t-h -\theight (in pixels) (maximum height if -r is specified)
+            \t-f -\tfit width and/or height exactly; ignore original aspect ratio
+            \t-B -\tmaximum band_height (pXm, pcl, pclm, ps, psd and png output only)
+            \t-T -\tnumber of threads to use for rendering (banded mode only)
+
+            \t-W -\tpage width for EPUB layout
+            \t-H -\tpage height for EPUB layout
+            \t-S -\tfont size for EPUB layout
+            \t-U -\tfile name of user stylesheet for EPUB layout
+            \t-X\tdisable document styles for EPUB layout
+            \t-a\tdisable usage of accelerator file
+
+            \t-c -\tcolorspace (mono, gray, grayalpha, rgb, rgba, cmyk, cmykalpha, filename of ICC profile)
+            \t-e -\tproof icc profile (filename of ICC profile)
+            \t-G -\tapply gamma correction
+            \t-I\tinvert colors
+
+            \t-A -\tnumber of bits of antialiasing (0 to 8)
+            \t-A -/-\tnumber of bits of antialiasing (0 to 8) (graphics, text)
+            \t-l -\tminimum stroked line width (in pixels)
+            \t-D\tdisable use of display list
+            \t-i\tignore errors
+            \t-L\tlow memory mode (avoid caching, clear objects after each page)
+            \t-P\tparallel interpretation/rendering
+            \t-N\tdisable ICC workflow (\"N\"o color management)
+            \t-O -\tControl spot/overprint rendering
+            \t\t 0 = No spot rendering
+            \t\t 1 = Overprint simulation (default)
+            \t\t 2 = Full spot rendering
+
+            \t-y l\tList the layer configs to stderr
+            \t-y -\tSelect layer config (by number)
+            \t-y -{{,-}}*\tSelect layer config (by number), and toggle the listed entries
+
+            \tpages\tcomma separated list of page numbers and ranges
+            ''')
+    sys.exit(1)
+
+
+gettime_first = None
+
+def gettime():
+    global gettime_first
+    if gettime_first is None:
+        gettime_first = time.time()
+
+    now = time.time()
+    return (now - gettime_first) * 1000
+
+
+def has_percent_d(s):
+    # find '%[0-9]*d' */
+    m = re.search( '%[0-9]*d', s)
+    if m:
+        return 1
+    return 0
+
+
+
+# Output file level (as opposed to page level) headers
+def file_level_headers():
+
+    if state.output_format in (OUT_STEXT, OUT_TRACE, OUT_XMLTEXT, OUT_BBOX):
+        state.out.fz_write_string( "<?xml version=\"1.0\"?>\n")
+
+    if state.output_format == OUT_HTML:
+        state.out.fz_print_stext_header_as_html()
+    if state.output_format == OUT_XHTML:
+        state.out.fz_print_stext_header_as_xhtml()
+
+    if state.output_format in (OUT_STEXT, OUT_TRACE, OUT_BBOX):
+        state.out.fz_write_string( f'<document name="{state.filename}">\n')
+
+    if state.output_format == OUT_PS:
+        state.out.fz_write_ps_file_header()
+
+    if state.output_format == OUT_PWG:
+        state.out.fz_write_pwg_file_header()
+
+    if state.output_format == OUT_PCLM:
+        opts = mupdf.FzPclmOptions( 'compression=flate')
+        state.bander = mupdf.FzBandWriter(state.out, opts)
+
+def file_level_trailers():
+    if state.output_format in (OUT_STEXT, OUT_TRACE, OUT_BBOX):
+        state.out.fz_write_string( "</document>\n")
+
+    if state.output_format == OUT_HTML:
+        state.out.fz_print_stext_trailer_as_html()
+    if state.output_format == OUT_XHTML:
+        state.out.fz_print_stext_trailer_as_xhtml()
+
+    if state.output_format == OUT_PS:
+        state.out.fz_write_ps_file_trailer( state.output_pagenum)
+
+def drawband( page, list_, ctm, tbounds, cookie, band_start, pix):
+
+    bit = None
+
+    if pix.alpha():
+        pix.fz_clear_pixmap()
+    else:
+        pix.fz_clear_pixmap_with_value( 255)
+
+    dev = mupdf.FzDevice( mupdf.FzMatrix(), pix, state.proof_cs)
+    if state.lowmemory:
+        dev.enable_device_hints( mupdf.FZ_NO_CACHE)
+    if state.alphabits_graphics == 0:
+        dev.enable_device_hints( mupdf.FZ_DONT_INTERPOLATE_IMAGES)
+    if list_:
+        list_.fz_run_display_list( dev, ctm, tbounds, cookie)
+    else:
+        page.fz_run_page( dev, ctm, cookie)
+    dev.fz_close_device()
+    dev = None  # lgtm [py/unused-local-variable]
+
+    if state.invert:
+        pix.fz_invert_pixmap()
+    if state.gamma_value != 1:
+        pix.fz_gamma_pixmap( state.gamma_value)
+
+    if ((state.output_format == OUT_PCL or state.output_format == OUT_PWG) and state.out_cs == CS_MONO) or (state.output_format == OUT_PBM) or (state.output_format == OUT_PKM):
+        bit = mupdf.FzBitmap( pix, mupdf.FzHalftone(), band_start)
+    return bit
+
+
+
+
+
+
+def dodrawpage( page, list_, pagenum, cookie, start, interptime, filename, bg, seps):
+
+    if state.output_file_per_page:
+        file_level_headers()
+
+    if list_:
+        mediabox = mupdf.FzRect( list_)
+    else:
+        mediabox = page.fz_bound_page()
+
+    if state.output_format == OUT_TRACE:
+        state.out.fz_write_string( "<page mediabox=\"%g %g %g %g\">\n" % (
+                mediabox.x0, mediabox.y0, mediabox.x1, mediabox.y1))
+        dev = mupdf.FzDevice( state.out)
+        if state.lowmemory:
+            dev.fz_enable_device_hints( mupdf.FZ_NO_CACHE)
+        if list_:
+            list_.fz_run_display_list( dev, mupdf.FzMatrix(), mupdf.FzRect(mupdf.fz_infinite_rect), cookie)
+        else:
+            page.fz_run_page( dev, fz_identity, cookie)
+        state.out.fz_write_string( "</page>\n")
+        dev.fz_close_device()
+        dev = None  # lgtm [py/unused-local-variable]
+
+    elif state.output_format == OUT_XMLTEXT:
+        state.out.fz_write_string( "<page mediabox=\"%g %g %g %g\">\n" % (
+                mediabox.x0, mediabox.y0, mediabox.x1, mediabox.y1))
+        dev = mupdf.FzDevice.fz_new_raw_device( state.out)
+        if list_:
+            list_.fz_run_display_list( dev, mupdf.FzMatrix(), mupdf.FzRect(mupdf.fz_infinite_rect), cookie)
+        else:
+            page.fz_run_page( dev, fz_identity, cookie)
+        state.out.fz_write_string( "</page>\n")
+        dev.fz_close_device()
+        dev = None  # lgtm [py/unused-local-variable]
+
+    elif state.output_format == OUT_BBOX:
+        bbox = mupdf.FzRect( mupdf.FzRect.Fixed_EMPTY)
+        dev = mupdf.FzDevice( bbox)
+        if state.lowmemory:
+            dev.fz_enable_device_hints( mupdf.FZ_NO_CACHE)
+        if list_:
+            list_.fz_run_display_list( dev, fz_identity, mupdf.FzRect(mupdf.fz_infinite_rect), cookie)
+        else:
+            page.fz_run_page( dev, fz_identity, cookie)
+        dev.fz_close_device()
+        state.out.fz_write_string( "<page bbox=\"%s %s %s %s\" mediabox=\"%s %s %s %s\" />\n",
+                bbox.x0,
+                bbox.y0,
+                bbox.x1,
+                bbox.y1,
+                mediabox.x0,
+                mediabox.y0,
+                mediabox.x1,
+                mediabox.y1,
+                )
+
+    elif state.output_format in (OUT_TEXT, OUT_HTML, OUT_XHTML, OUT_STEXT):
+        zoom = state.resolution / 72
+        ctm = mupdf.FzMatrix(mupdf.fz_pre_scale(mupdf.fz_rotate(state.rotation), zoom, zoom))
+
+        stext_options = mupdf.FzStextOptions()
+
+        stext_options.flags = mupdf.FZ_STEXT_PRESERVE_IMAGES if (state.output_format == OUT_HTML or state.output_format == OUT_XHTML) else 0
+        text = mupdf.FzStextPage( mediabox)
+        dev = mupdf.FzDevice( text, stext_options)
+        if state.lowmemory:
+            fz_enable_device_hints( dev, FZ_NO_CACHE)
+        if list_:
+            list_.fz_run_display_list( dev, ctm, mupdf.FzRect(mupdf.fz_infinite_rect), cookie)
+        else:
+            page.fz_run_page( dev, ctm, cookie)
+        dev.fz_close_device()
+        dev = None  # lgtm [py/unused-local-variable]
+        if state.output_format == OUT_STEXT:
+            state.out.fz_print_stext_page_as_xml( text, pagenum)
+        elif state.output_format == OUT_HTML:
+            state.out.fz_print_stext_page_as_html( text, pagenum)
+        elif state.output_format == OUT_XHTML:
+            state.out.fz_print_stext_page_as_xhtml( text, pagenum)
+        elif state.output_format == OUT_TEXT:
+            state.out.fz_print_stext_page_as_text( text)
+            state.out.fz_write_string( "\f\n")
+
+    elif state.output_format == OUT_SVG:
+        zoom = state.resolution / 72
+        ctm = mupdf.FzMatrix(zoom, zoom)
+        ctm.fz_pre_rotate( state.rotation)
+        tbounds = mupdf.FzRect(mediabox, ctm)
+
+        if not state.output or state.output == "-":
+            state.out = mupdf.FzOutput( mupdf.FzOutput.Fixed_STDOUT)
+        else:
+            buf = mupdf.fz_format_output_path( state.output, pagenum)
+            state.out = mupdf.FzOutput( buf, 0)
+
+        dev = mupdf.FzDevice( state.out, tbounds.x1-tbounds.x0, tbounds.y1-tbounds.y0, mupdf.FZ_SVG_TEXT_AS_PATH, 1)
+        if state.lowmemory:
+            dev.fz_enable_device_hints( dev, mupdf.FZ_NO_CACHE)
+        if list_:
+            list_.fz_run_display_list( dev, ctm, tbounds, cookie)
+        else:
+            page.fz_run_page( dev, ctm, cookie)
+        dev.fz_close_device()
+        state.out.fz_close_output()
+    else:
+        zoom = state.resolution / 72
+        ctm = mupdf.fz_pre_scale( mupdf.fz_rotate(state.rotation), zoom, zoom)
+        tbounds = mupdf.FzRect(mediabox, ctm)
+        ibounds = tbounds.fz_round_rect()
+
+        # Make local copies of our width/height
+        w = state.width
+        h = state.height
+
+        # If a resolution is specified, check to see whether w/h are
+        # exceeded; if not, unset them. */
+        if state.res_specified:
+            t = ibounds.x1 - ibounds.x0
+            if w and t <= w:
+                w = 0
+            t = ibounds.y1 - ibounds.y0
+            if h and t <= h:
+                h = 0
+
+        # Now w or h will be 0 unless they need to be enforced.
+        if w or h:
+            scalex = w / (tbounds.x1 - tbounds.x0)
+            scaley = h / (tbounds.y1 - tbounds.y0)
+
+            if state.fit:
+                if w == 0:
+                    scalex = 1.0
+                if h == 0:
+                    scaley = 1.0
+            else:
+                if w == 0:
+                    scalex = scaley
+                if h == 0:
+                    scaley = scalex
+            if not state.fit:
+                if scalex > scaley:
+                    scalex = scaley
+                else:
+                    scaley = scalex
+            scale_mat = mupdf.fz_scale(scalex, scaley)
+            ctm = mupdf.fz_concat(ctm, scale_mat)
+            tbounds = mupdf.FzRect( mediabox, ctm)
+        ibounds = tbounds.fz_round_rect()
+        tbounds = ibounds.fz_rect_from_irect()
+
+        band_ibounds = ibounds
+        bands = 1
+        totalheight = ibounds.y1 - ibounds.y0
+        drawheight = totalheight
+
+        if state.band_height != 0:
+            # Banded rendering; we'll only render to a
+            # given height at a time.
+            drawheight = state.band_height
+            if totalheight > state.band_height:
+                band_ibounds.y1 = band_ibounds.y0 + state.band_height
+            bands = (totalheight + state.band_height-1)/state.band_height
+            tbounds.y1 = tbounds.y0 + state.band_height + 2
+            #DEBUG_THREADS(("Using %d Bands\n", bands));
+
+        if state.num_workers > 0:
+            for band in range( min(state.num_workers, bands)):
+                state.workers[band].band = band
+                state.workers[band].ctm = ctm
+                state.workers[band].tbounds = tbounds
+                state.workers[band].cookie = mupdf.FzCookie()
+                state.workers[band].list = list_
+                state.workers[band].pix = mupdf.FzPixmap( state.colorspace, band_ibounds, seps, state.alpha)
+                state.workers[band].pix.fz_set_pixmap_resolution( state.resolution, state.resolution)
+                ctm.f -= drawheight
+            pix = state.workers[0].pix
+        else:
+            pix = mupdf.FzPixmap( state.colorspace, band_ibounds, seps, state.alpha)
+            pix.fz_set_pixmap_resolution( int(state.resolution), int(state.resolution))
+
+        # Output any page level headers (for banded formats)
+        if state.output:
+            state.bander = None
+            if state.output_format == OUT_PGM or state.output_format == OUT_PPM or state.output_format == OUT_PNM:
+                state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.PNM)
+            elif state.output_format == OUT_PAM:
+                state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.PAM)
+            elif state.output_format == OUT_PNG:
+                state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.PNG)
+            elif state.output_format == OUT_PBM:
+                state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.PBM)
+            elif state.output_format == OUT_PKM:
+                state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.PKM)
+            elif state.output_format == OUT_PS:
+                state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.PS)
+            elif state.output_format == OUT_PSD:
+                state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.PSD)
+            elif state.output_format == OUT_PWG:
+                if state.out_cs == CS_MONO:
+                    state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.MONO, mupdf.FzPwgOptions())
+                else:
+                    state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.COLOR, mupdf.FzPwgOptions())
+            elif state.output_format == OUT_PCL:
+                if state.out_cs == CS_MONO:
+                    state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.MONO, mupdf.FzPclOptions())
+                else:
+                    state.bander = mupdf.FzBandWriter( state.out, mupdf.FzBandWriter.COLOR, mupdf.FzPclOptions())
+            if state.bander:
+                state.bander.fz_write_header( pix.w(), totalheight, pix.n(), pix.alpha(), pix.xres(), pix.yres(), state.output_pagenum, pix.colorspace(), pix.seps())
+                state.output_pagenum += 1
+
+        for band in range( bands):
+            if state.num_workers > 0:
+                w = state.workers[band % state.num_workers]
+                pix = w.pix
+                bit = w.bit
+                w.bit = None
+                cookie.fz_increment_errors(w.cookie.errors())
+
+            else:
+                bit = drawband( page, list_, ctm, tbounds, cookie, band * state.band_height, pix)
+
+            if state.output:
+                if state.bander:
+                    if bit:
+                        state.bander.fz_write_band( bit.stride(), drawheight, bit.samples())
+                    else:
+                        state.bander.fz_write_band( pix.stride(), drawheight, pix.samples())
+                bit = None
+
+            if state.num_workers > 0 and band + state.num_workers < bands:
+                w = state.workers[band % state.num_workers]
+                w.band = band + state.num_workers
+                w.ctm = ctm
+                w.tbounds = tbounds
+                w.cookie = mupdf.FzCookie()
+            ctm.f -= drawheight
+
+        # FIXME
+        if state.showmd5:
+            digest = pix.fz_md5_pixmap()
+            sys.stderr.write( ' ')
+            for i in range(16):
+                sys.stderr.write( '%02x', digest[i])
+
+    if state.output_file_per_page:
+        file_level_trailers()
+
+    if state.showtime:
+        end = gettime()
+        diff = end - start
+
+        if bg:
+            if diff + interptime < timing.min:
+                timing.min = diff + interptime
+                timing.mininterp = interptime
+                timing.minpage = pagenum
+                timing.minfilename = filename
+            if diff + interptime > timing.max:
+                timing.max = diff + interptime
+                timing.maxinterp = interptime
+                timing.maxpage = pagenum
+                timing.maxfilename = filename
+            timing.count += 1
+
+            sys.stderr.write( " %dms (interpretation) %dms (rendering) %dms (total)" % (interptime, diff, diff + interptime))
+        else:
+            if diff < timing.min:
+                timing.min = diff
+                timing.minpage = pagenum
+                timing.minfilename = filename
+            if diff > timing.max:
+                timing.max = diff
+                timing.maxpage = pagenum
+                timing.maxfilename = filename
+
+            timing.total += diff
+            timing.count += 1
+
+            sys.stderr.write( " %dms" % diff)
+
+    if not state.quiet or state.showfeatures or state.showtime or state.showmd5:
+        sys.stderr.write( "\n")
+
+    if state.lowmemory:
+        mupdf.fz_empty_store()
+
+    if state.showmemory:
+        # Use low-level fn because mupdf.fz_stderr() returns fz_output*, not
+        # FzOutput.
+        mupdf.ll_fz_dump_glyph_cache_stats(mupdf.ll_fz_stderr())
+
+    mupdf.fz_flush_warnings()
+
+    if cookie.errors():
+        state.errored = 1
+
+
+
+def bgprint_flush():
+    if not bgprint.active or not bgprint.started:
+        return
+    bgprint.started = 0
+
+
+
+def drawpage( doc, pagenum):
+    list_ = None
+    cookie = mupdf.FzCookie()
+    seps = None
+    features = ""
+
+    start = gettime() if state.showtime else 0
+
+    page = mupdf.FzPage( doc, pagenum - 1)
+
+    if state.spots != SPOTS_NONE:
+        seps = page.fz_page_separations()
+        if seps.m_internal:
+            n = seps.fz_count_separations()
+            if state.spots == SPOTS_FULL:
+                for i in range(n):
+                    seps.fz_set_separation_behavior( i, mupdf.FZ_SEPARATION_SPOT)
+            else:
+                for i in range(n):
+                    seps.fz_set_separation_behavior( i, mupdf.FZ_SEPARATION_COMPOSITE)
+        elif page.fz_page_uses_overprint():
+            # This page uses overprint, so we need an empty
+            # sep object to force the overprint simulation on.
+            seps = mupdf.FzSeparations(0)
+        elif state.oi and state.oi.m_internal and state.oi.fz_colorspace_n() != state.colorspace.fz_colorspace_n():
+            # We have an output intent, and it's incompatible
+            # with the colorspace our device needs. Force the
+            # overprint simulation on, because this ensures that
+            # we 'simulate' the output intent too. */
+            seps = mupdf.FzSeparations(0)
+
+    if state.uselist:
+        list_ = mupdf.FzDisplayList( page.fz_bound_page())
+        dev = mupdf.FzDevice( list_)
+        if state.lowmemory:
+            dev.fz_enable_device_hints( FZ_NO_CACHE)
+        page.fz_run_page( dev, mupdf.FzMatrix(), cookie)
+        dev.fz_close_device()
+
+        if bgprint.active and state.showtime:
+            end = gettime()
+            start = end - start
+
+    if state.showfeatures:
+        # SWIG doesn't appear to handle the out-param is_color in
+        # mupdf.Device() constructor that wraps fz_new_test_device(), so we use
+        # the underlying mupdf function() instead.
+        #
+        dev, iscolor = mupdf.ll_fz_new_test_device( 0.02, 0, None)
+        dev = mupdf.FzDevice( dev)
+        if state.lowmemory:
+            dev.fz_enable_device_hints( mupdf.FZ_NO_CACHE)
+        if list_:
+            list_.fz_run_display_list( dev, mupdf.FzMatrix(mupdf.fz_identity), mupdf.FzRect(mupdf.fz_infinite_rect), mupdf.FzCookie())
+        else:
+            page.fz_run_page( dev, fz_identity, cookie)
+        dev.fz_close_device()
+        features = " color" if iscolor else " grayscale"
+
+    if state.output_file_per_page:
+        bgprint_flush()
+        if state.out:
+            state.out.fz_close_output()
+        text_buffer = mupdf.fz_format_output_path( state.output, pagenum)
+        state.out = mupdf.FzOutput( text_buffer, 0)
+
+    if bgprint.active:
+        bgprint_flush()
+        if bgprint.active:
+            if not state.quiet or state.showfeatures or state.showtime or state.showmd5:
+                sys.stderr.write( "page %s %d%s" % (state.filename, pagenum, features))
+
+        bgprint.started = 1
+        bgprint.page = page
+        bgprint.list = list_
+        bgprint.seps = seps
+        bgprint.filename = state.filename
+        bgprint.pagenum = pagenum
+        bgprint.interptime = start
+    else:
+        if not state.quiet or state.showfeatures or state.showtime or state.showmd5:
+            sys.stderr.write( "page %s %d%s" % (state.filename, pagenum, features))
+        dodrawpage( page, list_, pagenum, cookie, start, 0, state.filename, 0, seps)
+
+
+
+
+
+
+
+
+
+
+
+def drawrange( doc, range_):
+    pagecount = doc.fz_count_pages()
+
+    while 1:
+        range_, spage, epage = mupdf.fz_parse_page_range( range_, pagecount)
+        if range_ is None:
+            break
+        if spage < epage:
+            for page in range(spage, epage+1):
+                drawpage( doc, page)
+        else:
+            for page in range( spage, epage-1, -1):
+                drawpage( doc, page)
+
+
+
+def parse_colorspace( name):
+    ret = cs_name_table.get( name)
+    if ret:
+        return ret
+    state.icc_filename = name
+    return CS_ICC
+
+
+class trace_info:
+    def __init__( self):
+        self.current = 0
+        self.peak = 0
+        self.total = 0
+
+
+def iswhite(ch):
+    return (
+        ch == '\011' or ch == '\012' or
+        ch == '\014' or ch == '\015' or ch == '\040'
+        )
+
+def apply_layer_config( doc, lc):
+    pass
+
+
+
+
+def convert_to_accel_path(absname):
+    tmpdir = os.getenv('TEMP')
+    if  not tmpdir:
+        tmpdir = os.getenv('TMP')
+    if not tmpdir:
+        tmpdir = '/var/tmp'
+    if not os.path.isdir(tmpdir):
+        tmpdir = '/tmp'
+
+    if absname.startswith( '/') or absname.startswith( '\\'):
+        absname = absname[1:]
+
+    absname = absname.replace( '/', '%')
+    absname = absname.replace( '\\', '%')
+    absname = absname.replace( ':', '%')
+
+    return '%s/%s.accel' % (tmpdir, absname)
+
+def get_accelerator_filename( filename):
+    absname = os.path.realpath( filename)
+    return convert_to_accel_path( absname)
+
+def save_accelerator(doc, filename):
+    if not doc.fz_document_supports_accelerator():
+        return
+    absname = get_accelerator_filename( filename)
+    doc.fz_save_accelerator( absname)
+
+
+def draw( argv):
+
+    password = ''
+    info = trace_info()
+
+    items, argv = getopt.getopt( argv, 'qp:o:F:R:r:w:h:fB:c:e:G:Is:A:DiW:H:S:T:U:XLvPl:y:NO:a')
+    for option, value in items:
+        if 0:   pass
+        elif option == '-q':    state.quiet = 1
+        elif option == '-p':    password = value
+        elif option == '-o':    state.output = value
+        elif option == '-F':    state.format_ = value
+        elif option == '-R':    state.rotation = float( value)
+        elif option == '-r':
+            state.resolution = float( value)
+            state.res_specified = 1
+        elif option == '-w':    state.width = float( value)
+        elif option == '-h':    state.height = float( value)
+        elif option == '-f':    state.fit = 1
+        elif option == '-B':    state.band_height = int( value)
+        elif option == '-c':    state.out_cs = parse_colorspace( value)
+        elif option == '-e':    state.proof_filename = value
+        elif option == '-G':    state.gamma_value = float( value)
+        elif option == '-I':    state.invert += 1
+        elif option == '-W':    state.layout_w = float( value)
+        elif option == '-H':    state.layout_h = float( value)
+        elif option == '-S':    state.layout_em = float( value)
+        elif option == '-U':    state.layout_css = value
+        elif option == '-X':    state.layout_use_doc_css = 0
+        elif option == '-O':
+            state.spots = float( value)
+            if not mupdf.FZ_ENABLE_SPOT_RENDERING:
+                sys.stderr.write( 'Spot rendering/Overprint/Overprint simulation not enabled in this build\n')
+                state.spots = SPOTS_NONE
+        elif option == '-s':
+            if 't' in value: state.showtime += 1
+            if 'm' in value: state.showmemory += 1
+            if 'f' in value: state.showfeatures += 1
+            if '5' in value: state.showmd5 += 1
+
+        elif option == '-A':
+            state.alphabits_graphics = int(value)
+            sep = value.find( '/')
+            if sep >= 0:
+                state.alphabits_text = int(value[sep+1:])
+            else:
+                state.alphabits_text = state.alphabits_graphics
+        elif option == '-D': state.uselist = 0
+        elif option == '-l': state.min_line_width = float(value)
+        elif option == '-i': state.ignore_errors = 1
+        elif option == '-N': state.no_icc = 1
+
+        elif option == '-T': state.num_workers = int(value)
+        elif option == '-L': state.lowmemory = 1
+        elif option == '-P': bgprint.active = 1
+        elif option == '-y': state.layer_config = value
+        elif option == '-a': state.useaccel = 0
+
+        elif option == '-v': sys.stderr.write( f'mudraw version {mupdf.FZ_VERSION}\n')
+
+    if not argv:
+        usage()
+
+    if state.num_workers > 0:
+        if state.uselist == 0:
+            sys.stderr.write('cannot use multiple threads without using display list\n')
+            sys.exit(1)
+
+        if state.band_height == 0:
+            sys.stderr.write('Using multiple threads without banding is pointless\n')
+
+    if bgprint.active:
+        if state.uselist == 0:
+            sys.stderr.write('cannot bgprint without using display list\n')
+            sys.exit(1)
+
+    if state.proof_filename:
+        proof_buffer = mupdf.FzBuffer( state.proof_filename)
+        state.proof_cs = mupdf.FzColorspace( FZ_COLORSPACE_NONE, 0, None, proof_buffer)
+
+    mupdf.fz_set_text_aa_level( state.alphabits_text)
+    mupdf.fz_set_graphics_aa_level( state.alphabits_graphics)
+    mupdf.fz_set_graphics_min_line_width( state.min_line_width)
+    if state.no_icc:
+        mupdf.fz_disable_icc()
+    else:
+        mupdf.fz_enable_icc()
+
+    if state.layout_css:
+        buf = mupdf.FzBuffer( state.layout_css)
+        mupdf.fz_set_user_css( buf.string_from_buffer())
+
+    mupdf.fz_set_use_document_css( state.layout_use_doc_css)
+
+    # Determine output type
+    if state.band_height < 0:
+        sys.stderr.write( 'Bandheight must be > 0\n')
+        sys.exit(1)
+
+    state.output_format = OUT_PNG
+    if state.format_:
+        for i in range(len(suffix_table)):
+            if state.format_ == suffix_table[i].suffix[1:]:
+                state.output_format = suffix_table[i].format
+                if state.spots == SPOTS_FULL and suffix_table[i].spots == 0:
+                    sys.stderr.write( f'Output format {suffix_table[i].suffix[1:]} does not support spot rendering.\nDoing overprint simulation instead.\n')
+                    state.spots = SPOTS_OVERPRINT_SIM
+                break
+        else:
+            sys.stderr.write( f'Unknown output format {format}\n')
+            sys.exit(1)
+    elif state.output:
+        suffix = state.output
+        i = 0
+        while 1:
+            if i == len(suffix_table):
+                break
+            s = suffix.find( suffix_table[i].suffix)
+            if s != -1:
+                suffix = suffix_table[i].suffix[s+1:]
+                state.output_format = suffix_table[i].format
+                if state.spots == SPOTS_FULL and suffix_table[i].spots == 0:
+                    sys.stderr.write( 'Output format {suffix_table[i].suffix[1:]} does not support spot rendering\nDoing overprint simulation instead.\n')
+                    state.spots = SPOTS_OVERPRINT_SIM
+                i = 0
+            else:
+                i += 1
+
+    if state.band_height:
+        if state.output_format not in ( OUT_PAM, OUT_PGM, OUT_PPM, OUT_PNM, OUT_PNG, OUT_PBM, OUT_PKM, OUT_PCL, OUT_PCLM, OUT_PS, OUT_PSD):
+            sys.stderr.write( 'Banded operation only possible with PxM, PCL, PCLM, PS, PSD, and PNG outputs\n')
+            sys.exit(1)
+        if state.showmd5:
+            sys.stderr.write( 'Banded operation not compatible with MD5\n')
+            sys.exit(1)
+
+    for i in range(len(format_cs_table)):
+        if format_cs_table[i].format == state.output_format:
+            if state.out_cs == CS_UNSET:
+                state.out_cs = format_cs_table[i].default_cs
+            for j in range( len(format_cs_table[i].permitted_cs)):
+                if format_cs_table[i].permitted_cs[j] == state.out_cs:
+                    break
+            else:
+                sys.stderr.write( 'Unsupported colorspace for this format\n')
+                sys.exit(1)
+
+    state.alpha = 1
+    if state.out_cs in ( CS_MONO, CS_GRAY, CS_GRAY_ALPHA):
+        state.colorspace = mupdf.FzColorspace( mupdf.FzColorspace.Fixed_GRAY)
+        state.alpha = (state.out_cs == CS_GRAY_ALPHA)
+    elif state.out_cs in ( CS_RGB, CS_RGB_ALPHA):
+        state.colorspace = mupdf.FzColorspace( mupdf.FzColorspace.Fixed_RGB)
+        state.alpha = (state.out_cs == CS_RGB_ALPHA)
+    elif state.out_cs in ( CS_CMYK, CS_CMYK_ALPHA):
+        state.colorspace = mupdf.FzColorspace( mupdf.FzColorspace.Fixed_CMYK)
+        state.alpha = (state.out_cs == CS_CMYK_ALPHA)
+    elif state.out_cs == CS_ICC:
+        try:
+            icc_buffer = mupdf.FzBuffer( state.icc_filename)
+            state.colorspace = Colorspace( mupdf.FZ_COLORSPACE_NONE, 0, None, icc_buffer)
+        except Exception as e:
+            sys.stderr.write( 'Invalid ICC destination color space\n')
+            sys.exit(1)
+        if state.colorspace.m_internal is None:
+            sys.stderr.write( 'Invalid ICC destination color space\n')
+            sys.exit(1)
+        state.alpha = 0
+    else:
+        sys.stderr.write( 'Unknown colorspace!\n')
+        sys.exit(1)
+
+    if state.out_cs != CS_ICC:
+        state.colorspace = mupdf.FzColorspace( state.colorspace)
+    else:
+        # Check to make sure this icc profile is ok with the output format */
+        okay = 0
+        for i in range( len(format_cs_table)):
+            if format_cs_table[i].format == state.output_format:
+                for j in range( len(format_cs_table[i].permitted_cs)):
+                    x = format_cs_table[i].permitted_cs[j]
+                    if x in ( CS_MONO, CS_GRAY, CS_GRAY_ALPHA):
+                        if state.colorspace.fz_colorspace_is_gray():
+                            okay = 1
+                        elif x in ( CS_RGB, CS_RGB_ALPHA):
+                            if state.colorspace.fz_colorspace_is_rgb():
+                                okay = 1
+                        elif x in ( CS_CMYK, CS_CMYK_ALPHA):
+                            if state.colorspace.fz_colorspace_is_cmyk():
+                                okay = 1
+
+        if not okay:
+            sys.stderr.write( 'ICC profile uses a colorspace that cannot be used for this format\n')
+            sys.exit(1)
+
+    if state.output_format == OUT_SVG:
+        # SVG files are always opened for each page. Do not open "output".
+        pass
+    elif state.output and (not state.output.startswith('-') or len(state.output) >= 2) and len(state.output) >= 1:
+        if has_percent_d(state.output):
+            state.output_file_per_page = 1
+        else:
+            state.out = mupdf.FzOutput(state.output, 0)
+    else:
+        state.quiet = 1 # automatically be quiet if printing to stdout
+        if 0:   # lgtm [py/unreachable-statement]
+            # Windows specific code to make stdout binary.
+            if state.output_format not in( OUT_TEXT, OUT_STEXT, OUT_HTML, OUT_XHTML, OUT_TRACE, OUT_XMLTEXT):
+                setmode(fileno(stdout), O_BINARY)
+        state.out = mupdf.FzOutput( mupdf.FzOutput.Fixed_STDOUT)
+
+    state.filename = argv[0]
+    if not state.output_file_per_page:
+        file_level_headers()
+
+    timing.count = 0
+    timing.total = 0
+    timing.min = 1 << 30
+    timing.max = 0
+    timing.mininterp = 1 << 30
+    timing.maxinterp = 0
+    timing.minpage = 0
+    timing.maxpage = 0
+    timing.minfilename = ""
+    timing.maxfilename = ""
+    timing.layout = 0
+    timing.minlayout = 1 << 30
+    timing.maxlayout = 0
+    timing.minlayoutfilename = ""
+    timing.maxlayoutfilename = ""
+
+    if state.showtime and bgprint.active:
+        timing.total = gettime()
+
+    fz_optind = 0
+    try:
+        while fz_optind < len( argv):
+
+            try:
+                accel = None
+
+                state.filename = argv[fz_optind]
+                fz_optind += 1
+
+                state.files += 1
+
+                if not state.useaccel:
+                    accel = None
+                # If there was an accelerator to load, what would it be called?
+                else:
+                    accelpath = get_accelerator_filename( state.filename)
+                    # Check whether that file exists, and isn't older than
+                    # the document.
+                    atime = stat_mtime( accelpath)
+                    dtime = stat_mtime( state.filename)
+                    if atime == 0:
+                        # No accelerator
+                        pass
+                    elif atime > dtime:
+                        accel = accelpath
+                    else:
+                        # Accelerator data is out of date
+                        os.unlink( accelpath)
+                        accel = None # In case we have jumped up from below
+
+                # Unfortunately if accel=None, SWIG doesn't seem to think of it
+                # as a char*, so we end up in fz_open_document_with_stream().
+                #
+                # If we try to avoid this by setting accel='', SWIG correctly
+                # calls Document(const char *filename, const char *accel) =>
+                # fz_open_accelerated_document(), but the latter function tests
+                # for NULL not "" so fails.
+                #
+                # So we choose the constructor explicitly rather than leaving
+                # it up to SWIG.
+                #
+                if accel:
+                    doc = mupdf.FzDocument(state.filename, accel)
+                else:
+                    doc = mupdf.FzDocument(state.filename)
+
+                if doc.fz_needs_password():
+                    if not doc.fz_authenticate_password( password):
+                        raise Exception( f'cannot authenticate password: {state.filename}')
+
+                # Once document is open check for output intent colorspace
+                state.oi = doc.fz_document_output_intent()
+                if state.oi.m_internal:
+                    # See if we had explicitly set a profile to render
+                    if state.out_cs != CS_ICC:
+                        # In this case, we want to render to the output intent
+                        # color space if the number of channels is the same
+                        if state.oi.fz_colorspace_n() == state.colorspace.fz_colorspace_n():
+                            state.colorspace = state.oi
+
+                layouttime = time.time()
+                doc.fz_layout_document( state.layout_w, state.layout_h, state.layout_em)
+                doc.fz_count_pages()
+                layouttime = time.time() - layouttime
+
+                timing.layout += layouttime
+                if layouttime < timing.minlayout:
+                    timing.minlayout = layouttime
+                    timing.minlayoutfilename = state.filename
+                if layouttime > timing.maxlayout:
+                    timing.maxlayout = layouttime
+                    timing.maxlayoutfilename = state.filename
+
+                if state.layer_config:
+                    apply_layer_config( doc, state.layer_config)
+
+                if fz_optind == len(argv) or not mupdf.fz_is_page_range( argv[fz_optind]):
+                    drawrange( doc, "1-N")
+                if fz_optind < len( argv) and mupdf.fz_is_page_range( argv[fz_optind]):
+                    drawrange( doc, argv[fz_optind])
+                    fz_optind += 1
+
+                bgprint_flush()
+
+                if state.useaccel:
+                    save_accelerator( doc, state.filename)
+            except Exception as e:
+                if not state.ignore_errors:
+                    raise
+                bgprint_flush()
+                sys.stderr.write( f'ignoring error in {state.filename}\n')
+
+    except Exception as e:
+        bgprint_flush()
+        sys.stderr.write( f'error: cannot draw \'{state.filename}\' because: {e}\n')
+        state.errored = 1
+        if 0:
+            # Enable for debugging.
+            import traceback
+            traceback.print_exc()
+
+    if not state.output_file_per_page:
+        file_level_trailers()
+
+    if state.out:
+        state.out.fz_close_output()
+    state.out = None
+
+    if state.showtime and timing.count > 0:
+        if bgprint.active:
+            timing.total = gettime() - timing.total
+
+        if state.files == 1:
+            sys.stderr.write( f'total {timing.total:.0f}ms ({timing.layout:.0f}ms layout) / {timing.count} pages for an average of {timing.total / timing.count:.0f}ms\n')
+            if bgprint.active:
+                sys.stderr.write( f'fastest page {timing.minpage}: {timing.mininterp:.0f}ms (interpretation) {timing.min - timing.mininterp:.0f}ms (rendering) {timing.min:.0f}ms(total)\n')
+                sys.stderr.write( f'slowest page {timing.maxpage}: {timing.maxinterp:.0f}ms (interpretation) {timing.max - timing.maxinterp:.0f}ms (rendering) {timing.max:.0f}ms(total)\n')
+            else:
+                sys.stderr.write( f'fastest page {timing.minpage}: {timing.min:.0f}ms\n')
+                sys.stderr.write( f'slowest page {timing.maxpage}: {timing.max:.0f}ms\n')
+        else:
+            sys.stderr.write( f'total {timing.total:.0f}ms ({timing.layout:.0f}ms layout) / {timing.count} pages for an average of {timing.total / timing.count:.0f}ms in {state.files} files\n')
+            sys.stderr.write( f'fastest layout: {timing.minlayout:.0f}ms ({timing.minlayoutfilename})\n')
+            sys.stderr.write( f'slowest layout: {timing.maxlayout:.0f}ms ({timing.maxlayoutfilename})\n')
+            sys.stderr.write( f'fastest page {timing.minpage}: {timing.min:.0f}ms ({timing.minfilename})\n')
+            sys.stderr.write( f'slowest page {timing.maxpage}: {timing.max:.0f}ms ({timing.maxfilename})\n')
+
+    if state.showmemory:
+        sys.stderr.write( f'Memory use total={info.total} peak={info.peak} current={info.current}\n')
+
+    return state.errored != 0