#!/usr/bin/env python
# -*- mode: python -*-
#
# Copyright (c) 2009 Jaroslav Gresula
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__version__ = "0.2-beta"
__author__ = 'jgresula@jagpdf.org (Jaroslav Gresula)'
__doc__ = "Combines plain text and images into a single PDF."

import sys
try:
    import jagpdf
except ImportError:
    sys.stderr.write("""
This application requires the JagPDF library. It seems that it is not installed
on your system. The application cannot continue.
""")
    sys.exit(1)
from optparse import OptionParser, OptionValueError, OptionGroup, SUPPRESS_HELP
import re
import os
from urllib2 import urlopen, HTTPError, URLError
from subprocess import Popen, PIPE
from cStringIO import StringIO

class Bunch:
    def __init__(self, **kwds):
        self.__dict__.update(kwds)


class Error(Exception):        
    def __init__(self, msg):
        self.msg = msg


class PDFStream(jagpdf.StreamOut):
    def __init__(self, opts):
        if opts.outfile == '-':
            self.out = sys.stdout
            try:
                # set stdout and stdin to binary mode on windows
                import msvcrt
                msvcrt.setmode(1, os.O_BINARY)
                msvcrt.setmode(0, os.O_BINARY)
            except ImportError:
                pass
        else:
            self.out = open(opts.outfile, 'wb')
        jagpdf.StreamOut.__init__(self)
        
    def write(self, data):
        self.out.write(data)

    def close(self):
        self.out.close()



# ======================================================================
#                       Formatting

def enough_space_on_page(ctx, y):
    return ctx.y - y >= ctx.opts.margins.bottom()


def place_bookmark(ctx):
    if ctx.opts.bookmark:
        if ctx.opts.separator == PageBreak:
            y = ctx.opts.page[1]
        else:
            y = ctx.y
        spec = 'mode=XYZ; top=%.02f' % y
        expanded = subst_template(ctx, ctx.opts.bookmark)
        ctx.doc.outline().item(expanded, spec)

def subst_template(ctx, text):
    s = {'%basename' : os.path.basename(ctx.input_name),
         '%path' : ctx.input_name,
         '%page' : str(ctx.paging.page_nr + 1),
         '%filestem' : os.path.basename(os.path.splitext(ctx.input_name)[0])}
    def replace(match):
        return s[match.group(0)]
    rex_str = "|".join([k for k in s.iterkeys()])
    return re.compile(rex_str).sub(replace, text)

def insert_input_separator(ctx):
    if ctx.opts.separator == PageBreak:
        ctx.paging.next_page()
    elif ctx.opts.separator > 0.0:
        if enough_space_on_page(ctx, ctx.opts.separator):
            ctx.y -= ctx.opts.separator
        else:
            ctx.paging.next_page()
    else:
        ctx.y = min(ctx.y - ctx.opts.separator,
                    ctx.opts.page[1] - ctx.opts.margins.top())


#----------------------------------------------------------------------
#                         Images

def place_inline_image(ctx, img, w, h):
    if ctx.opts.image_fit_wider and w > ctx.opts.margins.canvas_width():
        fit_wider_image(ctx, img, w, h)
        return
    if not enough_space_on_page(ctx, h):
        ctx.paging.next_page()
    canvas = ctx.doc.page().canvas()
    if ctx.opts.image_align == 'left':
        offset = 0
    elif ctx.opts.image_align == 'center':
        offset = (ctx.opts.margins.canvas_width() - w) / 2.0
    elif ctx.opts.image_align == 'right':
        offset = ctx.opts.margins.canvas_width() - w
    place_bookmark(ctx)
    ctx.y -= h    
    canvas.image(img, ctx.opts.margins.left() + offset, ctx.y)

def fit_wider_image(ctx, img, w, h):
    scale = ctx.opts.margins.canvas_width() / w
    if not enough_space_on_page(ctx, scale*h):
        ctx.paging.next_page()
    canvas = ctx.doc.page().canvas()
    canvas.state_save()
    place_bookmark(ctx)
    ctx.y -= scale * h
    canvas.translate(ctx.opts.margins.left(), ctx.y)
    canvas.scale(scale, scale)
    canvas.image(img, 0, 0)
    canvas.state_restore()

def paint_image(ctx, stream):
    img = load_image(ctx, stream)
    w = img.width() / img.dpi_x() * 72
    h = img.height() / img.dpi_y() * 72
    place_inline_image(ctx, img, w, h)

