# Sketch - A Python-based interactive drawing program
# Copyright (C) 1998, 1999, 2000 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

###Sketch Config
#type = Import
#class_name = 'XFigLoader'
#rx_magic = r'^#FIG (?P<version>3\.[012])'
#tk_file_type = ('XFig', '.fig')
format_name = 'XFig'
#unload = 1
#standard_messages = 1
###End

(''"XFig")

#
# This is a simple import filter for Fig files.
#
# It is incomplete and may refuse to load some valid Fig files. It
# should work with versions 3.0, 3.1 and 3.2 of the Fig format.
#
# TODO (not in any particular order):
#
# - convert X-Splines (new in XFig 3.2) to bezier curves if that can be done
#
# - parse strings properly
#
# - use more of the header information: pagesize, orientation, ...
#
# - patterns, arrows, dashes
#


import sys
from string import strip, atoi, split, atof, lower
from operator import getitem
from math import atan2

from Sketch import const, SketchLoadError
from Sketch.warn import warn, INTERNAL, warn_tb

from Sketch.load import SimplifiedLoader, EmptyCompositeError
from Sketch import load, skread
tokenize = skread.tokenize_line

from Sketch import Scale, Trafo, Translation, Rotation, StandardColors, \
     XRGBColor, SolidPattern, EmptyPattern, Blend, GetFont, SimpleText, \
     CreatePath
from Sketch.Graphics import text, pagelayout



std_colors = [StandardColors.black, StandardColors.blue, StandardColors.green,
              StandardColors.cyan, StandardColors.red, StandardColors.magenta,
              StandardColors.yellow, StandardColors.white] + \
              map(XRGBColor, ("#000090", "#0000b0", "#0000d0", "#87ceff",
                              "#009000", "#00b000", "#00d000", "#009090",
                              "#00b0b0", "#00d0d0", "#900000", "#b00000",
                              "#d00000", "#900090", "#b000b0", "#d000d0",
                              "#803000", "#a04000", "#c06000", "#ff8080",
                              "#ffa0a0", "#ffc0c0", "#ffe0e0", "#ffd700"))

BLACK = 0
WHITE = 7
DEFAULT_COLOR = -1

xfig_join = (const.JoinMiter, const.JoinBevel, const.JoinRound)
xfig_cap = (const.CapButt, const.CapRound, const.CapProjecting)

psfonts = ("Times-Roman", "Times-Italic", "Times-Bold",
           "Times-BoldItalic", "AvantGarde-Book",
           "AvantGarde-BookOblique", "AvantGarde-Demi",
           "AvantGarde-DemiOblique", "Bookman-Light",
           "Bookman-LightItalic", "Bookman-Demi", "Bookman-DemiItalic",
           "Courier", "Courier-Oblique", "Courier-Bold",
           "Courier-BoldOblique", "Helvetica", "Helvetica-Oblique",
           "Helvetica-Bold", "Helvetica-BoldOblique",
           "Helvetica-Narrow", "Helvetica-Narrow-Oblique",
           "Helvetica-Narrow-Bold", "Helvetica-Narrow-BoldOblique",
           "NewCenturySchlbk-Roman", "NewCenturySchlbk-Italic",
           "NewCenturySchlbk-Bold", "NewCenturySchlbk-BoldItalic",
           "Palatino-Roman", "Palatino-Italic", "Palatino-Bold",
           "Palatino-BoldItalic", "Symbol", "ZapfChancery-MediumItalic",
           "ZapfDingbats", "Times-Roman")

texfonts = ('Times-Roman', 'Times-Roman', 'Times-Bold', 'Times-Italic',
            'Helvetica', 'Courier')
# for user messages:
tex_font_names = ('Default font', 'Roman', 'Bold', 'Italic', 'Sans Serif',
                  'Typewriter')

align = (text.ALIGN_LEFT, text.ALIGN_CENTER, text.ALIGN_RIGHT)

def coords_to_points(coords, trafo):
    # Given a list coords of coordinate pairs [x1, y1, x2, y2, ...],
    # return a new list of point objects: [Point(x1, y1), Point(x2, y2),...]
    # TRAFO is applied to point.
    # coords must have an even length.
    if len(coords) % 2 != 0:
        raise ValueError("coordinate has odd length")
    num = len(coords)
    coords = [coords] * (num / 2)
    return map(trafo, map(getitem, coords, range(0, num, 2)),
               map(getitem, coords, range(1, num, 2)))

