# Sketch - A Python-based interactive drawing program
# Copyright (C) 1996, 1997, 1998, 1999, 2000, 2002, 2003 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

#
# PostScriptDevice:
#
# A graphics device which outputs to a postscript file. The postscript
# file can be an EPS file.
#

import os
from types import StringType
from math import pi, sqrt
from string import join, strip, lstrip, split

from Sketch.Lib.util import Empty

from Sketch import _, config, _sketch, Scale, SketchVersion
from Sketch.warn import pdebug, warn_tb, warn, USER, INTERNAL
from Sketch.Lib.psmisc import quote_ps_string, make_textline

from Sketch import IdentityMatrix
from Sketch.Lib import type1

from graphics import CommonDevice
import color
import pagelayout
import font


class _DummyLineStyle:
    def Execute(device):
        device.SetLineColor(color.StandardColors.black)
        device.SetLineAttributes(1, 0, 1, 0)

defaultLineStyle = _DummyLineStyle()

ps_join = (0, 1, 2)
ps_cap = (None, 0, 1, 2)

# header comments. Currently the type of all parameters is <textline>
HeaderComments = [('For', '%%%%For: %s\n'),
                  ('CreationDate', '%%%%CreationDate: %s\n'),
                  ('Title', '%%%%Title: %s\n')]