def load_image_imagemagick(ctx, img_data):
    try:
        convert = Popen(["convert", "-", "png:-"], stdin=PIPE, stdout=PIPE)
        out, err = convert.communicate(img_data)
        if convert.returncode != 0:
            return None
        return load_image_to_document(ctx, out)
    except OSError:
        return None # imagemagick not found

def load_image_pil(ctx, img_data):
    try:
        from PIL import Image
        pil_img = Image.open(StringIO(img_data))
        png = StringIO()
        pil_img.save(png, "PNG")
        return load_image_to_document(ctx, png.getvalue())
    except ImportError, exc:
        return None

def load_image_to_document(ctx, img_data):
    imgdef = ctx.doc.image_definition()
    if ctx.opts.image_dpi:
        imgdef.dpi(ctx.opts.image_dpi, ctx.opts.image_dpi)
    imgdef.data(img_data)
    return ctx.doc.image_load(imgdef)
    

def load_image(ctx, stream):
    key = (ctx.input_name,ctx.opts.image_dpi)
    if key in ctx.img_cache:
       return ctx.img_cache[key]
    img_data = stream.read()
    try:
        img = load_image_to_document(ctx, img_data)
    except jagpdf.Exception:
        # the input stream contains unsupported image format, try to convert it
        if ctx.opts.force_pil:
            methods = [load_image_pil]
        elif ctx.opts.force_imagemagick:
            methods = [load_image_imagemagick]
        else:
            methods = [load_image_imagemagick, load_image_pil]
        for load_method in methods:
            img = load_method(ctx, img_data)
            if img:
                break
    if not img:
        raise Error("Unsupported image format.")
    ctx.img_cache[key] = img
    return img


# ----------------------------------------------------------------------
#                           Text

re_file_enc = re.compile('-\*- .*coding:\s*([^ ;]+).*-\*-', re.M | re.S)
class CannotHighlight: pass

def show_text(ctx, stream):
    if ctx.opts.highlight:
        try:
            return highlight_text(ctx, stream)
        except CannotHighlight:
            pass
    iter_lines(ctx, stream)

def highlight_text(ctx, stream):
    try:
        import pygments
        from pygments import lex
        from pygments.lexers import get_lexer_for_filename, get_lexer_by_name
        from pygments.styles import get_style_by_name
        if ctx.input_name.endswith('.h'):
            lexer = get_lexer_by_name('cpp')
        else:
            lexer = get_lexer_for_filename(ctx.input_name)
        stream_data = stream.read()
        m = re_file_enc.search(stream_data, 0, 160)
        if m:
            lexer.encoding = m.group(1)
        else:
            lexer.encoding = ctx.opts.encoding
        lexer.tabsize = ctx.opts.tabsize
        token_iter = lex(stream_data, lexer)
        style = get_style_by_name('default')
        driver = TxtDriver(ctx, style)
        for token in token_iter:
            driver.on_text(token[1], token[0])
    except ImportError:
        raise Error("Pygments not installed, cannot highlight")
    except pygments.util.ClassNotFound, exc:
        raise CannotHighlight()


class CodecStreamWrap:
    def __init__(self, ctx, stream):
        self.stream = stream
        self.line_nr = 0
        if ctx.opts.encoding != 'utf-8':
            self.install_codec(ctx.opts.encoding)
        else:
            self.encoder = lambda x: x
            self.decoder = lambda x: x

    def readline(self):
        line = self.stream.readline()
        if self.line_nr < 2:
            m = re_file_enc.search(line)
            if m:
                self.install_codec(m.group(1))
                self.line_nr = 2
            else:
                self.line_nr += 1
        return self.encoder(self.decoder(line)[0])[0]
            
    def install_codec(self, enc):
        import codecs
        self.encoder = codecs.getencoder('utf-8')
        self.decoder = codecs.getdecoder(enc)

    
def iter_lines(ctx, stream):
    cstream = CodecStreamWrap(ctx, stream)
    driver = TxtDriver(ctx)
    expand_tabs = ctx.opts.tabsize * ' '
    line = cstream.readline()
    while line:
        line.replace('\t', expand_tabs)
        driver.on_text(line)
        line = cstream.readline()
    
