1import collections
2import getopt
3import os
4import re
5import sys
6import time
7
8import mupdf
9
10
11# Force stderr to be line-buffered - i.e. python will flush to the underlying
12# stderr stream every newline. This ensures that our output interleaves with
13# the output of mupdf C code, making it easier to compare our output with that
14# of mutool.
15#
16sys.stderr = os.fdopen( os.dup( sys.stderr.fileno()), 'w', 1)
17
18
19OUT_NONE    = 0
20OUT_PNG     = 1
21OUT_PNM     = 2
22OUT_PGM     = 3
23OUT_PPM     = 4
24OUT_PAM     = 5
25OUT_PBM     = 6
26OUT_PKM     = 7
27OUT_PWG     = 8
28OUT_PCL     = 9
29OUT_PS      = 10
30OUT_PSD     = 11
31OUT_TEXT    = 12
32OUT_HTML    = 13
33OUT_XHTML   = 14
34OUT_STEXT   = 15
35OUT_PCLM    = 16
36OUT_TRACE   = 17
37OUT_BBOX    = 18
38OUT_SVG     = 19
39OUT_XMLTEXT = 20
40
41CS_INVALID      = 0
42CS_UNSET        = 1
43CS_MONO         = 2
44CS_GRAY         = 3
45CS_GRAY_ALPHA   = 4
46CS_RGB          = 5
47CS_RGB_ALPHA    = 6
48CS_CMYK         = 7
49CS_CMYK_ALPHA   = 8
50CS_ICC          = 9
51
52CS_INVALID      = 0
53CS_UNSET        = 1
54CS_MONO         = 2
55CS_GRAY         = 3
56CS_GRAY_ALPHA   = 4
57CS_RGB          = 5
58CS_RGB_ALPHA    = 6
59CS_CMYK         = 7
60CS_CMYK_ALPHA   = 8
61CS_ICC          = 9
62
63SPOTS_NONE          = 0
64SPOTS_OVERPRINT_SIM = 1
65SPOTS_FULL          = 2
66
67
68class suffix_t:
69    def __init__( self, suffix, format_, spots):
70        self.suffix = suffix
71        self.format = format_
72        self.spots = spots
73
74suffix_table = [
75        suffix_t( ".png", OUT_PNG, 0 ),
76        suffix_t( ".pgm", OUT_PGM, 0 ),
77        suffix_t( ".ppm", OUT_PPM, 0 ),
78        suffix_t( ".pnm", OUT_PNM, 0 ),
79        suffix_t( ".pam", OUT_PAM, 0 ),
80        suffix_t( ".pbm", OUT_PBM, 0 ),
81        suffix_t( ".pkm", OUT_PKM, 0 ),
82        suffix_t( ".svg", OUT_SVG, 0 ),
83        suffix_t( ".pwg", OUT_PWG, 0 ),
84        suffix_t( ".pclm", OUT_PCLM, 0 ),
85        suffix_t( ".pcl", OUT_PCL, 0 ),
86        suffix_t( ".psd", OUT_PSD, 1 ),
87        suffix_t( ".ps", OUT_PS, 0 ),
88
89        suffix_t( ".txt", OUT_TEXT, 0 ),
90        suffix_t( ".text", OUT_TEXT, 0 ),
91        suffix_t( ".html", OUT_HTML, 0 ),
92        suffix_t( ".xhtml", OUT_XHTML, 0 ),
93        suffix_t( ".stext", OUT_STEXT, 0 ),
94
95        suffix_t( ".trace", OUT_TRACE, 0 ),
96        suffix_t( ".raw", OUT_XMLTEXT, 0 ),
97        suffix_t( ".bbox", OUT_BBOX, 0 ),
98        ]
99
100class cs_name_t:
101    def __init__( self, name, colorspace):
102        self.name = name
103        self.colorspace = colorspace
104
105cs_name_table = dict(
106        m           = CS_MONO,
107        mono        = CS_MONO,
108        g           = CS_GRAY,
109        gray        = CS_GRAY,
110        grey        = CS_GRAY,
111        ga          = CS_GRAY_ALPHA,
112        grayalpha   = CS_GRAY_ALPHA,
113        greyalpha   = CS_GRAY_ALPHA,
114        rgb         = CS_RGB,
115        rgba        = CS_RGB_ALPHA,
116        rgbalpha    = CS_RGB_ALPHA,
117        cmyk        = CS_CMYK,
118        cmyka       = CS_CMYK_ALPHA,
119        cmykalpha   = CS_CMYK_ALPHA,
120        )
121
122
123class format_cs_table_t:
124    def __init__( self, format_, default_cs, permitted_cs):
125        self.format = format_
126        self.default_cs = default_cs
127        self.permitted_cs = permitted_cs
128
129format_cs_table = [
130        format_cs_table_t( OUT_PNG, CS_RGB, [ CS_GRAY, CS_GRAY_ALPHA, CS_RGB, CS_RGB_ALPHA, CS_ICC ] ),
131        format_cs_table_t( OUT_PPM, CS_RGB, [ CS_GRAY, CS_RGB ] ),
132        format_cs_table_t( OUT_PNM, CS_GRAY, [ CS_GRAY, CS_RGB ] ),
133        format_cs_table_t( OUT_PAM, CS_RGB_ALPHA, [ CS_GRAY, CS_GRAY_ALPHA, CS_RGB, CS_RGB_ALPHA, CS_CMYK, CS_CMYK_ALPHA ] ),
134        format_cs_table_t( OUT_PGM, CS_GRAY, [ CS_GRAY, CS_RGB ] ),
135        format_cs_table_t( OUT_PBM, CS_MONO, [ CS_MONO ] ),
136        format_cs_table_t( OUT_PKM, CS_CMYK, [ CS_CMYK ] ),
137        format_cs_table_t( OUT_PWG, CS_RGB, [ CS_MONO, CS_GRAY, CS_RGB, CS_CMYK ] ),
138        format_cs_table_t( OUT_PCL, CS_MONO, [ CS_MONO, CS_RGB ] ),
139        format_cs_table_t( OUT_PCLM, CS_RGB, [ CS_RGB, CS_GRAY ] ),
140        format_cs_table_t( OUT_PS, CS_RGB, [ CS_GRAY, CS_RGB, CS_CMYK ] ),
141        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 ] ),
142
143        format_cs_table_t( OUT_TRACE, CS_RGB, [ CS_RGB ] ),
144        format_cs_table_t( OUT_XMLTEXT, CS_RGB, [ CS_RGB ] ),
145        format_cs_table_t( OUT_BBOX, CS_RGB, [ CS_RGB ] ),
146        format_cs_table_t( OUT_SVG, CS_RGB, [ CS_RGB ] ),
147
148        format_cs_table_t( OUT_TEXT, CS_RGB, [ CS_RGB ] ),
149        format_cs_table_t( OUT_HTML, CS_RGB, [ CS_RGB ] ),
150        format_cs_table_t( OUT_XHTML, CS_RGB, [ CS_RGB ] ),
151        format_cs_table_t( OUT_STEXT, CS_RGB, [ CS_RGB ] ),
152        ]
153
154def stat_mtime(path):
155    try:
156        return os.path.getmtime(path)
157    except Exception:
158        return 0
159
160
161class worker_t:
162    def __init__( self):
163        self.num = 0
164        self.band = 0
165        self.list = None
166        self.ctm = None
167        self.tbounds = None
168        self.pix = None
169        self.bit = None
170        self.cookie = mupdf.Cookie()
171
172class state:
173
174    output = None
175    out = None
176    output_pagenum = 0
177    output_file_per_page = 0
178
179    format_ = None
180    output_format = OUT_NONE
181
182    rotation = 0
183    resolution = 72
184    res_specified = 0
185    width = 0
186    height = 0
187    fit = 0
188
189    layout_w = mupdf.FZ_DEFAULT_LAYOUT_W
190    layout_h = mupdf.FZ_DEFAULT_LAYOUT_H
191    layout_em = mupdf.FZ_DEFAULT_LAYOUT_EM
192    layout_css = None
193    layout_use_doc_css = 1
194    min_line_width = 0.0
195
196    showfeatures = 0
197    showtime = 0
198    showmemory = 0
199    showmd5 = 0
200
201    no_icc = 0
202    ignore_errors = 0
203    uselist = 1
204    alphabits_text = 8
205    alphabits_graphics = 8
206
207    out_cs = CS_UNSET
208    proof_filename = None
209    proof_cs = mupdf.Colorspace()
210    icc_filename = None
211    gamma_value = 1
212    invert = 0
213    band_height = 0
214    lowmemory = 0
215
216    quiet = 0
217    errored = 0
218    colorspace = mupdf.Colorspace()
219    oi = None
220    spots = SPOTS_OVERPRINT_SIM
221    alpha = 0
222    useaccel = 1
223    filename = None
224    files = 0
225    num_workers = 0
226    workers = None
227    bander = None
228
229    layer_config = None
230
231
232class bgprint:
233    active = 0
234    started = 0
235    pagenum = 0
236    filename = None
237    list_ = None
238    page = None
239    interptime = 0
240    seps = None
241
242
243class timing:
244    count = 0
245    total = 0
246    min_ = 0
247    max_ = 0
248    mininterp = 0
249    maxinterp = 0
250    minpage = 0
251    maxpage = 0
252    minfilename = None
253    maxfilename = None
254    layout = 0
255    minlayout = 0
256    maxlayout = 0
257    minlayoutfilename = None
258    maxlayoutfilename = None
259
260
261
262def usage():
263    sys.stderr.write( f'''
264            mudraw version {mupdf.FZ_VERSION} "
265            Usage: mudraw [options] file [pages]
266            \t-p -\tpassword
267
268            \t-o -\toutput file name (%d for page number)
269            \t-F -\toutput format (default inferred from output file name)
270            \t\traster: png, pnm, pam, pbm, pkm, pwg, pcl, ps
271            \t\tvector: svg, pdf, trace
272            \t\ttext: txt, html, stext
273
274            \t-q\tbe quiet (don't print progress messages)
275            \t-s -\tshow extra information:
276            \t\tm - show memory use
277            \t\tt - show timings
278            \t\tf - show page features
279            \t\t5 - show md5 checksum of rendered image
280
281            \t-R -\trotate clockwise (default: 0 degrees)
282            \t-r -\tresolution in dpi (default: 72)
283            \t-w -\twidth (in pixels) (maximum width if -r is specified)
284            \t-h -\theight (in pixels) (maximum height if -r is specified)
285            \t-f -\tfit width and/or height exactly; ignore original aspect ratio
286            \t-B -\tmaximum band_height (pXm, pcl, pclm, ps, psd and png output only)
287            \t-T -\tnumber of threads to use for rendering (banded mode only)
288
289            \t-W -\tpage width for EPUB layout
290            \t-H -\tpage height for EPUB layout
291            \t-S -\tfont size for EPUB layout
292            \t-U -\tfile name of user stylesheet for EPUB layout
293            \t-X\tdisable document styles for EPUB layout
294            \t-a\tdisable usage of accelerator file
295
296            \t-c -\tcolorspace (mono, gray, grayalpha, rgb, rgba, cmyk, cmykalpha, filename of ICC profile)
297            \t-e -\tproof icc profile (filename of ICC profile)
298            \t-G -\tapply gamma correction
299            \t-I\tinvert colors
300
301            \t-A -\tnumber of bits of antialiasing (0 to 8)
302            \t-A -/-\tnumber of bits of antialiasing (0 to 8) (graphics, text)
303            \t-l -\tminimum stroked line width (in pixels)
304            \t-D\tdisable use of display list
305            \t-i\tignore errors
306            \t-L\tlow memory mode (avoid caching, clear objects after each page)
307            \t-P\tparallel interpretation/rendering
308            \t-N\tdisable ICC workflow (\"N\"o color management)
309            \t-O -\tControl spot/overprint rendering
310            \t\t 0 = No spot rendering
311            \t\t 1 = Overprint simulation (default)
312            \t\t 2 = Full spot rendering
313
314            \t-y l\tList the layer configs to stderr
315            \t-y -\tSelect layer config (by number)
316            \t-y -{{,-}}*\tSelect layer config (by number), and toggle the listed entries
317
318            \tpages\tcomma separated list of page numbers and ranges
319            ''')
320    sys.exit(1)
321
322
323gettime_first = None
324
325def gettime():
326    global gettime_first
327    if gettime_first is None:
328        gettime_first = time.time()
329
330    now = time.time()
331    return (now - gettime_first) * 1000
332
333
334def has_percent_d(s):
335    # find '%[0-9]*d' */
336    m = re.search( '%[0-9]*d', s)
337    if m:
338        return 1
339    return 0
340
341
342
343# Output file level (as opposed to page level) headers
344def file_level_headers():
345
346    if state.output_format in (OUT_STEXT, OUT_TRACE, OUT_XMLTEXT, OUT_BBOX):
347        state.out.write_string( "<?xml version=\"1.0\"?>\n")
348
349    if state.output_format == OUT_HTML:
350        state.out.print_stext_header_as_html()
351    if state.output_format == OUT_XHTML:
352        state.out.print_stext_header_as_xhtml()
353
354    if state.output_format in (OUT_STEXT, OUT_TRACE, OUT_BBOX):
355        state.out.write_string( f'<document name="{state.filename}">\n')
356
357    if state.output_format == OUT_PS:
358        state.out.write_ps_file_header()
359
360    if state.output_format == OUT_PWG:
361        state.out.write_pwg_file_header()
362
363    if state.output_format == OUT_PCLM:
364        opts = mupdf.PclmOptions( 'compression=flate')
365        state.bander = mupdf.BandWriter(state.out, opts)
366
367def file_level_trailers():
368    if state.output_format in (OUT_STEXT, OUT_TRACE, OUT_BBOX):
369        state.out.write_string( "</document>\n")
370
371    if state.output_format == OUT_HTML:
372        state.out.print_stext_trailer_as_html()
373    if state.output_format == OUT_XHTML:
374        state.out.print_stext_trailer_as_xhtml()
375
376    if state.output_format == OUT_PS:
377        state.out.write_ps_file_trailer( state.output_pagenum)
378
379def drawband( page, list_, ctm, tbounds, cookie, band_start, pix):
380
381    bit = None
382
383    if pix.alpha():
384        pix.clear_pixmap()
385    else:
386        pix.clear_pixmap_with_value( 255)
387
388    dev = mupdf.Device( mupdf.Matrix(), pix, state.proof_cs)
389    if state.lowmemory:
390        dev.enable_device_hints( mupdf.FZ_NO_CACHE)
391    if state.alphabits_graphics == 0:
392        dev.enable_device_hints( mupdf.FZ_DONT_INTERPOLATE_IMAGES)
393    if list_:
394        list_.run_display_list( dev, ctm, tbounds, cookie)
395    else:
396        page.run( dev, ctm, cookie)
397    dev.close_device()
398    dev = None
399
400    if state.invert:
401        pix.invert_pixmap()
402    if state.gamma_value != 1:
403        pix.gamma_pixmap( state.gamma_value)
404
405    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):
406        bit = mupdf.Bitmap( pix, mupdf.Halftone(), band_start)
407    return bit
408
409
410
411
412
413
414def dodrawpage( page, list_, pagenum, cookie, start, interptime, filename, bg, seps):
415
416    if state.output_file_per_page:
417        file_level_headers()
418
419    if list_:
420        mediabox = mupdf.Rect( list_)
421    else:
422        mediabox = page.bound_page()
423
424    if state.output_format == OUT_TRACE:
425        state.out.write_string( "<page mediabox=\"%g %g %g %g\">\n" % (
426                mediabox.x0, mediabox.y0, mediabox.x1, mediabox.y1))
427        dev = mupdf.Device( state.out)
428        if state.lowmemory:
429            dev.enable_device_hints( mupdf.FZ_NO_CACHE)
430        if list_:
431            list_.run_display_list( dev, mupdf.Matrix(), mupdf.Rect(mupdf.fz_infinite_rect), cookie)
432        else:
433            page.run( dev, fz_identity, cookie)
434        state.out.write_string( "</page>\n")
435        dev.close_device()
436        dev = None
437
438    elif state.output_format == OUT_XMLTEXT:
439        state.out.write_string( "<page mediabox=\"%g %g %g %g\">\n" % (
440                mediabox.x0, mediabox.y0, mediabox.x1, mediabox.y1))
441        dev = mupdf.Device.new_raw_device( state.out)
442        if list_:
443            list_.run_display_list( dev, mupdf.Matrix(), mupdf.Rect(mupdf.fz_infinite_rect), cookie)
444        else:
445            page.run( dev, fz_identity, cookie)
446        state.out.write_string( "</page>\n")
447        dev.close_device()
448        dev = None
449
450    elif state.output_format == OUT_BBOX:
451        bbox = mupdf.Rect( mupdf.Rect.Fixed_EMPTY)
452        dev = mupdf.Device( bbox)
453        if state.lowmemory:
454            dev.enable_device_hints( mupdf.FZ_NO_CACHE)
455        if list_:
456            list_.run_display_list( dev, fz_identity, mupdf.Rect(mupdf.fz_infinite_rect), cookie)
457        else:
458            page.run( dev, fz_identity, cookie)
459        dev.close_device()
460        state.out.write_string( "<page bbox=\"%s %s %s %s\" mediabox=\"%s %s %s %s\" />\n",
461                bbox.x0,
462                bbox.y0,
463                bbox.x1,
464                bbox.y1,
465                mediabox.x0,
466                mediabox.y0,
467                mediabox.x1,
468                mediabox.y1,
469                )
470
471    elif state.output_format in (OUT_TEXT, OUT_HTML, OUT_XHTML, OUT_STEXT):
472        zoom = state.resolution / 72
473        ctm = mupdf.Matrix(mupdf.pre_scale(mupdf.rotate(state.rotation), zoom, zoom))
474
475        stext_options = mupdf.StextOptions()
476
477        stext_options.flags = mupdf.FZ_STEXT_PRESERVE_IMAGES if (state.output_format == OUT_HTML or state.output_format == OUT_XHTML) else 0
478        text = mupdf.StextPage( mediabox)
479        dev = mupdf.Device( text, stext_options)
480        if state.lowmemory:
481            fz_enable_device_hints( dev, FZ_NO_CACHE)
482        if list_:
483            list_.run_display_list( dev, ctm, mupdf.Rect(mupdf.fz_infinite_rect), cookie)
484        else:
485            page.run( dev, ctm, cookie)
486        dev.close_device()
487        dev = None
488        if state.output_format == OUT_STEXT:
489            state.out.print_stext_page_as_xml( text, pagenum)
490        elif state.output_format == OUT_HTML:
491            state.out.print_stext_page_as_html( text, pagenum)
492        elif state.output_format == OUT_XHTML:
493            state.out.print_stext_page_as_xhtml( text, pagenum)
494        elif state.output_format == OUT_TEXT:
495            state.out.print_stext_page_as_text( text)
496            state.out.write_string( "\f\n")
497
498    elif state.output_format == OUT_SVG:
499        zoom = state.resolution / 72
500        ctm = mupdf.Matrix(zoom, zoom)
501        ctm.pre_rotate( state.rotation)
502        tbounds = mupdf.Rect(mediabox, ctm)
503
504        if not state.output or state.output == "-":
505            state.out = mupdf.Output( mupdf.Output.Fixed_STDOUT)
506        else:
507            buf = mupdf.format_output_path( state.output, pagenum)
508            state.out = mupdf.Output( buf, 0)
509
510        dev = mupdf.Device( state.out, tbounds.x1-tbounds.x0, tbounds.y1-tbounds.y0, mupdf.FZ_SVG_TEXT_AS_PATH, 1)
511        if state.lowmemory:
512            dev.enable_device_hints( dev, mupdf.FZ_NO_CACHE)
513        if list_:
514            list_.run_display_list( dev, ctm, tbounds, cookie)
515        else:
516            page.run( dev, ctm, cookie)
517        dev.close_device()
518        state.out.close_output()
519    else:
520        zoom = state.resolution / 72
521        m = mupdf.rotate(state.rotation)
522        ctm = mupdf.Matrix( mupdf.pre_scale( mupdf.rotate(state.rotation), zoom, zoom))
523        tbounds = mupdf.Rect(mediabox, ctm)
524        ibounds = tbounds.round_rect()
525
526        # Make local copies of our width/height
527        w = state.width
528        h = state.height
529
530        # If a resolution is specified, check to see whether w/h are
531        # exceeded; if not, unset them. */
532        if state.res_specified:
533            t = ibounds.x1 - ibounds.x0
534            if w and t <= w:
535                w = 0
536            t = ibounds.y1 - ibounds.y0
537            if h and t <= h:
538                h = 0
539
540        # Now w or h will be 0 unless they need to be enforced.
541        if w or h:
542            scalex = w / (tbounds.x1 - tbounds.x0)
543            scaley = h / (tbounds.y1 - tbounds.y0)
544
545            if state.fit:
546                if w == 0:
547                    scalex = 1.0
548                if h == 0:
549                    scaley = 1.0
550            else:
551                if w == 0:
552                    scalex = scaley
553                if h == 0:
554                    scaley = scalex
555            if not state.fit:
556                if scalex > scaley:
557                    scalex = scaley
558                else:
559                    scaley = scalex
560            scale_mat = mupdf.Matrix.scale(scalex, scaley)
561            ctm = mupdf.Matrix( mupdf.concat(ctm.internal(), scale_mat.internal()))
562            tbounds = mupdf.Rect( mediabox, ctm)
563        ibounds = tbounds.round_rect()
564        tbounds = ibounds.rect_from_irect()
565
566        band_ibounds = ibounds
567        bands = 1
568        totalheight = ibounds.y1 - ibounds.y0
569        drawheight = totalheight
570
571        if state.band_height != 0:
572            # Banded rendering; we'll only render to a
573            # given height at a time.
574            drawheight = state.band_height
575            if totalheight > state.band_height:
576                band_ibounds.y1 = band_ibounds.y0 + state.band_height
577            bands = (totalheight + state.band_height-1)/state.band_height
578            tbounds.y1 = tbounds.y0 + state.band_height + 2
579            #DEBUG_THREADS(("Using %d Bands\n", bands));
580
581        if state.num_workers > 0:
582            for band in range( min(state.num_workers, bands)):
583                state.workers[band].band = band
584                state.workers[band].ctm = ctm
585                state.workers[band].tbounds = tbounds
586                state.workers[band].cookie = mupdf.Cookie()
587                state.workers[band].list = list_
588                state.workers[band].pix = mupdf.Pixmap( state.colorspace, band_ibounds, seps, state.alpha)
589                state.workers[band].pix.set_pixmap_resolution( state.resolution, state.resolution)
590                ctm.f -= drawheight
591            pix = state.workers[0].pix
592        else:
593            pix = mupdf.Pixmap( state.colorspace, band_ibounds, seps, state.alpha)
594            pix.set_pixmap_resolution( int(state.resolution), int(state.resolution))
595
596        # Output any page level headers (for banded formats)
597        if state.output:
598            state.bander = None
599            if state.output_format == OUT_PGM or state.output_format == OUT_PPM or state.output_format == OUT_PNM:
600                state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.PNM)
601            elif state.output_format == OUT_PAM:
602                state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.PAM)
603            elif state.output_format == OUT_PNG:
604                state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.PNG)
605            elif state.output_format == OUT_PBM:
606                state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.PBM)
607            elif state.output_format == OUT_PKM:
608                state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.PKM)
609            elif state.output_format == OUT_PS:
610                state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.PS)
611            elif state.output_format == OUT_PSD:
612                state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.PSD)
613            elif state.output_format == OUT_PWG:
614                if state.out_cs == CS_MONO:
615                    state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.MONO, mupdf.PwgOptions())
616                else:
617                    state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.COLOR, mupdf.PwgOptions())
618            elif state.output_format == OUT_PCL:
619                if state.out_cs == CS_MONO:
620                    state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.MONO, mupdf.PclOptions())
621                else:
622                    state.bander = mupdf.BandWriter( state.out, mupdf.BandWriter.COLOR, mupdf.PclOptions())
623            if state.bander:
624                state.bander.write_header( pix.w(), totalheight, pix.n(), pix.alpha(), pix.xres(), pix.yres(), state.output_pagenum, pix.colorspace(), pix.seps())
625                state.output_pagenum += 1
626
627        for band in range( bands):
628            if state.num_workers > 0:
629                w = state.workers[band % state.num_workers]
630                pix = w.pix
631                bit = w.bit
632                w.bit = None
633                cookie.increment_errors(w.cookie.errors())
634
635            else:
636                bit = drawband( page, list_, ctm, tbounds, cookie, band * state.band_height, pix)
637
638            if state.output:
639                if state.bander:
640                    if bit:
641                        state.bander.write_band( bit.stride(), drawheight, bit.samples())
642                    else:
643                        state.bander.write_band( pix.stride(), drawheight, pix.samples())
644                bit = None
645
646            if state.num_workers > 0 and band + state.num_workers < bands:
647                w = state.workers[band % state.num_workers]
648                w.band = band + state.num_workers
649                w.ctm = ctm
650                w.tbounds = tbounds
651                w.cookie = mupdf.Cookie()
652            ctm.f -= drawheight
653
654        # FIXME
655        if state.showmd5:
656            digest = pix.md5_pixmap()
657            sys.stderr.write( ' ')
658            for i in range(16):
659                sys.stderr.write( '%02x', digest[i])
660
661    if state.output_file_per_page:
662        file_level_trailers()
663
664    if state.showtime:
665        end = gettime()
666        diff = end - start
667
668        if bg:
669            if diff + interptime < timing.min:
670                timing.min = diff + interptime
671                timing.mininterp = interptime
672                timing.minpage = pagenum
673                timing.minfilename = filename
674            if diff + interptime > timing.max:
675                timing.max = diff + interptime
676                timing.maxinterp = interptime
677                timing.maxpage = pagenum
678                timing.maxfilename = filename
679            timing.count += 1
680
681            sys.stderr.write( " %dms (interpretation) %dms (rendering) %dms (total)" % (interptime, diff, diff + interptime))
682        else:
683            if diff < timing.min:
684                timing.min = diff
685                timing.minpage = pagenum
686                timing.minfilename = filename
687            if diff > timing.max:
688                timing.max = diff
689                timing.maxpage = pagenum
690                timing.maxfilename = filename
691
692            timing.total += diff
693            timing.count += 1
694
695            sys.stderr.write( " %dms" % diff)
696
697    if not state.quiet or state.showfeatures or state.showtime or state.showmd5:
698        sys.stderr.write( "\n")
699
700    if state.lowmemory:
701        mupdf.empty_store()
702
703    if state.showmemory:
704        mupdf.dump_glyph_cache_stats(mupdf.stderr_())
705
706    mupdf.flush_warnings()
707
708    if cookie.get_errors():
709        state.errored = 1
710
711
712
713def bgprint_flush():
714    if not bgprint.active or not bgprint.started:
715        return
716    bgprint.started = 0
717
718
719
720def drawpage( doc, pagenum):
721    list_ = None
722    cookie = mupdf.Cookie()
723    seps = None
724    features = ""
725
726    start = gettime() if state.showtime else 0
727
728    page = mupdf.Page( doc, pagenum - 1)
729
730    if state.spots != SPOTS_NONE:
731        seps = page.page_separations()
732        if seps.m_internal:
733            n = seps.count_separations()
734            if state.spots == SPOTS_FULL:
735                for i in range(n):
736                    seps.set_separation_behavior( i, mupdf.FZ_SEPARATION_SPOT)
737            else:
738                for i in range(n):
739                    seps.set_separation_behavior( i, mupdf.FZ_SEPARATION_COMPOSITE)
740        elif page.page_uses_overprint():
741            # This page uses overprint, so we need an empty
742            # sep object to force the overprint simulation on.
743            seps = mupdf.Separations(0)
744        elif state.oi and state.oi.m_internal and state.oi.colorspace_n() != state.colorspace.colorspace_n():
745            # We have an output intent, and it's incompatible
746            # with the colorspace our device needs. Force the
747            # overprint simulation on, because this ensures that
748            # we 'simulate' the output intent too. */
749            seps = mupdf.Separations(0)
750
751    if state.uselist:
752        list_ = mupdf.DisplayList( page.bound_page())
753        dev = mupdf.Device( list_)
754        if state.lowmemory:
755            dev.enable_device_hints( FZ_NO_CACHE)
756        page.run( dev, mupdf.Matrix(), cookie)
757        dev.close_device()
758
759        if bgprint.active and state.showtime:
760            end = gettime()
761            start = end - start
762
763    if state.showfeatures:
764        # SWIG doesn't appear to handle the out-param is_color in
765        # mupdf.Device() constructor that wraps fz_new_test_device(), so we use
766        # the underlying mupdf function() instead.
767        #
768        dev, iscolor = mupdf.new_test_device( 0.02, 0, None)
769        dev = mupdf.Device( dev)
770        if state.lowmemory:
771            dev.enable_device_hints( mupdf.FZ_NO_CACHE)
772        if list_:
773            list_.run_display_list( dev, mupdf.Matrix(mupdf.fz_identity), mupdf.Rect(mupdf.fz_infinite_rect), mupdf.Cookie())
774        else:
775            page.run( dev, fz_identity, cookie)
776        dev.close_device()
777        features = " color" if iscolor else " grayscale"
778
779    if state.output_file_per_page:
780        bgprint_flush()
781        if state.out:
782            state.out.close_output()
783        text_buffer = mupdf.format_output_path( state.output, pagenum)
784        state.out = mupdf.Output( text_buffer, 0)
785
786    if bgprint.active:
787        bgprint_flush()
788        if bgprint.active:
789            if not state.quiet or state.showfeatures or state.showtime or state.showmd5:
790                sys.stderr.write( "page %s %d%s" % (state.filename, pagenum, features))
791
792        bgprint.started = 1
793        bgprint.page = page
794        bgprint.list = list_
795        bgprint.seps = seps
796        bgprint.filename = state.filename
797        bgprint.pagenum = pagenum
798        bgprint.interptime = start
799    else:
800        if not state.quiet or state.showfeatures or state.showtime or state.showmd5:
801            sys.stderr.write( "page %s %d%s" % (state.filename, pagenum, features))
802        dodrawpage( page, list_, pagenum, cookie, start, 0, state.filename, 0, seps)
803
804
805
806
807
808
809
810
811
812
813
814def drawrange( doc, range_):
815    pagecount = doc.count_pages()
816
817    while 1:
818        range_, spage, epage = mupdf.parse_page_range( range_, pagecount)
819        if range_ is None:
820            break
821        if spage < epage:
822            for page in range(spage, epage+1):
823                drawpage( doc, page)
824        else:
825            for page in range( spage, epage-1, -1):
826                drawpage( doc, page)
827
828
829
830def parse_colorspace( name):
831    ret = cs_name_table.get( name)
832    if ret:
833        return ret
834    state.icc_filename = name
835    return CS_ICC
836
837
838class trace_info:
839    def __init__( self):
840        self.current = 0
841        self.peak = 0
842        self.total = 0
843
844
845def iswhite(ch):
846    return (
847        ch == '\011' or ch == '\012' or
848        ch == '\014' or ch == '\015' or ch == '\040'
849        )
850
851def apply_layer_config( doc, lc):
852    pass
853
854
855
856
857def convert_to_accel_path(absname):
858    tmpdir = os.getenv('TEMP')
859    if  not tmpdir:
860        tmpdir = os.getenv('TMP')
861    if not tmpdir:
862        tmpdir = '/var/tmp'
863    if not os.path.isdir(tmpdir):
864        tmpdir = '/tmp'
865
866    if absname.startswith( '/') or absname.startswith( '\\'):
867        absname = absname[1:]
868
869    absname = absname.replace( '/', '%')
870    absname = absname.replace( '\\', '%')
871    absname = absname.replace( ':', '%')
872
873    return '%s/%s.accel' % (tmpdir, absname)
874
875def get_accelerator_filename( filename):
876    absname = os.path.realpath( filename)
877    return convert_to_accel_path( absname)
878
879def save_accelerator(doc, filename):
880    if not doc.document_supports_accelerator():
881        return
882    absname = get_accelerator_filename( filename)
883    doc.save_accelerator( absname)
884
885
886def draw( argv):
887
888    password = ''
889    info = trace_info()
890
891    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')
892    for option, value in items:
893        if 0:   pass
894        elif option == '-q':    state.quiet = 1
895        elif option == '-p':    password = value
896        elif option == '-o':    state.output = value
897        elif option == '-F':    state.format_ = value
898        elif option == '-R':    state.rotation = float( value)
899        elif option == '-r':
900            state.resolution = float( value)
901            state.res_specified = 1
902        elif option == '-w':    state.width = float( value)
903        elif option == '-h':    state.height = float( value)
904        elif option == '-f':    state.fit = 1
905        elif option == '-B':    state.band_height = int( value)
906        elif option == '-c':    state.out_cs = parse_colorspace( value)
907        elif option == '-e':    state.proof_filename = value
908        elif option == '-G':    state.gamma_value = float( value)
909        elif option == '-I':    state.invert += 1
910        elif option == '-W':    state.layout_w = float( value)
911        elif option == '-H':    state.layout_h = float( value)
912        elif option == '-S':    state.layout_em = float( value)
913        elif option == '-U':    state.layout_css = value
914        elif option == '-X':    state.layout_use_doc_css = 0
915        elif option == '-O':
916            state.spots = float( value)
917            if not mupdf.FZ_ENABLE_SPOT_RENDERING:
918                sys.stderr.write( 'Spot rendering/Overprint/Overprint simulation not enabled in this build\n')
919                state.spots = SPOTS_NONE
920        elif option == '-s':
921            if 't' in value: state.showtime += 1
922            if 'm' in value: state.showmemory += 1
923            if 'f' in value: state.showfeatures += 1
924            if '5' in value: state.showmd5 += 1
925
926        elif option == '-A':
927            state.alphabits_graphics = int(value)
928            sep = value.find( '/')
929            if sep >= 0:
930                state.alphabits_text = int(value[sep+1:])
931            else:
932                state.alphabits_text = state.alphabits_graphics
933        elif option == '-D': state.uselist = 0
934        elif option == '-l': state.min_line_width = float(value)
935        elif option == '-i': state.ignore_errors = 1
936        elif option == '-N': state.no_icc = 1
937
938        elif option == '-T': state.num_workers = int(value)
939        elif option == '-L': state.lowmemory = 1
940        elif option == '-P': bgprint.active = 1
941        elif option == '-y': state.layer_config = value
942        elif option == '-a': state.useaccel = 0
943
944        elif option == '-v': sys.stderr.write( f'mudraw version {mupdf.FZ_VERSION}\n')
945
946    if not argv:
947        usage()
948
949    if state.num_workers > 0:
950        if state.uselist == 0:
951            sys.stderr.write('cannot use multiple threads without using display list\n')
952            sys.exit(1)
953
954        if state.band_height == 0:
955            sys.stderr.write('Using multiple threads without banding is pointless\n')
956
957    if bgprint.active:
958        if state.uselist == 0:
959            sys.stderr.write('cannot bgprint without using display list\n')
960            sys.exit(1)
961
962    if state.proof_filename:
963        proof_buffer = mupdf.Buffer( state.proof_filename)
964        state.proof_cs = mupdf.Colorspace( FZ_COLORSPACE_NONE, 0, None, proof_buffer)
965
966    mupdf.set_text_aa_level( state.alphabits_text)
967    mupdf.set_graphics_aa_level( state.alphabits_graphics)
968    mupdf.set_graphics_min_line_width( state.min_line_width)
969    if state.no_icc:
970        mupdf.disable_icc()
971    else:
972        mupdf.enable_icc()
973
974    if state.layout_css:
975        buf = mupdf.Buffer( state.layout_css)
976        mupdf.set_user_css( buf.string_from_buffer())
977
978    mupdf.set_use_document_css( state.layout_use_doc_css)
979
980    # Determine output type
981    if state.band_height < 0:
982        sys.stderr.write( 'Bandheight must be > 0\n')
983        sys.exit(1)
984
985    state.output_format = OUT_PNG
986    if state.format_:
987        for i in range(len(suffix_table)):
988            if state.format_ == suffix_table[i].suffix[1:]:
989                state.output_format = suffix_table[i].format
990                if state.spots == SPOTS_FULL and suffix_table[i].spots == 0:
991                    sys.stderr.write( f'Output format {suffix_table[i].suffix[1:]} does not support spot rendering.\nDoing overprint simulation instead.\n')
992                    state.spots = SPOTS_OVERPRINT_SIM
993                break
994        else:
995            sys.stderr.write( f'Unknown output format {format}\n')
996            sys.exit(1)
997    elif state.output:
998        suffix = state.output
999        i = 0
1000        while 1:
1001            if i == len(suffix_table):
1002                break
1003            s = suffix.find( suffix_table[i].suffix)
1004            if s != -1:
1005                suffix = suffix_table[i].suffix[s+1:]
1006                state.output_format = suffix_table[i].format
1007                if state.spots == SPOTS_FULL and suffix_table[i].spots == 0:
1008                    sys.stderr.write( 'Output format {suffix_table[i].suffix[1:]} does not support spot rendering\nDoing overprint simulation instead.\n')
1009                    state.spots = SPOTS_OVERPRINT_SIM
1010                i = 0
1011            else:
1012                i += 1
1013
1014    if state.band_height:
1015        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):
1016            sys.stderr.write( 'Banded operation only possible with PxM, PCL, PCLM, PS, PSD, and PNG outputs\n')
1017            sys.exit(1)
1018        if state.showmd5:
1019            sys.stderr.write( 'Banded operation not compatible with MD5\n')
1020            sys.exit(1)
1021
1022    for i in range(len(format_cs_table)):
1023        if format_cs_table[i].format == state.output_format:
1024            if state.out_cs == CS_UNSET:
1025                state.out_cs = format_cs_table[i].default_cs
1026            for j in range( len(format_cs_table[i].permitted_cs)):
1027                if format_cs_table[i].permitted_cs[j] == state.out_cs:
1028                    break
1029            else:
1030                sys.stderr.write( 'Unsupported colorspace for this format\n')
1031                sys.exit(1)
1032
1033    state.alpha = 1
1034    if state.out_cs in ( CS_MONO, CS_GRAY, CS_GRAY_ALPHA):
1035        state.colorspace = mupdf.Colorspace( mupdf.Colorspace.Fixed_GRAY)
1036        state.alpha = (state.out_cs == CS_GRAY_ALPHA)
1037    elif state.out_cs in ( CS_RGB, CS_RGB_ALPHA):
1038        state.colorspace = mupdf.Colorspace( mupdf.Colorspace.Fixed_RGB)
1039        state.alpha = (state.out_cs == CS_RGB_ALPHA)
1040    elif state.out_cs in ( CS_CMYK, CS_CMYK_ALPHA):
1041        state.colorspace = mupdf.Colorspace( mupdf.Colorspace.Fixed_CMYK)
1042        state.alpha = (state.out_cs == CS_CMYK_ALPHA)
1043    elif state.out_cs == CS_ICC:
1044        try:
1045            icc_buffer = mupdf.Buffer( state.icc_filename)
1046            state.colorspace = Colorspace( mupdf.FZ_COLORSPACE_NONE, 0, None, icc_buffer)
1047        except Exception as e:
1048            sys.stderr.write( 'Invalid ICC destination color space\n')
1049            sys.exit(1)
1050        if state.colorspace.m_internal is None:
1051            sys.stderr.write( 'Invalid ICC destination color space\n')
1052            sys.exit(1)
1053        state.alpha = 0
1054    else:
1055        sys.stderr.write( 'Unknown colorspace!\n')
1056        sys.exit(1)
1057
1058    if state.out_cs != CS_ICC:
1059        state.colorspace = mupdf.Colorspace( state.colorspace)
1060    else:
1061        # Check to make sure this icc profile is ok with the output format */
1062        okay = 0
1063        for i in range( len(format_cs_table)):
1064            if format_cs_table[i].format == state.output_format:
1065                for j in range( len(format_cs_table[i].permitted_cs)):
1066                    x = format_cs_table[i].permitted_cs[j]
1067                    if x in ( CS_MONO, CS_GRAY, CS_GRAY_ALPHA):
1068                        if state.colorspace.colorspace_is_gray():
1069                            okay = 1
1070                        elif x in ( CS_RGB, CS_RGB_ALPHA):
1071                            if state.colorspace.colorspace_is_rgb():
1072                                okay = 1
1073                        elif x in ( CS_CMYK, CS_CMYK_ALPHA):
1074                            if state.colorspace.colorspace_is_cmyk():
1075                                okay = 1
1076
1077        if not okay:
1078            sys.stderr.write( 'ICC profile uses a colorspace that cannot be used for this format\n')
1079            sys.exit(1)
1080
1081    if state.output_format == OUT_SVG:
1082        # SVG files are always opened for each page. Do not open "output".
1083        pass
1084    elif state.output and (not state.output.startswith('-') or len(state.output) >= 2) and len(state.output) >= 1:
1085        if has_percent_d(state.output):
1086            state.output_file_per_page = 1
1087        else:
1088            state.out = mupdf.Output(state.output, 0)
1089    else:
1090        state.quiet = 1 # automatically be quiet if printing to stdout
1091        if 0:
1092            # Windows specific code to make stdout binary.
1093            if state.output_format not in( OUT_TEXT, OUT_STEXT, OUT_HTML, OUT_XHTML, OUT_TRACE, OUT_XMLTEXT):
1094                setmode(fileno(stdout), O_BINARY)
1095        state.out = mupdf.Output( mupdf.Output.Fixed_STDOUT)
1096
1097    state.filename = argv[0]
1098    if not state.output_file_per_page:
1099        file_level_headers()
1100
1101    timing.count = 0
1102    timing.total = 0
1103    timing.min = 1 << 30
1104    timing.max = 0
1105    timing.mininterp = 1 << 30
1106    timing.maxinterp = 0
1107    timing.minpage = 0
1108    timing.maxpage = 0
1109    timing.minfilename = ""
1110    timing.maxfilename = ""
1111    timing.layout = 0
1112    timing.minlayout = 1 << 30
1113    timing.maxlayout = 0
1114    timing.minlayoutfilename = ""
1115    timing.maxlayoutfilename = ""
1116
1117    if state.showtime and bgprint.active:
1118        timing.total = gettime()
1119
1120    fz_optind = 0
1121    try:
1122        while fz_optind < len( argv):
1123
1124            try:
1125                accel = None
1126
1127                state.filename = argv[fz_optind]
1128                fz_optind += 1
1129
1130                state.files += 1
1131
1132                if not state.useaccel:
1133                    accel = None
1134                # If there was an accelerator to load, what would it be called?
1135                else:
1136                    accelpath = get_accelerator_filename( state.filename)
1137                    # Check whether that file exists, and isn't older than
1138                    # the document.
1139                    atime = stat_mtime( accelpath)
1140                    dtime = stat_mtime( state.filename)
1141                    if atime == 0:
1142                        # No accelerator
1143                        pass
1144                    elif atime > dtime:
1145                        accel = accelpath
1146                    else:
1147                        # Accelerator data is out of date
1148                        os.unlink( accelpath)
1149                        accel = None # In case we have jumped up from below
1150
1151                # Unfortunately if accel=None, SWIG doesn't seem to think of it
1152                # as a char*, so we end up in fz_open_document_with_stream().
1153                #
1154                # If we try to avoid this by setting accel='', SWIG correctly
1155                # calls Document(const char *filename, const char *accel) =>
1156                # fz_open_accelerated_document(), but the latter function tests
1157                # for NULL not "" so fails.
1158                #
1159                # So we choose the constructor explicitly rather than leaving
1160                # it up to SWIG.
1161                #
1162                if accel:
1163                    doc = mupdf.Document(state.filename, accel)
1164                else:
1165                    doc = mupdf.Document(state.filename)
1166
1167                if doc.needs_password():
1168                    if not doc.authenticate_password( password):
1169                        raise Exception( f'cannot authenticate password: {state.filename}')
1170
1171                # Once document is open check for output intent colorspace
1172                state.oi = doc.document_output_intent()
1173                if state.oi.m_internal:
1174                    # See if we had explicitly set a profile to render
1175                    if state.out_cs != CS_ICC:
1176                        # In this case, we want to render to the output intent
1177                        # color space if the number of channels is the same
1178                        if state.oi.colorspace_n() == state.colorspace.colorspace_n():
1179                            state.colorspace = state.oi
1180
1181                layouttime = time.time()
1182                doc.layout_document( state.layout_w, state.layout_h, state.layout_em)
1183                doc.count_pages()
1184                layouttime = time.time() - layouttime
1185
1186                timing.layout += layouttime
1187                if layouttime < timing.minlayout:
1188                    timing.minlayout = layouttime
1189                    timing.minlayoutfilename = state.filename
1190                if layouttime > timing.maxlayout:
1191                    timing.maxlayout = layouttime
1192                    timing.maxlayoutfilename = state.filename
1193
1194                if state.layer_config:
1195                    apply_layer_config( doc, state.layer_config)
1196
1197                if fz_optind == len(argv) or not mupdf.is_page_range( argv[fz_optind]):
1198                    drawrange( doc, "1-N")
1199                if fz_optind < len( argv) and mupdf.is_page_range( argv[fz_optind]):
1200                    drawrange( doc, argv[fz_optind])
1201                    fz_optind += 1
1202
1203                bgprint_flush()
1204
1205                if state.useaccel:
1206                    save_accelerator( doc, state.filename)
1207            except Exception as e:
1208                if not state.ignore_errors:
1209                    raise
1210                bgprint_flush()
1211                sys.stderr.write( f'ignoring error in {state.filename}\n')
1212
1213    except Exception as e:
1214        bgprint_flush()
1215        sys.stderr.write( f'error: cannot draw \'{state.filename}\' because: {e}\n')
1216        state.errored = 1
1217
1218    if not state.output_file_per_page:
1219        file_level_trailers()
1220
1221    if state.out:
1222        state.out.close_output()
1223    state.out = None
1224
1225    if state.showtime and timing.count > 0:
1226        if bgprint.active:
1227            timing.total = gettime() - timing.total
1228
1229        if state.files == 1:
1230            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')
1231            if bgprint.active:
1232                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')
1233                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')
1234            else:
1235                sys.stderr.write( f'fastest page {timing.minpage}: {timing.min:.0f}ms\n')
1236                sys.stderr.write( f'slowest page {timing.maxpage}: {timing.max:.0f}ms\n')
1237        else:
1238            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')
1239            sys.stderr.write( f'fastest layout: {timing.minlayout:.0f}ms ({timing.minlayoutfilename})\n')
1240            sys.stderr.write( f'slowest layout: {timing.maxlayout:.0f}ms ({timing.maxlayoutfilename})\n')
1241            sys.stderr.write( f'fastest page {timing.minpage}: {timing.min:.0f}ms ({timing.minfilename})\n')
1242            sys.stderr.write( f'slowest page {timing.maxpage}: {timing.max:.0f}ms ({timing.maxfilename})\n')
1243
1244    if state.showmemory:
1245        sys.stderr.write( f'Memory use total={info.total} peak={info.peak} current={info.current}\n')
1246
1247    return state.errored != 0
1248