class XFigLoader(SimplifiedLoader):

    format_name = format_name
    
    functions = [('define_color',	0),
                 ('read_ellipse',       1),
                 ('read_polyline',      2),
                 ('read_spline',        3),
                 ('read_text',          4),
                 ('read_arc',           5),
                 ('begin_compound',     6),
                 ('end_compound',       -6)]

    def __init__(self, file, filename, match):
	SimplifiedLoader.__init__(self, file, filename, match)
        self.layout = None
        self.format_version = atof(match.group('version'))
        self.trafo = Trafo(1.0, 0.0, 0.0, -1.0, 0.0, 800)
        self.colors = std_colors + [StandardColors.black] * 512
        self.depths = {} # map object ids to depth
        self.guess_cont()

    def readline(self):
        line = SimplifiedLoader.readline(self)
        while line[:1] == '#':
            line = SimplifiedLoader.readline(self)
        return line

    def get_compiled(self):
	funclist = {}
	for name, rx in self.functions:
	    funclist[rx] = getattr(self, name)
	return funclist

    def set_depth(self, depth):
        self.depths[id(self.object)] = -depth

    def define_color(self, line):
        idx, color = split(line, None, 1)
        self.colors[atoi(idx)] = XRGBColor(color)

    def get_pattern(self, color, style = None):
        if style == -1:
            return EmptyPattern
        rgb = self.colors[color]
        if style is not None:
            if color in (BLACK, DEFAULT_COLOR):
                if style > 0 and style <= 20:
                    rgb = Blend(self.colors[WHITE], rgb, (20 - style) / 20.0)
                elif style == 0:
                    rgb = self.colors[WHITE]
            else:
                if style >= 0 and style < 20:
                    rgb = Blend(self.colors[BLACK], rgb, (20 - style) / 20.0)
                elif style > 20 and style <= 40:
                    rgb = Blend(self.colors[WHITE], rgb, (style - 20) / 20.0)
        return SolidPattern(rgb)

    def line(self, color, width, join, cap, style = 0, style_val = 0):
        if width:
            val = style_val / width
            width = width * 72.0/80.0
            pattern = self.get_pattern(color)
            dashes = ()
            if style == 1:
                # dashed
                dashes = (val, val)
            elif style == 2:
                # dotted
                dashes = (1 / width, val)
            elif style == 3:
                # dash-dot
                dashes = (val, 0.5 * val, 1 / width, 0.5 * val)
            elif style == 4:
                # dash-dot-dot
                dashes = (val, 0.45 * val, 1 / width, 0.333 * val,
                          1 / width, 0.45 * val)
            elif style == 5:
                # dash-dot-dot-dot
                dashes = (val, 0.4 * val,
                          1 / width, 0.333 * val, 1 / width, 0.333 * val,
                          1 / width, 0.4 * val)
            try:
                self.set_properties(line_pattern = pattern,
                                    line_width = width,
                                    line_join = xfig_join[join],
                                    line_cap = xfig_cap[cap],
                                    line_dashes = dashes)
            except:
                raise SketchLoadError("can't assign line style: %s:%s"
                                      % sys.exc_info()[:2])
        else:
            self.empty_line()

    def fill(self, color, style):
        pattern = self.get_pattern(color, style)
	try:
            self.set_properties(fill_pattern = pattern)
	except:
	    raise SketchLoadError("can't assign fill style: %s:%s"
				  % sys.exc_info()[:2])

    def font(self, font, size, flags):
        if flags & 4:
            # A PostScript font
            name = psfonts[font]
        else:
            # A TeX font. map to psfont
            name = texfonts[font]
            self.add_message(_("PostScript font `%(ps)s' substituted for "
                               "TeX-font `%(tex)s'")
                             % {'ps':name, 'tex':tex_font_names[font]})
                             
        self.set_properties(font = GetFont(name), font_size = size)

    def read_tokens(self, num):
        # read NUM tokens from the input file. return an empty list if
        # eof met before num items are read
        readline = self.readline; tokenize = skread.tokenize_line
        tokens = []
        while len(tokens) < num:
            line = readline()
            if not line:
                return []
            tokens = tokens + tokenize(line)
        return tokens

    def read_arc(self, line):
        readline = self.readline; tokenize = skread.tokenize_line
        args = tokenize(line)
        if len(args) != 21:
            raise SketchLoadError('Invalid Arc specification')
        sub_type, line_style, thickness, pen_color, fill_color, depth, \
                  pen_style, area_fill, style, cap, direction, \
                  forward_arrow, backward_arrow, \
                  cx, cy, x1, y1, x2, y2, x3, y3 = args
        self.fill(fill_color, area_fill)
        self.line(pen_color, thickness, const.JoinMiter, cap,
                  line_style, style)
        
        if forward_arrow: readline() # XXX: implement this
        if backward_arrow:readline() # XXX: implement this

        trafo = self.trafo
        center = trafo(cx, cy); start = trafo(x1, y1); end = trafo(x3, y3)
        radius = abs(start - center)
        start_angle = atan2(start.y - center.y, start.x - center.x)
        end_angle = atan2(end.y - center.y, end.x - center.x)
        if direction == 0:
            start_angle, end_angle = end_angle, start_angle
        if sub_type == 1:
            sub_type = const.ArcArc
        else:
            sub_type = const.ArcPieSlice
        self.ellipse(radius, 0, 0, radius, center.x, center.y,
                     start_angle, end_angle, sub_type)
        self.set_depth(depth)

    def read_ellipse(self, line):
        readline = self.readline; tokenize = skread.tokenize_line
        args = tokenize(line)
        if len(args) != 19:
            raise SketchLoadError('Invalid Ellipse specification')
        sub_type, line_style, thickness, pen_color, fill_color, depth, \
                  pen_style, area_fill, style, direction, angle, \
                  cx, cy, rx, ry, sx, sy, ex, ey = args
        self.fill(fill_color, area_fill)
        self.line(pen_color, thickness, const.JoinMiter, const.CapButt,
                  line_style, style)
        
        center = self.trafo(cx, cy); radius = self.trafo.DTransform(rx, ry)
        trafo = Trafo(radius.x, 0, 0, radius.y)
        trafo = Rotation(angle)(trafo)
        trafo = Translation(center)(trafo)
        apply(self.ellipse, trafo.coeff())
        self.set_depth(depth)

    def read_polyline(self, line):
        readline = self.readline; tokenize = skread.tokenize_line
        args = tokenize(line)
        if len(args) != 15:
            raise SketchLoadError('Invalid PolyLine specification')
        sub_type, line_style, thickness, pen_color, fill_color, depth, \
                  pen_style, area_fill, style, join, cap, \
                  radius, forward_arrow, backward_arrow, npoints = args
        self.fill(fill_color, area_fill)
        self.line(pen_color, thickness, join, cap, line_style, style)

        if forward_arrow: readline() # XXX: implement this
        if backward_arrow:readline() # XXX: implement this
        if sub_type == 5: readline() # imported picture

        path = CreatePath()
        ncoords = npoints * 2
        pts = self.read_tokens(ncoords)
        if not pts:
            raise SketchLoadError('Missing points for polyline')
        if len(pts) > ncoords:
            del pts[ncoords:]
        map(path.AppendLine, coords_to_points(pts, self.trafo))
        if sub_type in (2, 3, 4):
            path.load_close(1)
        self.bezier(paths = path)
        self.set_depth(depth)

    def read_spline(self, line):
        readline = self.readline; tokenize = skread.tokenize_line
        args = tokenize(line)
        if len(args) != 13:
            raise SketchLoadError('Invalid Spline specification')
        sub_type, line_style, thickness, pen_color, fill_color, depth, \
                  pen_style, area_fill, style, cap, \
                  forward_arrow, backward_arrow, npoints = args
        closed = sub_type & 1
        if forward_arrow: readline()
        if backward_arrow:readline()

        # in 3.2 all splines are stored as x-splines...
        if self.format_version == 3.2:
            if sub_type in (0, 2):
                sub_type = 4
            else:
                sub_type = 5
        
        self.fill(fill_color, area_fill)
        self.line(pen_color, thickness, 0, cap, line_style, style)
        
        ncoords = npoints * 2
        pts = self.read_tokens(ncoords)
        if not pts:
            raise SketchLoadError('Missing points for spline')
        if len(pts) > ncoords:
            del pts[ncoords:]
        pts = coords_to_points(pts, self.trafo)
        
        path = CreatePath()
        if sub_type in (2, 3):
            # interpolated spline, read 2 control points for each node
            ncontrols = 4 * npoints
            controls = self.read_tokens(ncontrols)
            if not controls:
                raise SketchLoadError('Missing control points for spline')
            if len(controls) > ncontrols:
                del controls[ncontrols:]
            controls = coords_to_points(controls[2:-2], self.trafo)
            path.AppendLine(pts[0])
            ncontrols = 2 * (npoints - 1)
            controls = [controls] * (npoints - 1)
            map(path.AppendBezier,
                map(getitem, controls, range(0, ncontrols, 2)),
                map(getitem, controls, range(1, ncontrols, 2)),
                pts[1:])
        elif sub_type in (0, 1):
            # approximated spline
            f13 = 1.0 / 3.0; f23 = 2.0 / 3.0
            curve = path.AppendBezier
            straight = path.AppendLine
            last = pts[0]
            cur = pts[1]
            start = node = (last + cur) / 2
            if closed:
                straight(node)
            else:
                straight(last)
                straight(node)
            last = cur
            for cur in pts[2:]:
                c1 = f13 * node + f23 * last
                node = (last + cur) / 2
                c2 = f13 * node + f23 * last
                curve(c1, c2, node)
                last = cur
            if closed:
                curve(f13 * node + f23 * last, f13 * start + f23 * last, start)
            else:
                straight(last)
        elif sub_type in (4, 5):
            # An X-spline. Treat it like a polyline for now.
            # read and discard the control info
            self.read_tokens(npoints)
            self.add_message(_("X-Spline treated as PolyLine"))
            map(path.AppendLine, pts)
            if closed:
                path.AppendLine(path.Node(0))
        if closed:
            path.load_close(1)
        self.bezier(paths = path)
        self.set_depth(depth)

    def read_text(self, line):
        args = tokenize(line, 12) # don't tokenize the text itself
        if len(args) != 13: # including the unparsed rest of the line
            raise SketchLoadError('Invalid text specification')
        sub_type, color, depth, pen_style, font, size, angle, flags, \
                  height, length, x, y, rest = args
        self.fill(color, None)
        self.font(font, size * 0.9, flags)
        
        if len(rest) > 2: #at least a space and a newline
            # read the actual text. This implementation may fail in
            # certain cases!
            string = rest[1:]
            while string[-5:] != '\\001\n':
                line = self.readline()
                if not line:
                    raise SketchLoadError('Premature end of string')
                string = string + line
            globals = {'__builtins__': {}}
            try:
                # using eval here might be a security hole!
                string = eval('"""' + string[:-5] + '"""', globals)
            except:
                string = eval("'''" + string[:-5] + "'''", globals)
        else:
            raise SketchLoadError('Invalid text string')
            
        trafo = Translation(self.trafo(x, y))(Rotation(angle))
        self.simple_text(string, trafo = trafo, halign = align[sub_type])
        self.set_depth(depth)

    def begin_compound(self, line):
        self.begin_group()

    def end_compound(self, line):
        try:
            self.end_group()
        except EmptyCompositeError:
            pass

    def end_composite(self):
        # sort composite_items by their depth
        items = self.composite_items
        depths = map(self.depths.get, map(id, items), [-10000]*len(items))
        depths = map(None, depths, range(len(items)), items)
        depths.sort()
        self.composite_items = map(getitem, depths, [2] * len(items))
        SimplifiedLoader.end_composite(self)

    def read_header(self):
        format = orientation = None
        if self.format_version >= 3.0:
            line = strip(self.readline())
            if line:
                # portrait/landscape
                if lower(line) == 'landscape':
                    orientation = pagelayout.Landscape
                else:
                    orientation = pagelayout.Portrait
            else:
                raise SketchLoadError('No format specification')
            line = strip(self.readline())
            if line:
                # centering
                line = lower(line)
                if line == 'center' or line == 'flush left':
                    line = lower(strip(self.readline()))
            if not line:
                raise SketchLoadError(
                    'No Center/Flushleft or Units specification')
            if line == 'metric':
                # ignore for now
                pass
            if self.format_version >= 3.2:
                self.readline() # papersize
                self.readline() # magnification
                self.readline() # pages
                self.readline() # transparent color
        line = strip(self.readline())
        if line:
            try:
                ppi, coord = map(atoi, split(line))
            except:
                raise SketchLoadError('Invalid Resolution specification')
            self.trafo = self.trafo(Scale(72.0 / ppi))


    def Load(self):
	file = self.file
	funclist = self.get_compiled()
	# binding frequently used functions to local variables speeds up
	# the process considerably...
        readline = file.readline; tokenize = skread.tokenize_line
        self.document()
        self.layer(_("Layer 1"))
	try:
            self.read_header()
            line = self.readline()
            while line:
                tokens = tokenize(line, 1)
                if len(tokens) > 1:
                    function, rest = tokens
                else:
                    function = tokens[0]
                    rest = ''
                if type(function) == type(0):
                    function = funclist.get(function)
                    if function:
                        function(rest)
                line = self.readline()
                
	except SketchLoadError, value:
            warn_tb(INTERNAL)
	    raise SketchLoadError('%d:%s' % (self.lineno, str(value))), None,\
                  sys.exc_traceback
	except:
	    if load._dont_handle_exceptions:
		warn(INTERNAL, 'XFigLoader: error reading line %d:%s',
		     self.lineno, `line`)
		raise
	    raise SketchLoadError(_("error in line %d:\n%s")
				  % (self.lineno, `line`)), None,\
				  sys.exc_traceback
	self.end_all()
	self.object.load_Completed()
        return self.object