rex_line_split = re.compile("(\r?\n)|(\f)")
class TxtDriver:
    """single text input"""
    def __init__(self, ctx, style=None):
        self.ctx = ctx
        self.line_nr = 0
        self.first_time = True
        self.canvas = None
        self.style = style
        self.style_to_color = {}

    def line_height(self):
        linespacing = 1.0 + self.ctx.opts.linespacing
        return self.ctx.font.height() * linespacing

    def on_line_end(self):
        if self.canvas:
            self.canvas.text_end()
        else:
            self.paint_zebra(self.ctx.y)
        self.line_nr += 1
        self.canvas = None
        self.ctx.y -= self.line_height()

    def process_token(self, token):
        try:
            color = self.style_to_color[token]
        except KeyError:
            color = self.style.style_for_token(token)['color']
            if color:
                color = parse_color(color)
            else:
                color = self.ctx.opts.font_color
            self.style_to_color[token] = color
        self.canvas.color("f", *color)

    def on_text(self, text, token=None):
        m = rex_line_split.search(text)
        if not m:
            self.on_span(text, token)
        else:
            search = 0
            while m:
                self.on_span(text[search:m.start()], token)
                if m.group(1):
                    self.on_line_end()
                else:
                    assert m.group(2)
                    self.on_next_page()
                search = m.end()
                m = rex_line_split.search(text, search)
            if search < len(text):
                self.on_span(text[search:], token) 
        
    def on_span(self, text, token=None):
        if not self.canvas:
            if not enough_space_on_page(self.ctx,self.line_height()):
                self.on_next_page()
            self.paint_zebra(self.ctx.y)
            baseline = self.ctx.y - self.ctx.font.height() - self.ctx.font.bbox_ymin()
            self.canvas = self.ctx.doc.page().canvas()
            self.canvas.text_start(self.ctx.opts.margins.left(), baseline)
            if self.first_time:
                place_bookmark(self.ctx)
                self.first_time = False
        if token:
            self.process_token(token)
        self.canvas.text(text)
        
    def on_next_page(self):
        if self.canvas:
            self.on_line_end()
        self.ctx.paging.next_page()
        self.line_nr = 0
        
    def paint_zebra(self, y):
        zebra = self.ctx.opts.zebra
        if zebra:
            col = zebra[self.line_nr % len(zebra)]
            if col:
                canvas = self.ctx.doc.page().canvas()
                canvas.state_save()
                height = self.line_height()
                canvas.color("f", *col)
                canvas.rectangle(self.ctx.opts.margins.left(), y - height,
                                 self.ctx.opts.margins.canvas_width(), height)
                canvas.path_paint("f")
                canvas.state_restore()
        

# ----------------------------------------------------------------------
#                   document pagination
       
class Paging:
    def __init__(self, ctx):
        self.ctx = ctx
        self.page_nr = 0 # nr of physical pages
        self.page_start()

    def set_page_topleft_y(self):
        self.ctx.y = self.ctx.opts.page[1] - self.ctx.opts.margins.top()

    def page_start(self):
        self.ctx.doc.page_start(*self.ctx.opts.page)
        self.establish_state()
        self.set_page_topleft_y()
        self.is_page_open = True

    def page_end(self):
        self.ctx.doc.page_end()
        self.is_page_open = False
        self.page_nr += 1

    def next_page(self):
        self.page_end()
        self.page_start()

    def finalize(self):
        if self.is_page_open:
            Paging.page_end(self)

    def establish_state(self):
        opts = self.ctx.opts
        canvas = self.ctx.doc.page().canvas()
        canvas.text_font(self.ctx.font)
        canvas.text_character_spacing(opts.charspacing)
        canvas.color_space("fs", jagpdf.CS_DEVICE_RGB)
        canvas.color("f", *opts.font_color)
        if opts.page_color:
            canvas.state_save()
            canvas.color("fs", *opts.page_color)
            canvas.rectangle(0, 0, *opts.page)
            canvas.path_paint("fs")
            canvas.state_restore()
        #self.draw_margin()
            
    def draw_margin(self):
        opts = self.ctx.opts
        canvas = self.ctx.doc.page().canvas()
        canvas.color("fs", *opts.font_color)
        canvas.rectangle(opts.margins.left(), opts.margins.bottom(),
                         opts.margins.canvas_width(), opts.margins.canvas_height())
        canvas.path_paint('s')

        