class PostScriptDevice(CommonDevice):

    draw_visible = 0
    draw_printable = 1

    file = None # default value for file in case open fails.

    def __init__(self, file, as_eps = 1, bounding_box = None,
                 document = None, printable = 1, visible = 0, rotate = 0,
                 embed_fonts = 0, **options):
        if as_eps and not bounding_box:
            raise ValueError, 'bounding_box required for EPS'

        self.fill = 0; self.line = 0
        self.properties = None
        self.line = defaultLineStyle
        self.as_eps = as_eps
        self.needed_resources = {}
        self.include_resources = {}
        self.supplied_resources = []
        self.draw_visible = visible
        self.draw_printable = printable
        self.gradient_steps = config.preferences.gradient_steps_print
        self.init_props()

        if document:
            document.WalkHierarchy(self.add_obj_resources,
                                   visible = self.draw_visible,
                                   printable = self.draw_printable)
            self.needed_resources.update(self.include_resources)

        # take care of the case where we're writing to an EPS that's
        # referenced by the document. This is a bit tricky. If the file
        # argument is an already opened file object, all hope is lost.
        # If it's a filename we look at all EPS files contained in the
        # document and load them into memory if and only if they are the
        # file we're writing into.

        # dictionary used as a set of ids of EpsData objects.
        self.loaded_eps_files = {}

        # the original contents of the file we're writing to if it is
        # actually referenced.
        self.loaded_eps_contents = None

        # search through the document if file is a string with the name
        # of an existing file.
        if isinstance(file, StringType) and os.path.exists(file):
            self.filename = file
            document.WalkHierarchy(self.handle_writes_to_embedded_eps,
                                   visible = self.draw_visible,
                                   printable = self.draw_printable)

        if type(file) == StringType:
            file = open(file, 'w')
            self.close_file = 1
        else:
            self.close_file = 0
        self.file = file

        if rotate:
            width, height = document.PageSize()
            llx, lly, urx, ury = bounding_box
            bounding_box = (height - ury, llx, height - lly, urx)

        write = file.write
        if as_eps:
            write("%!PS-Adobe-3.0 EPSF-3.0\n")
        else:
            write("%!PS-Adobe-3.0\n")
        # header comments
        for name, format in HeaderComments:
            if options.has_key(name):
                value = options[name]
                if value:
                    write(format % make_textline(value))
        write("%%%%Creator: Skencil %s\n" % SketchVersion)
        write("%%Pages: 1\n")	# there is exactly one page
        if bounding_box:
            [llx, lly, urx, ury] = map(int, bounding_box)
            write('%%%%BoundingBox: %d %d %d %d\n'
                  % (llx - 1, lly - 1, urx + 1, ury + 1))

        if rotate and document.Layout().Orientation() == pagelayout.Landscape:
            write("%%Orientation: Landscape\n")

        # XXX The Extensions comment should take embedded EPS files into
        # account. (the colorimage operator should be optional)
        write("%%Extensions: CMYK\n")

        write("%%DocumentSuppliedResources: (atend)\n")

        if self.needed_resources:
            start_written = 0
            for restype, value in self.needed_resources.keys():
                if restype == 'font' and embed_fonts:
                    continue
                if not start_written:
                    write('%%%%DocumentNeededResources: %s %s\n'
                          % (restype, value))
                else:
                    write('%%%%+ %s %s\n' % (restype, value))

        write("%%EndComments\n\n")

        write('%%BeginProlog\n')
        procfile = os.path.join(config.sketch_dir, config.postscript_prolog)
        procfile = open(procfile, 'r')
        line = procfile.readline()
        self.supplied_resources.append(join(split(strip(line))[1:]))
        write(line)
        for line in map(lstrip, procfile.readlines()):
            if line and line[0] != '%':
                write(line)
        write('%%EndResource\n')
        procfile.close()

        write('%%EndProlog\n\n')

        write('%%BeginSetup\n')
        if self.needed_resources:
            for res in self.include_resources.keys():
                restype, value = res
                if restype == 'font' and embed_fonts:
                    fontfile = font.GetFont(value).FontFileName()
                    if fontfile:
                        try:
                            fontfile = open(fontfile, 'rb')
                            write("%%%%BeginResource: %s %s\n" % res)
                            type1.embed_type1_file(fontfile, self.file)
                            write("\n%%EndResource\n")
                        except IOError, exc:
                            warn(USER, _("Can't embed font '%s': %s")
                                 % (value, exc))
                        del self.needed_resources[res]
                        self.supplied_resources.append(join(res))
                        continue
                    else:
                        warn(USER,
                             _("Can't find file for font '%s' for embedding")
                             % value)
                write('%%%%IncludeResource: %s %s\n' % res)
        write('\n10.433 setmiterlimit\n')	# 11 degree
        write('%%EndSetup\n\n')

        write('%%Page: 1 1\n')
        write('SketchDict begin\n')
        if rotate:
            self.Rotate(pi/2)
            self.Translate(0, -height)

    def init_props(self):
        self.init_line_props()
        self.init_color_props()
        self.init_font_props()

    def add_obj_resources(self, obj):
        # append reources from OBJ to self.needed_resources
        if obj.has_font:
            res = ('font', obj.Font().PostScriptName())
            self.include_resources[res] = 1
        if obj.is_Eps:
            self.needed_resources.update(obj.PSNeededResources())

    def handle_writes_to_embedded_eps(self, obj):
        if obj.is_Eps:
            data = obj.Data()
            # we only need to investigate this if we've not processed it yet
            if not self.loaded_eps_files.has_key(id(data)):
                filename = data.Filename()
                if os.path.samefile(self.filename, filename):
                    # it's the same file we're going to write into, so load it
                    self.loaded_eps_files[id(data)] = 1
                    if self.loaded_eps_contents is None:
                        self.loaded_eps_contents = open(filename).read()

    trailer_written = 0
    def Close(self):
        if self.file is not None and not self.file.closed:
            if not self.trailer_written:
                write = self.file.write
                write('%%PageTrailer\n')
                write('showpage\n')
                write('%%Trailer\n')
                write('end\n')
                if self.supplied_resources:
                    write('%%%%DocumentSuppliedResources: %s\n'
                          % self.supplied_resources[0])
                    for res in self.supplied_resources[1:]:
                        write('%%%%+ %s\n' % res)

                write('%%EOF\n')
                self.trailer_written = 1
            if self.close_file:
                self.file.close()

    def __del__(self):
        try:
            self.Close()
        except:
            warn_tb(INTERNAL, "In __del__ of psdevice")

    def PushTrafo(self):
        self.file.write('pusht\n')

    def Concat(self, trafo):
        self.file.write('[%g %g %g %g %g %g] concat\n'
                        % (trafo.m11, trafo.m21, trafo.m12, trafo.m22,
                           trafo.v1, trafo.v2))

    def Translate(self, x, y = None):
        if y is None:
            x, y = x
        self.file.write('%g %g translate\n' % (x, y))

    def Rotate(self, angle):
        self.file.write('%g rotate\n' % (angle * 180 / pi))

    def Scale(self, scale):
        self.file.write('%g dup scale\n' % scale)

    def PopTrafo(self):
        self.file.write('popt\n')

    def PushClip(self):
        self.file.write('pushc\n')

    def PopClip(self):
        self.file.write('popc\n')
        # popc is grestore. make sure properties are set properly again
        # after this.
        self.init_props()

    def init_color_props(self):
        self.current_color = None

    def _set_color(self, color):
        if self.current_color != color:
            r, g, b = color
            self.file.write('%g %g %g rgb\n'
                            % (round(r, 3), round(g, 3), round(b, 3)))
            self.current_color = color

    SetFillColor = _set_color
    SetLineColor = _set_color

    def init_line_props(self):
        self.current_width = self.current_cap = self.current_join = None
        self.current_dash = None

    def SetLineAttributes(self, width, cap = 1, join = 0, dashes = ()):
        write = self.file.write
        if self.current_width != width:
            width_changed = 1
            write('%g w\n' % width)
            self.current_width = width
        else:
            width_changed = 0
        join = ps_join[join]
        if self.current_join != join:
            write('%d j\n' % join)
            self.current_join = join
        cap = ps_cap[cap]
        if self.current_cap != cap:
            write('%d J\n' % cap)
            self.current_cap = cap

        if self.current_dash != dashes or width_changed:
            # for long dashes tuples this could theoretically produce
            # lines longer than 255 chars, which means that the file
            # would not conform to the DSC
            if width < 1:
                width = 1
            write('[')
            for dash in dashes:
                dash = width * dash
                if dash < 0.001:
                    dash = 0.001
                write('%g ' % dash)
            write('] 0 d\n')
            self.current_dash = dashes

    def SetLineSolid(self):
        self.file.write('[ ] 0 d\n')
        self.current_dash = ()

    def DrawLine(self, start, end):
        self.file.write('%g %g m %g %g l s\n' % (tuple(start) + tuple(end)))

    def DrawLineXY(self, x1, y1, x2, y2):
        self.file.write('%g %g m %g %g l s\n' % (x1, y1, x2, y2))

    def DrawRectangle(self, start, end):
        self.file.write('%g %g %g %g R s\n' % (tuple(start) + tuple(end)))

    def FillRectangle(self, left, bottom, right, top):
        self.file.write('%g %g %g %g R f\n' % (left, bottom, right, top))

    def DrawCircle(self, center, radius):
        self.file.write('%g %g %g C s\n' % (center.x, center.y, radius))

    def FillCircle(self, center, radius):
        self.file.write('%g %g %g C f\n' % (center.x, center.y, radius))

    def FillPolygon(self, pts):
        write = self.file.write
        if len(pts) > 1:
            write('%g %g m\n' % pts[0])
            for p in pts[1:]:
                write('%g %g l\n' % p)
            write('f\n')

    def DrawBezierPath(self, path, rect = None):
        self.write_path(path.get_save())
        self.file.write('S\n')

    def FillBezierPath(self, path, rect = None):
        self.write_path(path.get_save())
        self.file.write('F\n')

    def fill_path(self, clip = 0):
        write = self.file.write
        if self.proc_fill:
            if not clip:
                self.PushClip()
            write('eoclip newpath\n')
            self.properties.ExecuteFill(self, self.pattern_rect)
            if not clip:
                self.PopClip()
                write('newpath\n')
        else:
            self.properties.ExecuteFill(self, self.pattern_rect)
            if not clip:
                write('F\n')
            else:
                write('gsave F grestore eoclip newpath\n')

    def stroke_path(self):
        self.properties.ExecuteLine(self, self.pattern_rect)
        self.file.write('S\n')

    def fill_and_stroke(self, clip = 0):
        write = self.file.write

        if not clip and self.line:
            if self.fill:
                write('gsave\n')
                self.fill_path()
                write('grestore\n')
                self.init_props()
                self.stroke_path()
            else:
                self.stroke_path()
        else:
            if self.fill:
                self.fill_path(clip)
            elif clip:
                write('eoclip newpath\n')


    def write_path(self, list):
        write = self.file.write
        write('%g %g m\n' % list[0][:-1])
        for item in list[1:]:
            if len(item) == 3:
                write('%g %g l\n' % item[:-1])
            elif len(item) == 7:
                write('%g %g %g %g %g %g c\n' % item[:-1])
            else:
                if __debug__:
                    pdebug('PS', 'PostScriptDevice: invalid bezier item:',item)

    def MultiBezier(self, paths, rect = None, clip = 0):
        # XXX: try to write path only once
        write = self.file.write
        line = self.line; fill = self.fill

        if line or fill or clip:
            open = 0
            for path in paths:
                open = open or not path.closed

            write('newpath\n')
            if fill or clip:
                if not open:
                    # all subpaths are closed
                    for path in paths:
                        self.write_path(path.get_save())
                        write('closepath\n')
                    self.fill_and_stroke(clip)
                    line = 0
                else:
                    # some but not all sub paths are closed
                    for path in paths:
                        self.write_path(path.get_save())
                        if not path.closed:
                            write('closepath\n')
                    self.fill_path(clip)

            if line:
                for path in paths:
                    self.write_path(path.get_save())
                    if path.closed:
                        write('closepath\n')
                self.stroke_path()
                self.draw_arrows(paths)

    def Rectangle(self, trafo, clip = 0):
        if self.fill or self.line or clip:
            self.file.write('[%g %g %g %g %g %g] rect\n' % trafo.coeff())
            self.fill_and_stroke(clip)

    def RoundedRectangle(self, trafo, radius1, radius2, clip = 0):
        if self.fill or self.line or clip:
            self.file.write('[%g %g %g %g %g %g] %g %g rect\n'
                            % (trafo.coeff() + (radius1, radius2)))
            self.fill_and_stroke(clip)

    def SimpleEllipse(self, trafo, start_angle, end_angle, arc_type,
                      rect = None, clip = 0):
        if self.fill or self.line or clip:
            if start_angle == end_angle:
                self.file.write('[%g %g %g %g %g %g] ellipse\n'
                                % trafo.coeff())
                self.fill_and_stroke(clip)
            else:
                self.file.write('[%g %g %g %g %g %g] %g %g %d ellipse\n' %
                                (trafo.coeff()
                                 + (start_angle, end_angle, arc_type)))
                self.fill_and_stroke(clip)
            self.draw_ellipse_arrows(trafo, start_angle, end_angle, arc_type,
                                     rect)

    def init_font_props(self):
        self.current_font = None

    def set_font(self, font, size):
        spec = (font.PostScriptName(), size)
        if self.current_font != spec:
            self.file.write('/%s %g sf\n' % spec)
            self.current_font = spec

    def DrawText(self, text, trafo, clip = 0, cache = None):
        # XXX: should make sure that lines in eps file will not be
        # longer than 255 characters.
        font = self.properties.font
        if font:
            write = self.file.write
            self.set_font(font, self.properties.font_size)
            write('(%s)\n' % quote_ps_string(text))
            if trafo.matrix() == IdentityMatrix:
                write('%g %g ' % tuple(trafo.offset()))
            else:
                write('[%g %g %g %g %g %g] ' % trafo.coeff())
            if self.proc_fill:
                write('P ')
                write('gsave clip newpath\n')
                self.properties.ExecuteFill(self, self.pattern_rect)
                write('grestore ')
                if clip:
                    write('clip ')
                write('newpath\n')
            else:
                self.properties.ExecuteFill(self, self.pattern_rect)
                if clip:
                    write('TP clip newpath\n')
                else:
                    write('T\n')

    complex_text = None
    def BeginComplexText(self, clip = 0, cache = None):
        # XXX clip does not work yet...
        self.complex_text = Empty(clip = clip, fontname = '', size = None)
        if self.proc_fill or clip:
            self.PushClip()
        else:
            self.properties.ExecuteFill(self, self.pattern_rect)

    def DrawComplexText(self, text, trafo, font, font_size):
        write = self.file.write
        complex_text = self.complex_text
        self.set_font(font, font_size)
        write('(%s) ' % quote_ps_string(text))
        if trafo.matrix() == IdentityMatrix:
            write('%g %g ' % tuple(trafo.offset()))
        else:
            write('[%g %g %g %g %g %g] ' % trafo.coeff())
        if self.proc_fill:
            write('P\n')
        else:
            write('T\n')

    def EndComplexText(self):
        if self.proc_fill:
            self.file.write('eoclip newpath\n')
            self.properties.ExecuteFill(self, self.pattern_rect)
            self.PopClip()
        self.complex_text = None


    def DrawImage(self, image, trafo, clip = 0):
        write = self.file.write
        w, h = image.size
        if len(image.mode) >= 3:
            # compute number of hex lines. 80 hex digits per line. 3 bytes
            # per pixel
            digits = w * h * 6	# 3 bytes per pixel, 2 digits per byte
            if digits <= 0:
                # an empty image. (it should never be < 0 ...)
                return
            lines = (digits - 1) / 80 + 1
            write('%d %d ' % image.size)
            write('[%g %g %g %g %g %g] true\n' % trafo.coeff())
            write('%%%%BeginData: %d Hex Lines\n' % (lines + 1))
            write('skcimg\n')
        else:
            digits = w * h * 2	# 2 digits per byte
            if digits <= 0:
                # an empty image. (it should never be < 0 ...)
                return
            lines = (digits - 1) / 80 + 1
            write('%d %d ' % image.size)
            write('[%g %g %g %g %g %g] true\n' % trafo.coeff())
            write('%%%%BeginData: %d Hex Lines\n' % (lines + 1))
            write('skgimg\n')

        _sketch.write_ps_hex(image.im, self.file)
        write('%%EndData\n')
        if clip:
            write('pusht [%g %g %g %g %g %g] concat\n' % trafo.coeff())
            write('%d %d scale\n' % image.size)
            write('0 0 m  1 0 l	 1 1 l	0 1 l closepath popt clip\n')


    def DrawEps(self, data, trafo):
        write = self.file.write
        write('%g %g %g %g ' % (data.Start() + data.Size()))
        write('[%g %g %g %g %g %g]\n' % trafo.coeff())
        write('skeps\n')
        write('%%%%BeginDocument: %s\n' % data.Filename())
        if self.loaded_eps_files.has_key(id(data)):
            write(self.loaded_eps_contents)
        else:
            data.WriteLines(self.file)
        write('\n%%EndDocument\n')
        write('skepsend\n')

    def DrawGrid(self, orig_x, orig_y, xwidth, ywidth, rect):
        pass

    def DrawGuideLine(self, *args):
        pass

    def SetProperties(self, properties, rect = None):
        self.properties = properties
        self.line = properties.HasLine()
        self.fill = properties.HasFill()
        self.pattern_rect = rect
        self.proc_fill = properties.IsAlgorithmicFill()
        self.proc_line = properties.IsAlgorithmicLine()


    def write_gradient(self, gradient):
        # self.current_color is implicitly reset because gradients are
        # nested in a PushClip/PopClip, so we don't have to update that
        write = self.file.write
        samples = gradient.Sample(self.gradient_steps)
        write('%d gradient\n' % len(samples))
        last = None
        for color in samples:
            if color != last:
                r, g, b = last = color
                write('%g %g %g $\n' % (round(r, 3), round(g, 3), round(b, 3)))
            else:
                write('!\n')

    has_axial_gradient = 1
    def AxialGradient(self, gradient, p0, p1):
        # must accept p0 and p1 as PointSpecs
        self.write_gradient(gradient)
        self.file.write('%g %g %g %g axial\n' % (tuple(p0) + tuple(p1)))

    has_radial_gradient = 1
    def RadialGradient(self, gradient, p, r0, r1):
        # must accept p as PointSpec
        self.write_gradient(gradient)
        self.file.write('%g %g %g %g radial\n' % (tuple(p) + (r0, r1)))

    has_conical_gradient = 1
    def ConicalGradient(self, gradient, p, angle):
        # must accept p as PointSpec
        self.write_gradient(gradient)
        self.file.write('%g %g %g conical\n' % (tuple(p) + (angle,)))

    def TileImage(self, image, trafo):
        width, height = image.size
        if image.mode == 'RGBA':
            # We don't support transparency in textures, so we simply
            # treat RGBA as as RGB images
            mode = 'RGB'
        else:
            mode = image.mode
        length = width * height * len(mode)
        if length > 65536:
            # the tile image is too large to fit into a PostScript
            # string. Resize it.
            #warn(USER, "Image data to big for tiling, resizing...")
            ratio = float(width) / height
            max_size = 65536 / len(mode)
            width = int(sqrt(max_size * ratio))
            height = int(sqrt(max_size / ratio))
            tile = image.im.resize((width, height))
            length = width * height * len(mode)
            trafo = trafo(Scale(float(image.size[0]) / width,
                                float(image.size[1]) / height))
        else:
            tile = image.im
        write = self.file.write
        write('%d %d %d [%g %g %g %g %g %g]\n'
              % ((width, height, len(mode)) + trafo.coeff()))
        digits = length * 2	# 2 digits per byte
        lines = (digits - 1) / 80 + 1
        write('%%%%BeginData: %d Hex Lines\n' % (lines + 1))
        write("tileimage\n")
        # write_ps_hex treats RGBA like RGB
        _sketch.write_ps_hex(tile, self.file)
        write('%%EndData\n')

    #
    #	Outline Mode
    #	This will be ignored in PostScript devices
    #

    def StartOutlineMode(self, *rest):
        pass

    def EndOutlineMode(self, *rest):
        pass

    def IsOutlineActive(self, *rest):
        return 0
