# 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
|