class NupPaging(Paging):
    def __init__(self, ctx):
        assert ctx.opts.nup
        self.nups = nup_matrices(ctx.opts.nup, *ctx.opts.page)
        self.logical_page_nr = 0
        Paging.__init__(self, ctx)

    def page_start(self):
        i = self.logical_page_nr % len(self.nups)
        if i == 0:
            Paging.page_start(self)
        else:
            self.ctx.doc.page().canvas().state_restore()
            self.set_page_topleft_y()
        canvas = self.ctx.doc.page().canvas()
        canvas.state_save()
        canvas.transform(*self.nups[i])

    def page_end(self):
        canvas = self.ctx.doc.page().canvas()
        canvas.state_restore()
        self.logical_page_nr += 1
        if 0 == (self.logical_page_nr % len(self.nups)):
            Paging.page_end(self)
        else:
            canvas.state_save()


def nup_matrices(power, w, h):
    import math
    dim = 2 ** (power / 2)
    c = 1.0 / math.sqrt(2 ** power)
    if power % 2:
        return [(0.0, c, -c, 0.0, i * h * c, j * w * c) \
                for i in range(1, dim+1) for j in range(2 * dim)]
    else:
        return [(c, 0.0, 0.0, c, j * w * c, i * h * c) \
                for i in range(dim-1, -1, -1) for j in range(dim)]

# ----------------------------------------------------------------------
#                        state management
#
def load_font(ctx):
    opts = ctx.opts
    if os.path.isfile(opts.font):
        name = 'file=' + opts.font
    else:
        name = 'standard; name=' + opts.font
    spec = "%s; size=%d; enc=utf-8" % (name, opts.fontsize)
    ctx.font = ctx.doc.font_load(spec)

def create_doc_profile(opts):
    profile = jagpdf.create_profile()
    viewer_prefs = []
    encrypted = False
    if opts.docname:
        profile.set('info.title', opts.docname)
        viewer_prefs.append('DisplayDocTitle')
    if viewer_prefs:
        profile.set('doc.viewer_preferences', ';'.join(viewer_prefs))
    if opts.initdest:
        profile.set('doc.initial_destination', opts.initdest)
    if opts.fullscreen:
        profile.set('doc.page_mode', 'FullScreen')
    if opts.pagelayout:
        profile.set('doc.page_layout', opts.pagelayout)
    if None != opts.ownerpwd:
        profile.set('stdsh.pwd_owner', opts.ownerpwd)
        encrypted = True
    if None != opts.userpwd:
        profile.set('stdsh.pwd_user', opts.userpwd)
        encrypted = True
    if opts.userpermissions:
        profile.set('stdsh.permissions', opts.userpermissions.replace(',', ';'))
        encrypted = True
    if encrypted:
        profile.set('doc.encryption', 'standard')
    #profile.set("doc.trace_level", "5")
    #profile.set("doc.compressed", "0")
    return profile

def create_initial_context(opts):
    """Creates an initial document context from global options."""
    profile = create_doc_profile(opts)
    doc = jagpdf.create_stream(PDFStream(opts), profile)
    ctx = Bunch(doc=doc, img_cache={}, input_nr=0, opts=opts)
    load_font(ctx)
    ctx.paging = opts.nup and NupPaging(ctx) or Paging(ctx)
    return ctx

def update_context(ctx):
    """Updates the document context from the current options."""
    load_font(ctx)



# ----------------------------------------------------------------------
#          command line option callbacks & related helpers
#

def parse_color(color):
    if color == '':
        return None
    try:
        return int(color[0:2], 16) / 255.0, \
               int(color[2:4], 16) / 255.0, \
               int(color[4:6], 16) / 255.0
    except ValueError:
        raise OptionValueError("invalid color specification %s" % color)

def hook_paper_format(option, opt, value, parser):
    formats = {'a4': [8.3 * 72, 11.7 * 72],
               'letter': [8.5 * 72, 11.0 * 72]}
    try:
        page = formats[value.lower()]
    except KeyError:
        m = re.compile('^([0-9.]+),([0-9.]+)$').match(value)
        if m:
            page = [float(m.group(1)), float(m.group(2))]
        else:
            raise OptionValueError("unrecognized page format: %s" % value)
    setattr(parser.values, option.dest, page)

def hook_color(option, opt, value, parser):
    setattr(parser.values, option.dest, parse_color(value))

