pixelsnap.py revision 796977f97e351f01d3e904ec89b99f0c2eb33d11
"""
TODO: This only snaps selected elements, and if those elements are part of a
group or layer that has it's own transform, that won't be taken into
account, unless you snap the group or layer as a whole. This can account
for unexpected results in some cases (eg where you've got a non-integer
translation on the layer you're working in, the elements in that layer
won't snap properly). The workaround for now is to snap the whole
I could fix it in the code by traversing the parent elements up to the
document root & calculating the cumulative parent_transform. This could
be done at the top of the pixel_snap method if parent_transform==None,
or before calling it for the first time.
TODO: Transforming points isn't quite perfect, to say the least. In particular,
when translating a point bezier curve, we translate the handles by the same amount.
BUT, some handles that are attached to a particular point are conceptually
Best way to fix it would be to keep a list of the fractional_offsets[] of
each point, without transforming anything. Then go thru each point and
transform the appropriate handle according to the relevant fraction_offset
in the list.
i.e. calculate first, then modify.
In fact, that might be a simpler algorithm anyway -- it avoids having
TODO: make elem_offset return [x_offset, y_offset] so we can handle non-symetric scaling
------------
Note: This doesn't work very well on paths which have both straight segments
and curved segments.
The biggest three problems are:
a) we don't take handles into account (segments where the nodes are
aligned are always treated as straight segments, even where the
handles make it curve)
doesn't make any attempt to keep the transition from the straight
segment to the curve smooth.
c) no attempt is made to keep equal widths equal. (or nearly-equal
widths nearly-equal). For example, font strokes.
I guess that amounts to the problyem that font hinting solves for fonts.
I wonder if I could find an automatic font-hinting algorithm and munge
it to my purposes?
Some good autohinting concepts that may help:
Note: Paths that have curves & arcs on some sides of the bounding box won't
be snapped correctly on that side of the bounding box, and nor will they
be translated/resized correctly before the path is modified. Doesn't affect
most applications of this extension, but it highlights the fact that we
take a geometrically simplistic approach to inspecting & modifying the path.
"""
from __future__ import division
import sys
# *** numpy causes issue #4 on Mac OS 10.6.2. I use it for
# matrix inverse -- my linear algebra's a bit rusty, but I could implement my
# own matrix inverse function if necessary, I guess.
# INKEX MODULE
# If you get the "No module named inkex" error, uncomment the relevant line
# below by removing the '#' at the start of the line.
#
#sys.path += ['/usr/share/inkscape/extensions'] # If you're using a standard Linux installation
#sys.path += ['/usr/local/share/inkscape/extensions'] # If you're using a custom Linux installation
#sys.path += ['C:\\Program Files\\Inkscape\\share\\extensions'] # If you're using a standard Windows installation
try:
import inkex
except ImportError:
raise ImportError("No module named inkex.\nPlease edit the file %s and see the section titled 'INKEX MODULE'" % __file__)
class TransformError(Exception): pass
for m in matches:
return False
def invert_transform(transform):
return inverse
""" Better than simpletransform.applyTransformToPoint,
a) coz it's a simpler name
b) coz it returns the new xy, rather than modifying the input
"""
if inverse:
return x,y
makes in this context, but we currently ignore anything besides scale.
"""
return True
elif vlen==0:
return False
return True
elif hlen==0:
return False
""" Returns a value which is the amount the
bounding-box is offset due to the stroke-width.
Transform is taken into account.
"""
raise TransformError("Selection contains non-symetric scaling") # *** wouldn't be hard to get around this by calculating vertical_offset & horizontal_offset separately, maybe 2 functions, or maybe returning a tuple
return (stroke_width/2)
""" Return stroke-width in pixels, untransformed
"""
stroke_width = 0
if setval:
else:
return stroke_width
raise TransformError("Selection contains non-symetric scaling, can't snap stroke width")
if stroke_width:
""" Gets this element's transform. Use setval=matrix to
set this element's transform.
You can only specify parent_transform when getting.
"""
if transform:
else:
if parent_transform:
if setval:
else:
return transform
# Only snaps the x/y translation of the transform, nothing else.
# Scale transforms are handled only in snap_rect()
# Doesn't take any parent_transform into account -- assumes
# that the parent's transform has already been snapped.
""" Modifies a segment so that every point is transformed, including handles
"""
if segtype == 'z': return
elif segtype == 'h':
elif segtype == 'v':
else:
first_coordinate = 0
if (segtype == 'a'): first_coordinate = 5 # for elliptical arcs, skip the radius x/y, rotation, large-arc, and sweep
x, y = transform_point(transform, (x, y))
path[i][1][j] = x
""" Return the endpoint of the given path segment.
Inspects the segment type to know which elements are the endpoints.
"""
x = y = 0
if segtype == 'h':
elif segtype == 'v':
else:
else:
if setval is None: return [x, y]
""" Returns [min_x, min_y], [max_x, max_y] of the transformed
element. (It doesn't make any sense to return the untransformed
bounding box, with the intent of transforming it later, because
The returned bounding box includes stroke-width offset.
This function uses a simplistic algorithm & doesn't take curves
or arcs into account, just node positions.
"""
# If we have a Live Path Effect, modify original-d. If anyone clamours
# for it, we could make an option to ignore paths with Live Path Effects
x, y = transform_point(transform, (x, y))
if i == 0:
else:
# If we have a Live Path Effect, modify original-d. If anyone clamours
# for it, we could make an option to ignore paths with Live Path Effects
# In case somebody tries to snap a 0-high element,
# because we should always check for divide-by-zero!
path, i)
# If we have a Live Path Effect, modify original-d. If anyone clamours
# for it, we could make an option to ignore paths with Live Path Effects
fractional_offset = transform_dimensions(transform, fractional_offset[0], fractional_offset[1], inverse=True)
path, i)
# If we have a Live Path Effect, modify original-d. If anyone clamours
# for it, we could make an option to ignore paths with Live Path Effects
raise TransformError("Selection contains transformations with skew/rotation")
if segtype == 'z':
else:
continue
if len(path) > 2 or i==0: # on 2-point paths, first.next==first.prev==last and last.next==last.prev==first
if on_vertical:
if on_horizontal:
fractional_offset = transform_dimensions(transform, fractional_offset[0], fractional_offset[1], inverse=True)
path, i)
raise TransformError("Selection contains transformations with skew/rotation")
x, y = transform_point(transform, [x, y])
# Snap to the nearest pixel
x = round(x - offset) + offset # If there's a stroke of non-even width, it's shifted by half a pixel
# Position the elem at the newly calculate values
for e in elem:
try:
except TransformError, e:
return
return
try:
except TransformError, e:
self.snap_path(elem, parent_transform) # would be quite useful to make this an option, as scale/pos alone doesn't mess with the path itself, and works well for sans-serif text
self.document_offset = unittouu(svg.attrib['height']) % 1 # although SVG units are absolute, the elements are positioned relative to the top of the page, rather than zero
try:
except TransformError, e:
if __name__ == '__main__':