#!/usr/bin/env python
'''
frame.py
* Puts a frame around an object, using the specified H margin and W/H margins ratio
* This code is modeled after inkscape/extensions/dimension.py, developed in
2007 by Peter Lewerin, peter.lewerin@tele2.se
* It uses the selection's bounding box, so if the bounding box has empty
space in the x- or y-direction (such as with some stars) the results
will look strange. Strokes might also overlap the edge of the
bounding box.
* This code, like dimension.py, contains code from existing effects in the Inkscape
extensions library, and marker data from markers.svg.
'''
# imports:
# : sys
# : copy
# : subprocess (Popen, PIPE)
# : inkex imports:
# : copy (deep copy)
# : gettext (internationalization)
# : optparse (deprecated since python 3.2 but still available)
# : os (popen3, though still available, has a python3 replacement)
# : random (random numbers)
# : re (regular expressions)
# : sys (runtime services)
# : math * (math)
# : etree (from lxml)
# : locale (localization)
#
# : among other things, inkex provides:
# : NSS (namespace URLs)
# : addNS() (prefixes namespace url to tag)
# : xpathSingle() (returns first occurrence of matching node)
# : errormsg() (writes messages to modal box)
import sys, copy
sys.path.append('/usr/share/inkscape/extensions')
try:
from subprocess import Popen, PIPE
gBSPr = True
except:
gBSPr = False
# local library:
import inkex
import pathmodifier
from simpletransform import *
inkex.localize()
import inkex, simpletransform, simplestyle
# oFram (frame)
# : subclasses pathmodifier.PathModifier
# : PathModifier in turn subclasses inkex.Effect
#
class oFram( pathmodifier.PathModifier):
def __init__( s):
inkex.Effect.__init__( s)
s.OptionParser.add_option('-p', '--Page', action='store', type='string', dest='aPage', default='', help='The selected UI-tab when OK was pressed')
s.OptionParser.add_option('-n', '--NewT', action='store', type='string', dest='aNewT', default='rectangle', help='New object type')
s.OptionParser.add_option('-t', '--BTyp', action='store', type='string', dest='aBTyp', default='geometric', help='Bounding box type')
s.OptionParser.add_option('-m', '--Marg', action='store', type='float', dest='aMarg', default=40, help='Vertical margin')
s.OptionParser.add_option('-r', '--WHRa', action='store', type='float', dest='aWHRa', default=2, help='W/H margin ratio')
s.OptionParser.add_option('-c', '--CRad', action='store', type='float', dest='aCRad', default=0, help='Corner radius')
s.OptionParser.add_option('-k', '--StrC', action='store', type='string', dest='aStrC', default='#000000', help='Stroke color (RGB)')
s.OptionParser.add_option('-w', '--StrW', action='store', type='float', dest='aStrW', default=4, help='Stroke thickness')
s.OptionParser.add_option('-f', '--Fill', action='store', type='string', dest='aFill', default='', help='Background fill')
s.OptionParser.add_option('-s', '--Styl', action='store', type='string', dest='aStyl', default='', help='Other style options')
s.OptionParser.add_option('-q', '--Trac', action='store', type='string', dest='aTrac', default='', help='Diagnostic information')
# Values used by fMSel:
s.mMATr = None # First flowPara that contains the text 'Trace:'
s.mMAPa = None # Parent of s.mMATr
s.mMASt = None # Style of s.mMATr
# fMSel: Select message destination and write the message
#
# Input:
# : aMess: The message to post
# : mTrac: The destination, from the inx option
# : none: Discard the message
# : errm: Use the errormsg function; post the message to a modal window
# : text:
# : Find a flowPara element that contains "Trace:" and nothing else; note colon
# : Append the message as a new flowPara in the "Trace:" flowRegion
# : Copy attributes from the "Trace:" element to the new element
#
def fMSel( s, aMess):
while True:
if s.mTrac == 'none': break
if s.mTrac == 'errm':
inkex.errormsg( _('%s' % aMess))
inkex.errormsg( '')
break
if s.mTrac == 'text':
if s.mMAPa == None:
tRoot = s.document.getroot()
# # Test xpath call:
# tElem = tElLi = tRoot.xpath( './/svg:ellipse', namespaces=inkex.NSS)
# if isinstance( tElLi, list) and len( tElLi) > 0: tElem = tElLi[ 0]
# inkex.errormsg( 'fMSel: 2: tElem(%s)' % tElem)
# Find flowPara element that contains the text 'Trace:' and nothing else
tElLi = tRoot.xpath( './/svg:flowPara', namespaces=inkex.NSS)
if not isinstance( tElLi, list): tElLi = [ tElLi]
tMATr = None
for tElNu, tElem in enumerate( tElLi):
tText = tElem.text
# inkex.errormsg( 'fMSel: 3: tText(%s) tElNu(%s) tElem(%s)' % (tText, tElNu, tElem)) # Voluminous!
if tText == 'Trace:':
tMATr = tElem
tMASt = tMATr.get( 'style')
tMASt = simplestyle.parseStyle( tMASt)
# inkex.errormsg( 'fMSel: 4: tMATr(%s)' % tMATr)
s.mMATr = tMATr
s.mMAPa = tMAPa = tMATr.getparent()
s.mMASt = tMASt
# inkex.errormsg( 'fMSel: 5: tMAPa(%s)' % tMAPa)
if tMATr == None:
s.mTrac = 'errm'
inkex.errormsg( _('Trace mode switching to "errm":'))
inkex.errormsg( _('No text area containing only "Trace:" was found'))
continue # Back to the while True
# # Append to existing text
# tText = s.mMATr.text
# tText += aMess
# s.mMATr.text = tText
# Create a new flowPara for the message
tMAPa = s.mMAPa
tMASt = s.mMASt
tMANe = inkex.etree.SubElement( tMAPa, inkex.addNS( 'flowPara', 'svg'), tMASt )
tMANe.text = aMess
tMANe = inkex.etree.SubElement( tMAPa, inkex.addNS( 'flowPara', 'svg'), tMASt ) # Blank line: separator
break
break
# effect:
# : argument 1:
# : Name of temporary file, that contains a copy of the file being edited
# : E.g., '/tmp/ink_ext_XXXXXX.svgONVAQ0'
# : This file is used when "visual" is specified as the bounding-box type:
# : Inkscape, invoked with --query options as a subprocess, reads this
# file and returns the actual dimensions of a text
# : The dimensions of the text are not present in the file; this implies
# that inkscape --query calculates the dimensions of the text; I wonder
# then why it is not possible to invoke the query routines directly
# : s.options, s.args:
# : defined in inkex.Effect
# : output from optparse.ObjectParser.parse_args( sys.argv[ 1:])
# : s.options:
# : ids: list of ids for selected nodes
# : s.selected:
# : selected nodes
#
def effect( s):
s.mTrac = s.options.aTrac
tFill = {
# 'stroke' : '#000000',
# 'stroke-width' : '4',
'fill' : 'none'
}
tBBox = None
tNewT = s.options.aNewT
tMarg = float( s.options.aMarg)
tWHRa = float( s.options.aWHRa)
tCRad = float( s.options.aCRad)
tStrC = s.options.aStrC
tStrW = s.options.aStrW
tFill = s.options.aFill
# Size deltas:
# : Pos value makes frame larger than selected object
# : Neg value makes frame smaller than selected object
tSiDY = tMarg
tSiDX = tSiDY * tWHRa
tScal = s.unittouu('1px') # convert to document units
tSiDY *= tScal
tSiDX *= tScal
# Query inkscape about selection and bounding box type
# * ids: List of selected SVG elements: element id= value
# * selected: Dict of selected elements: key is element id=, value is the SVG element
# * options: inx options plus ids (ids for selected elements)
# inkex.errormsg( 'args(%s)' % (s.args))
# inkex.errormsg( 'ids(%s)' % (s.options.ids))
# inkex.errormsg( 'options(%s)' % (s.options))
# inkex.errormsg( 'selected(%s)' % (s.selected))
s.fMSel( 'args(%s)' % (s.args))
s.fMSel( 'ids(%s)' % (s.options.ids))
s.fMSel( 'options(%s)' % (s.options))
s.fMSel( 'selected(%s)' % (s.selected))
if len( s.options.ids) == 0:
inkex.errormsg( _( 'Please select an object.'))
exit()
tObjP = s.current_layer # Parent?
tFile = s.args[ -1]
# Need visual bounding box? Use --query-all to obtain list for use later
#
tBTyp = s.options.aBTyp
if tBTyp == 'visual':
if gBSPr:
tProc = Popen( 'inkscape --query-all "%s"' % tFile, shell=True, stdout=PIPE, stderr=PIPE)
tRetC = tProc.wait()
tFStR = tProc.stdout.read() # List of all SVG objects in tFile
tErrM = tProc.stderr.read()
else:
tFStO, tFStE = os.popen3( 'inkscape --query-all "%s"' % tFile)[ 1:] # Returns stdin, stdout, stderr
tFStR = tFStO.read()
tFStO.close()
tFStE.close()
tBBLi = tFStR.splitlines()
# inkex.errormsg( 'BBLi: lBBLi(%d)' % len( tBBLi))
s.fMSel( 'BBLi: lBBLi(%d)' % len( tBBLi))
# Process selected items
# : selected: dict, using item id as key and item as value
#
for tNoId, tNoCu in s.selected.iteritems():
# inkex.errormsg( 'Iter: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu)) # Diagnostic
s.fMSel( 'Iter: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu)) # Diagnostic
tBTyp = s.options.aBTyp # Refresh
tElem = None
# duplicate: Get geometric bounding box from a duplicate of the selected object
#
# : This experimental feature is motivated by the need to get
# a geometrical bounding box for a text object;
# : Unfortunately, the size of the duplicate is taken from the text flow area, not from the text
# : Support for this option is minimal
#
if tBTyp == 'duplicate':
tStDi = { 'style': simplestyle.formatStyle( tFill) }
while tElem == None:
if tNewT == 'rectangle': tNoNe = inkex.etree.SubElement( tObjP, inkex.addNS( 'rect', 'svg'), tStDi ); break
if tNewT == 'ellipse': tNoNe = inkex.etree.SubElement( tObjP, inkex.addNS( 'ellipse', 'svg'), tStDi ); break
if tNewT == 'none': tNoNe = tNoCu; break
break;
tNoNe = copy.deepcopy( tNoCu) # Derive next node from curr
tNoId = s.uniqueId( tNoId)
tNoNe.set( 'id', tNoId)
s.current_layer.append( tNoNe)
tNoCu = tNoNe
# inkex.errormsg( 'Dupl: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu))
s.fMSel( 'Dupl: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu))
tBTyp = 'geometric'
# Select process depending on bounding box type:
# : geometric: Use computeBBox() in simpletransform.py to get xmin, xmax, ymin, ymax
# : visual: Use inkscape command-line --query option to obtain viewbox values
tBBDi = { 'x':0, 'y':0, 'width':0, 'height':0} # Bounding box dictionary
while True:
if tBTyp == 'geometric':
s.mBBox = tBBox = computeBBox( [ tNoCu]) # arg is a list of nodes
tBBDi[ 'x'] = tBBox[ 0]
tBBDi[ 'y'] = tBBox[ 2]
tBBDi[ 'width'] = tBBox[ 1] - tBBox[ 0]
tBBDi[ 'height'] = tBBox[ 3] - tBBox[ 2]
break
if tBTyp == 'visual':
for tLine in tBBLi:
if tLine.startswith( tNoId):
tLSpl = tLine.split( ',')
s.mBBox = tBBox = [ tLSpl[ 1], tLSpl[ 1] + tLSpl[ 3], tLSpl[ 2], tLSpl[ 2] + tLSpl[ 4]]
tBBDi[ 'x'] = float( tLSpl[ 1])
tBBDi[ 'y'] = float( tLSpl[ 2])
tBBDi[ 'width'] = float( tLSpl[ 3])
tBBDi[ 'height'] = float( tLSpl[ 4])
break
break # while
# Avoid ugly failure on rects and texts.
#
if tBBox == None or tBBox[ 0] == None:
inkex.errormsg( _( '%s-type bounding box not found for object: Try another bbox type.'))
exit()
# Assemble frame attributes
# : Parse the style for the selected node (tStyl) into a dictionary (tStDi)
# : Copy stroke and stroke-width from dictionary to frame attributes
# : Parse the style specified on the inx panel and add to frame attributes (tFrDi)
# : Explicitly specified stroke and stroke-width override earlier values
# tFrDi = { 'fill': 'none' }
tStyl = tNoCu.get( 'style')
# inkex.errormsg( 'Styl: tStyl(%s)' % tStyl)
s.fMSel( 'Styl: tStyl(%s)' % tStyl)
tStDi = simplestyle.parseStyle( tStyl)
tFrDi = {}
if tStDi.has_key( 'stroke'): tFrDi[ 'stroke'] = str( tStDi[ 'stroke'])
if tStDi.has_key( 'stroke-width'): tFrDi[ 'stroke-width'] = str( tStDi[ 'stroke-width'])
tStyl = s.options.aStyl # style from inx
tStDi = simplestyle.parseStyle( tStyl)
for tStDK, tStDV in tStDi.items():
tFrDi[ tStDK] = tStDV
if tStrC != '': tFrDi[ 'stroke'] = str( tStrC)
if tStrW > 0: tFrDi[ 'stroke-width'] = str( tStrW)
if tFill != '': tFrDi[ 'fill'] = str( tFill)
if not tFrDi.has_key( 'fill'): tFrDi[ 'fill'] = 'none'
while tElem == None:
if tNewT == 'rectangle': tElem = inkex.etree.SubElement( tObjP, inkex.addNS( 'rect', 'svg'), tFrDi ); break
if tNewT == 'ellipse': tElem = inkex.etree.SubElement( tObjP, inkex.addNS( 'ellipse', 'svg'), tFrDi ); break
# if tNewT == 'rectangle': tElem = inkex.etree.SubElement( tElPa, inkex.addNS( 'rect', 'svg'), tFrDi ); break
# if tNewT == 'ellipse': tElem = inkex.etree.SubElement( tElPa, inkex.addNS( 'ellipse', 'svg'), tFrDi ); break
if tNewT == 'none': tElem = tNoCu; break
break;
# If new node is filled, move it behind the selected node
if tFrDi[ 'fill'] != 'none' and tSiDY > 0:
tElPa = tNoCu.getparent()
tElPI = tElPa.index( tNoCu)
tElPa.insert( tElPI, tElem)
# inkex.errormsg( 'Down: tElPI(%s) tElPa( %s)' % (tElPI, tElPa))
s.fMSel( 'Down: tElPI(%s) tElPa( %s)' % (tElPI, tElPa))
# Set the shape and size of the new node
# : rect: simple case, margins constant, no adjustment needed
# : ellipse: margin varies with the curve
# : equation: (x/a)**2 + (y/b)**2 = 1
# : let the size of the selected object be 2s wide and 2t high
# then vertical and horizontal margins (mv and mh) decrease as the curve
# approaches the corner of the selected object
# : at x = 0: y = b and mv = y - t = b - t
# : at y = 0: x = a and mh = x - s = a - s
# : at x = s: y = b * sqrt( ( 1 - (s/a)**2) ) = t + Mv
# : at y = t: x = a * sqrt( ( 1 - (t/b)**2) ) = s + Mh
# : (t + Mv)**2 / b**2 = 1 - (s/a)**2
# : (s + Mh)**2 / a**2 = 1 - (t/b)**2
# : s**2 + (a*t + a*Mv)**2 = (a*b)**2
# : t**2 + (b*s + b*Mh)**2 = (a*b)**2
# : s**2 - t**2 = b**2 * (s+Mh)**2 - a**2 * (t+Mv)**2
# : To solve, we need to assume that a nd b are proportional to s and t
#
while True:
if tElem.tag == inkex.addNS( 'rect', 'svg'):
tFrDi = {}
tFrDi[ 'sy'] = tBBDi[ 'height'] + 2 * tSiDY
tFrDi[ 'sx'] = tBBDi[ 'width'] + 2 * tSiDX
tFrDi[ 'py'] = tBBDi[ 'y'] - tSiDY
tFrDi[ 'px'] = tBBDi[ 'x'] - tSiDX
tElem.set( 'height', str( tFrDi[ 'sy']))
tElem.set( 'width', str( tFrDi[ 'sx']))
tElem.set( 'y', str( tFrDi[ 'py']))
tElem.set( 'x', str( tFrDi[ 'px']))
tElem.set( 'ry', str( tCRad))
tElem.set( 'rx', str( tCRad))
break
if tElem.tag == inkex.addNS( 'ellipse', 'svg'):
tFrDi = {}
tFrDi[ 'ry'] = tBBDi[ 'height'] / 2 + tSiDY
tFrDi[ 'rx'] = tBBDi[ 'width'] / 2 + tSiDX
tFrDi[ 'cy'] = tBBDi[ 'y'] + tBBDi[ 'height'] / 2
tFrDi[ 'cx'] = tBBDi[ 'x'] + tBBDi[ 'width'] / 2
tElem.set( 'ry', str( tFrDi[ 'ry']))
tElem.set( 'rx', str( tFrDi[ 'rx']))
tElem.set( 'cy', str( tFrDi[ 'cy']))
tElem.set( 'cx', str( tFrDi[ 'cx']))
break
break
# if tStrC != '': tElem.set( 'stroke', str( tStrC))
# if tStrW > 0: tElem.set( 'stroke-width', str( tStrW))
if __name__ == '__main__':
tFram = oFram()
tFram.affect()
.91 extension: frame.py: Frames, circles or enlarges selected items
コメントするにはログインしてください!