def hook_color_list(option, opt, value, parser):
    colors = [parse_color(c) for c in value.split(',')]
    setattr(parser.values, option.dest, colors)
    
def hook_destination(option, opt, value, parser):
    try:
        dest = "mode=XYZ; zoom=%.2f" % float(value)
    except ValueError:
        optmap = { 'fith': 'FitH', 'fitv': 'FitV', 'fit': 'Fit'}
        if value in optmap:
            dest = "mode=" + optmap[value]
        else:
            raise OptionValueError('unrecognized destination: %s' % value)
    setattr(parser.values, option.dest, dest)


class Margins:
    def __init__(self, parser, margins):
        self.parser = parser
        self.margins = margins
        
    def top(self): return self.margins[0]
    def right(self): return self.margins[1]
    def bottom(self): return self.margins[2]
    def left(self): return self.margins[3]
    def width(self): return self.left() + self.right()
    def height(self): return self.top() + self.bottom()
    def canvas_width(self): return self.parser.values.page[0] - self.width()
    def canvas_height(self): return self.parser.values.page[1] - self.height()


def hook_margins(option, opt, value, parser):
    try:
        margins = [float(m) for m in value.split(',')]
        if len(margins) != 4:
            raise OptionValueError("unrecognized margins: %s" % value)
    except ValueError:
        raise OptionValueError("unrecognized margins: %s" % value)
    setattr(parser.values, option.dest, Margins(parser, margins))

optmap_layout = {'single': 'SinglePage',
                 'cont': 'OneColumn',
                 'cont-facing': 'TwoColumnLeft'}

optset_perm = {'no_copy': 'no_copy',
               'no_print' : 'no_print',
               'no_modify' : 'no_modify'}
optset_intype = set(['auto', 'text', 'image'])

def hook_string_remap(option, opt, values, parser, optmap, is_list=False):
    result = []
    for value in values.split(','):
        if value in optmap:
            if type(optmap) == dict:
                value = optmap[value]
            result.append(value)
        else:
            raise OptionValueError("unrecognized value: %s" % values)
    if not is_list and len(result) > 1:
        raise OptionValueError("unrecognized value: %s" % values)
    setattr(parser.values, option.dest, ";".join(result))

class PageBreak: pass
def hook_separator(option, opt, value, parser):
    if value == 'break':
        result = PageBreak
    else:
        try:
            result = float(value)
        except ValueError:
            raise OptionValueError("unrecognized value %s" % value)
    setattr(parser.values, option.dest, result)

def create_opt_parser():
    usage = """usage: %prog [global-options] [[input-options] INPUT] ..."""
    desc = __doc__
    # A command comprises of input file options and an input file ('-' stands
    # for if token[ stdin). lists are comma separated, units are 1/72 inch,
    # color format is rrggbb
    version="%%prog %s (JagPDF %06x)" % (__version__, jagpdf.version())
    parser = OptionParser(usage=usage,
                          version=version,
                          description=desc)
    parser.set_defaults(page=[8.3 * 72, 11.7 * 72], #a4
                        fontsize=10,
                        encoding='iso-8859-1',
                        charspacing=0.0,
                        linespacing=0.0,
                        font_color=(0, 0, 0),
                        page_color=None,
                        docname=None,
                        initdest=None,
                        fullscreen=False,
                        pagelayout=None,
                        font="Courier",
                        ownerpwd=None,
                        userpwd=None,
                        userpermissions=None,
                        margins=Margins(parser, (72, 68, 72, 68)),
                        zebra=None,
                        nup=None,
                        separator=PageBreak,
                        image_align='left',
                        image_fit_wider=True,
                        image_dpi=None,
                        outfile='-',
                        bookmark=None,
                        highlight=False,
                        tabsize=4,
                        filtercmd=None,
                        force_pil=False,
                        force_imagemagick=False,
                        input_type='auto'                        
                        )
    parser.add_option("--force-pil", action="store_true", dest="force_pil",
                      help=SUPPRESS_HELP)
    parser.add_option("--force-imagemagick", action="store_true", dest="force_imagemagick",
                      help=SUPPRESS_HELP)
    group = OptionGroup(parser, "Common input options")
    group.add_option("--input-type",
                     action="callback", type="string", dest="input_type",
                     callback=hook_string_remap, callback_args=(optset_intype,),
                     help="set the type of the input, can be one of: text, image or auto [%default]",
                     metavar="TYPE")
    group.add_option("--page",
                     action='callback', type='string', dest='page',
                     callback=hook_paper_format,
                     help="set page size to PAGE, can be A4, Letter, or custom width,height [A4]")
    group.add_option("--page-color",
                     action='callback', type='string', dest="page_color",
                     callback=hook_color,
                     help="set the page background color [white]", metavar="COLOR")
    group.add_option("--margins",
                     action='callback', type='string', dest="margins",
                     callback=hook_margins,
                     help="set page margins, MARGINS is a top,right,bottom,left list",
                     metavar="MARGINS")
    group.add_option("--bookmark",
                     action='store', type='string', dest='bookmark',
                     help="create a bookmark labeled with STRING", metavar='STRING')
    group.add_option("--separator",
                     action='callback', type='string', dest='separator',
                     callback=hook_separator,
                     help="insert vertical SPACE before the input; setting SPACE to 'break' inserts a page break",
                     metavar="SPACE")
    group.add_option("--filter", action='store', dest='filtercmd',
                     help="run CMD through the shell and use its stdout", metavar='CMD')
    #
    groupt = OptionGroup(parser, "Text input options")
    groupt.add_option("--font",
                     action='store', type='string', dest='font',
                     help="show text using FONT, can be either a full path or a core font name [Courier]")
    groupt.add_option("--font-size",
                      action='store', type='int', dest="fontsize",
                      help="set font size [%default]", metavar="SIZE")
    groupt.add_option("--text-color",
                     action='callback', type='string', dest="font_color",
                     callback=hook_color,
                     help="set text color",
                     metavar="COLOR")
    groupt.add_option("--encoding",
                     action='store', type='string', dest="encoding",
                     help="specify text encoding [%default]",
                     metavar="ENC")
    groupt.add_option("--zebra",
                      action='callback', type='string', dest="zebra",
                      callback=hook_color_list,
                      help="paint a zebra with the list of COLORS",
                      metavar="COLORS")
    groupt.add_option("--char-spacing",
                     action='store', type='float', dest="charspacing",
                     help="scale character spacing with FACTOR [0.0]",
                     metavar="FACTOR")
    groupt.add_option("--line-spacing",
                     action='store', type='float', dest="linespacing",
                     help="scale character spacing with FACTOR [0.0]",
                     metavar="FACTOR")
    groupt.add_option("--highlight",
                      action="store_true", dest="highlight",
                      help="syntax highlight the input (needs Pygments)")
    groupt.add_option("--no-highlight",
                      action="store_false", dest="highlight",
                      help="do not syntax highlight the input (default)")
    #
    groupi = OptionGroup(parser, "Image input options")
    groupi.add_option("--image-align", type="string", dest="image_align",
                      help="set the image alignment mode, IMAGE can be on of: left, center, right [%default]",
                      metavar="ALIGN")
    groupi.add_option("--image-fit-wide", action="store_true",
                      dest="image_fit_wider",
                      help="resize the image if it is wider than the page [default]")
    groupi.add_option("--no-image-fit-wide", action="store_false",
                      dest="image_fit_wider",
                      help="turn off --image-fit-wide")
    groupi.add_option("--image-dpi", type="float", dest="image_dpi",
                      help="use DPI instead of the dpi specified in the image", metavar="DPI")
    parser.add_option_group(group)
    parser.add_option_group(groupt)
    parser.add_option_group(groupi)
    #
    group = OptionGroup(parser, "Global options")
    group.add_option("-o", "--output-file",
                      action='store', type='string', dest="outfile",
                      help="send output to FILE instead of stdout",
                     metavar="FILE")
    group.add_option("--initial-dest",
                     action='callback', type='string', dest="initdest",
                     callback=hook_destination,
                     help="set the initial destination, DST can be one of: fitv, fith, fit, or a number specifying the zoom factor",
                     metavar="DST")
    group.add_option("--full-screen",
                     action='store_true', dest="fullscreen",
                     help="document will be initially displayed in full-screen mode")
    group.add_option("--page-layout",
                     action='callback', type='string', dest='pagelayout',
                     callback=hook_string_remap, callback_args=(optmap_layout,),
                     help="set initial page layout, MODE can be one of: single, cont, and cont-facing", metavar="MODE")
    group.add_option("--owner-pwd",
                     action='store', type='string', dest='ownerpwd',
                     help="set the owner password", metavar="PWD")
    group.add_option("--user-pwd",
                     action='store', type='string', dest='userpwd',
                     help="set the user password", metavar="PWD")
    group.add_option("--user-perm",
                     action='callback', type='string', dest='userpermissions',
                     callback=hook_string_remap,
                     callback_args=(optset_perm, True),
                     help="set access permissions, PERM can be a combination of: no_print, no_modify, and no_copy",
                     metavar="PERM")
    group.add_option("--n-up",
                     action='store', type='int', dest='nup',
                     help="perform imposition of 2^N pages on a sheet [0]", metavar="N")
    group.add_option("--doc-name",
                     action='store', type='string', dest="docname",
                     help="set document name", metavar="STRING")
    parser.add_option_group(group)
    
    return parser

arg_re = r'((?:(?<=\\)"|[^"])+)' # anything except " not preceded by \
reg_args = re.compile(r'(?<!\\)"%s(?<!\\)"|(\S+)' % arg_re, re.M | re.S)
def expand_args(args):
    """Expands arguments with those defined in an external @file"""
    result = []
    for arg in args:
        if arg.startswith('@'):
            from_file = open(arg[1:]).read()
            matches = re.finditer(reg_args, from_file)
            fargs = [g.replace(r'\"', '"') for m in matches for g in m.groups() if g]
            result += fargs
        else:
            result.append(arg)
    return result

# ---------------------------------------------------------------------------
#                           main loop

def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]
    try:
        argv = expand_args(argv)
        parser = create_opt_parser()
        parser.disable_interspersed_args()
        ctx, opts = None, None
        while 1:
            opts, argv = parser.parse_args(argv, opts)
            if not len(argv):
                raise Error("no input, run with --help")
            if not ctx:
                ctx = create_initial_context(opts)
            update_context(ctx)
            if ctx.input_nr > 0:
                insert_input_separator(ctx)
            process_input(ctx, argv.pop(0))
            ctx.input_nr += 1
            if not argv:
                ctx.paging.finalize()
                ctx.doc.finalize()
                break
    except Error, err:
        print >>sys.stderr, err.msg
        return 1
    except jagpdf.Exception, err:
        print >>sys.stderr, "JagPDF error:", err
        return 1
    except (URLError, HTTPError), err:
        print >>sys.stderr, 'Cannot read from remote stream:', err.reason[1]
        return 1
    except IOError, err:
        print >>sys.stderr, 'IO Error:', err
        return 1

re_is_img = re.compile('^.*\.(?:png|jpg|bmp|tiff|gif|p[pgbn]m)$', re.I)
def treat_as_text(ctx):
    if ctx.opts.input_type == 'auto':
        return not re_is_img.match(ctx.input_name)
    elif ctx.opts.input_type == 'text':
        return True
    else:
        assert ctx.opts.input_type == 'image'
        return False

re_is_url = re.compile('^(?:http|ftp)://\S+$', re.I)
def get_stream(ctx):
    if ctx.opts.filtercmd:
        cmd = subst_template(ctx, ctx.opts.filtercmd)
        try:
            pipe = Popen(cmd, stdout=PIPE, shell=True)
            out, err = pipe.communicate()
            stream = StringIO(out)
        except OSError, why:
            raise Error("cmd execution failed:" + str(why))
    elif ctx.input_name == '-':
        stream = sys.stdin
    else:
        if re_is_url.match(ctx.input_name):
            stream = urlopen(ctx.input_name)
        else:
            stream = open(ctx.input_name, 'rb')
    return stream

def process_input(ctx, input_name):
    """Processes single input."""
    ctx.input_name = input_name
    stream = get_stream(ctx)
    if treat_as_text(ctx):
        show_text(ctx, stream)
    else:
        paint_image(ctx, stream)
    stream.close()

if __name__ == '__main__':
    sys.exit(main())

# TBD:
# ----
#  - improve help screen
#    - brief description before option list
#    - point to man page or to the online docs
#  - underscore overpainted by a zebra stripe
#  - spell check the help argument in parser.add_option
#  - cmd err code (according to docs - linux only)

# Enhancements
# ------------
#  - nup - now ok, support for 2, 4, 6, 9, 16
#  - load profile from a separate file
#  - automatic horizontal margins according chars per line
#  - input prologue
#  - headers/footers
    
    

