1. #!/usr/bin/env python
  2. '''
  3. frame.py
  4. * Puts a frame around an object, using the specified H margin and W/H margins ratio
  5. * This code is modeled after inkscape/extensions/dimension.py, developed in
  6. 2007 by Peter Lewerin, peter.lewerin@tele2.se
  7. * It uses the selection's bounding box, so if the bounding box has empty
  8. space in the x- or y-direction (such as with some stars) the results
  9. will look strange. Strokes might also overlap the edge of the
  10. bounding box.
  11. * This code, like dimension.py, contains code from existing effects in the Inkscape
  12. extensions library, and marker data from markers.svg.
  13. '''
  14. # imports:
  15. # : sys
  16. # : copy
  17. # : subprocess (Popen, PIPE)
  18. # : inkex imports:
  19. # : copy (deep copy)
  20. # : gettext (internationalization)
  21. # : optparse (deprecated since python 3.2 but still available)
  22. # : os (popen3, though still available, has a python3 replacement)
  23. # : random (random numbers)
  24. # : re (regular expressions)
  25. # : sys (runtime services)
  26. # : math * (math)
  27. # : etree (from lxml)
  28. # : locale (localization)
  29. #
  30. # : among other things, inkex provides:
  31. # : NSS (namespace URLs)
  32. # : addNS() (prefixes namespace url to tag)
  33. # : xpathSingle() (returns first occurrence of matching node)
  34. # : errormsg() (writes messages to modal box)
  35. import sys, copy
  36. sys.path.append('/usr/share/inkscape/extensions')
  37. try:
  38. from subprocess import Popen, PIPE
  39. gBSPr = True
  40. except:
  41. gBSPr = False
  42. # local library:
  43. import inkex
  44. import pathmodifier
  45. from simpletransform import *
  46. inkex.localize()
  47. import inkex, simpletransform, simplestyle
  48. # oFram (frame)
  49. # : subclasses pathmodifier.PathModifier
  50. # : PathModifier in turn subclasses inkex.Effect
  51. #
  52. class oFram( pathmodifier.PathModifier):
  53. def __init__( s):
  54. inkex.Effect.__init__( s)
  55. s.OptionParser.add_option('-p', '--Page', action='store', type='string', dest='aPage', default='', help='The selected UI-tab when OK was pressed')
  56. s.OptionParser.add_option('-n', '--NewT', action='store', type='string', dest='aNewT', default='rectangle', help='New object type')
  57. s.OptionParser.add_option('-t', '--BTyp', action='store', type='string', dest='aBTyp', default='geometric', help='Bounding box type')
  58. s.OptionParser.add_option('-m', '--Marg', action='store', type='float', dest='aMarg', default=40, help='Vertical margin')
  59. s.OptionParser.add_option('-r', '--WHRa', action='store', type='float', dest='aWHRa', default=2, help='W/H margin ratio')
  60. s.OptionParser.add_option('-c', '--CRad', action='store', type='float', dest='aCRad', default=0, help='Corner radius')
  61. s.OptionParser.add_option('-k', '--StrC', action='store', type='string', dest='aStrC', default='#000000', help='Stroke color (RGB)')
  62. s.OptionParser.add_option('-w', '--StrW', action='store', type='float', dest='aStrW', default=4, help='Stroke thickness')
  63. s.OptionParser.add_option('-f', '--Fill', action='store', type='string', dest='aFill', default='', help='Background fill')
  64. s.OptionParser.add_option('-s', '--Styl', action='store', type='string', dest='aStyl', default='', help='Other style options')
  65. s.OptionParser.add_option('-q', '--Trac', action='store', type='string', dest='aTrac', default='', help='Diagnostic information')
  66. # Values used by fMSel:
  67. s.mMATr = None # First flowPara that contains the text 'Trace:'
  68. s.mMAPa = None # Parent of s.mMATr
  69. s.mMASt = None # Style of s.mMATr
  70. # fMSel: Select message destination and write the message
  71. #
  72. # Input:
  73. # : aMess: The message to post
  74. # : mTrac: The destination, from the inx option
  75. # : none: Discard the message
  76. # : errm: Use the errormsg function; post the message to a modal window
  77. # : text:
  78. # : Find a flowPara element that contains "Trace:" and nothing else; note colon
  79. # : Append the message as a new flowPara in the "Trace:" flowRegion
  80. # : Copy attributes from the "Trace:" element to the new element
  81. #
  82. def fMSel( s, aMess):
  83. while True:
  84. if s.mTrac == 'none': break
  85. if s.mTrac == 'errm':
  86. inkex.errormsg( _('%s' % aMess))
  87. inkex.errormsg( '')
  88. break
  89. if s.mTrac == 'text':
  90. if s.mMAPa == None:
  91. tRoot = s.document.getroot()
  92. # # Test xpath call:
  93. # tElem = tElLi = tRoot.xpath( './/svg:ellipse', namespaces=inkex.NSS)
  94. # if isinstance( tElLi, list) and len( tElLi) > 0: tElem = tElLi[ 0]
  95. # inkex.errormsg( 'fMSel: 2: tElem(%s)' % tElem)
  96. # Find flowPara element that contains the text 'Trace:' and nothing else
  97. tElLi = tRoot.xpath( './/svg:flowPara', namespaces=inkex.NSS)
  98. if not isinstance( tElLi, list): tElLi = [ tElLi]
  99. tMATr = None
  100. for tElNu, tElem in enumerate( tElLi):
  101. tText = tElem.text
  102. # inkex.errormsg( 'fMSel: 3: tText(%s) tElNu(%s) tElem(%s)' % (tText, tElNu, tElem)) # Voluminous!
  103. if tText == 'Trace:':
  104. tMATr = tElem
  105. tMASt = tMATr.get( 'style')
  106. tMASt = simplestyle.parseStyle( tMASt)
  107. # inkex.errormsg( 'fMSel: 4: tMATr(%s)' % tMATr)
  108. s.mMATr = tMATr
  109. s.mMAPa = tMAPa = tMATr.getparent()
  110. s.mMASt = tMASt
  111. # inkex.errormsg( 'fMSel: 5: tMAPa(%s)' % tMAPa)
  112. if tMATr == None:
  113. s.mTrac = 'errm'
  114. inkex.errormsg( _('Trace mode switching to "errm":'))
  115. inkex.errormsg( _('No text area containing only "Trace:" was found'))
  116. continue # Back to the while True
  117. # # Append to existing text
  118. # tText = s.mMATr.text
  119. # tText += aMess
  120. # s.mMATr.text = tText
  121. # Create a new flowPara for the message
  122. tMAPa = s.mMAPa
  123. tMASt = s.mMASt
  124. tMANe = inkex.etree.SubElement( tMAPa, inkex.addNS( 'flowPara', 'svg'), tMASt )
  125. tMANe.text = aMess
  126. tMANe = inkex.etree.SubElement( tMAPa, inkex.addNS( 'flowPara', 'svg'), tMASt ) # Blank line: separator
  127. break
  128. break
  129. # effect:
  130. # : argument 1:
  131. # : Name of temporary file, that contains a copy of the file being edited
  132. # : E.g., '/tmp/ink_ext_XXXXXX.svgONVAQ0'
  133. # : This file is used when "visual" is specified as the bounding-box type:
  134. # : Inkscape, invoked with --query options as a subprocess, reads this
  135. # file and returns the actual dimensions of a text
  136. # : The dimensions of the text are not present in the file; this implies
  137. # that inkscape --query calculates the dimensions of the text; I wonder
  138. # then why it is not possible to invoke the query routines directly
  139. # : s.options, s.args:
  140. # : defined in inkex.Effect
  141. # : output from optparse.ObjectParser.parse_args( sys.argv[ 1:])
  142. # : s.options:
  143. # : ids: list of ids for selected nodes
  144. # : s.selected:
  145. # : selected nodes
  146. #
  147. def effect( s):
  148. s.mTrac = s.options.aTrac
  149. tFill = {
  150. # 'stroke' : '#000000',
  151. # 'stroke-width' : '4',
  152. 'fill' : 'none'
  153. }
  154. tBBox = None
  155. tNewT = s.options.aNewT
  156. tMarg = float( s.options.aMarg)
  157. tWHRa = float( s.options.aWHRa)
  158. tCRad = float( s.options.aCRad)
  159. tStrC = s.options.aStrC
  160. tStrW = s.options.aStrW
  161. tFill = s.options.aFill
  162. # Size deltas:
  163. # : Pos value makes frame larger than selected object
  164. # : Neg value makes frame smaller than selected object
  165. tSiDY = tMarg
  166. tSiDX = tSiDY * tWHRa
  167. tScal = s.unittouu('1px') # convert to document units
  168. tSiDY *= tScal
  169. tSiDX *= tScal
  170. # Query inkscape about selection and bounding box type
  171. # * ids: List of selected SVG elements: element id= value
  172. # * selected: Dict of selected elements: key is element id=, value is the SVG element
  173. # * options: inx options plus ids (ids for selected elements)
  174. # inkex.errormsg( 'args(%s)' % (s.args))
  175. # inkex.errormsg( 'ids(%s)' % (s.options.ids))
  176. # inkex.errormsg( 'options(%s)' % (s.options))
  177. # inkex.errormsg( 'selected(%s)' % (s.selected))
  178. s.fMSel( 'args(%s)' % (s.args))
  179. s.fMSel( 'ids(%s)' % (s.options.ids))
  180. s.fMSel( 'options(%s)' % (s.options))
  181. s.fMSel( 'selected(%s)' % (s.selected))
  182. if len( s.options.ids) == 0:
  183. inkex.errormsg( _( 'Please select an object.'))
  184. exit()
  185. tObjP = s.current_layer # Parent?
  186. tFile = s.args[ -1]
  187. # Need visual bounding box? Use --query-all to obtain list for use later
  188. #
  189. tBTyp = s.options.aBTyp
  190. if tBTyp == 'visual':
  191. if gBSPr:
  192. tProc = Popen( 'inkscape --query-all "%s"' % tFile, shell=True, stdout=PIPE, stderr=PIPE)
  193. tRetC = tProc.wait()
  194. tFStR = tProc.stdout.read() # List of all SVG objects in tFile
  195. tErrM = tProc.stderr.read()
  196. else:
  197. tFStO, tFStE = os.popen3( 'inkscape --query-all "%s"' % tFile)[ 1:] # Returns stdin, stdout, stderr
  198. tFStR = tFStO.read()
  199. tFStO.close()
  200. tFStE.close()
  201. tBBLi = tFStR.splitlines()
  202. # inkex.errormsg( 'BBLi: lBBLi(%d)' % len( tBBLi))
  203. s.fMSel( 'BBLi: lBBLi(%d)' % len( tBBLi))
  204. # Process selected items
  205. # : selected: dict, using item id as key and item as value
  206. #
  207. for tNoId, tNoCu in s.selected.iteritems():
  208. # inkex.errormsg( 'Iter: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu)) # Diagnostic
  209. s.fMSel( 'Iter: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu)) # Diagnostic
  210. tBTyp = s.options.aBTyp # Refresh
  211. tElem = None
  212. # duplicate: Get geometric bounding box from a duplicate of the selected object
  213. #
  214. # : This experimental feature is motivated by the need to get
  215. # a geometrical bounding box for a text object;
  216. # : Unfortunately, the size of the duplicate is taken from the text flow area, not from the text
  217. # : Support for this option is minimal
  218. #
  219. if tBTyp == 'duplicate':
  220. tStDi = { 'style': simplestyle.formatStyle( tFill) }
  221. while tElem == None:
  222. if tNewT == 'rectangle': tNoNe = inkex.etree.SubElement( tObjP, inkex.addNS( 'rect', 'svg'), tStDi ); break
  223. if tNewT == 'ellipse': tNoNe = inkex.etree.SubElement( tObjP, inkex.addNS( 'ellipse', 'svg'), tStDi ); break
  224. if tNewT == 'none': tNoNe = tNoCu; break
  225. break;
  226. tNoNe = copy.deepcopy( tNoCu) # Derive next node from curr
  227. tNoId = s.uniqueId( tNoId)
  228. tNoNe.set( 'id', tNoId)
  229. s.current_layer.append( tNoNe)
  230. tNoCu = tNoNe
  231. # inkex.errormsg( 'Dupl: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu))
  232. s.fMSel( 'Dupl: tNoId(%s) tNoCu(%s)' % ( tNoId, tNoCu))
  233. tBTyp = 'geometric'
  234. # Select process depending on bounding box type:
  235. # : geometric: Use computeBBox() in simpletransform.py to get xmin, xmax, ymin, ymax
  236. # : visual: Use inkscape command-line --query option to obtain viewbox values
  237. tBBDi = { 'x':0, 'y':0, 'width':0, 'height':0} # Bounding box dictionary
  238. while True:
  239. if tBTyp == 'geometric':
  240. s.mBBox = tBBox = computeBBox( [ tNoCu]) # arg is a list of nodes
  241. tBBDi[ 'x'] = tBBox[ 0]
  242. tBBDi[ 'y'] = tBBox[ 2]
  243. tBBDi[ 'width'] = tBBox[ 1] - tBBox[ 0]
  244. tBBDi[ 'height'] = tBBox[ 3] - tBBox[ 2]
  245. break
  246. if tBTyp == 'visual':
  247. for tLine in tBBLi:
  248. if tLine.startswith( tNoId):
  249. tLSpl = tLine.split( ',')
  250. s.mBBox = tBBox = [ tLSpl[ 1], tLSpl[ 1] + tLSpl[ 3], tLSpl[ 2], tLSpl[ 2] + tLSpl[ 4]]
  251. tBBDi[ 'x'] = float( tLSpl[ 1])
  252. tBBDi[ 'y'] = float( tLSpl[ 2])
  253. tBBDi[ 'width'] = float( tLSpl[ 3])
  254. tBBDi[ 'height'] = float( tLSpl[ 4])
  255. break
  256. break # while
  257. # Avoid ugly failure on rects and texts.
  258. #
  259. if tBBox == None or tBBox[ 0] == None:
  260. inkex.errormsg( _( '%s-type bounding box not found for object: Try another bbox type.'))
  261. exit()
  262. # Assemble frame attributes
  263. # : Parse the style for the selected node (tStyl) into a dictionary (tStDi)
  264. # : Copy stroke and stroke-width from dictionary to frame attributes
  265. # : Parse the style specified on the inx panel and add to frame attributes (tFrDi)
  266. # : Explicitly specified stroke and stroke-width override earlier values
  267. # tFrDi = { 'fill': 'none' }
  268. tStyl = tNoCu.get( 'style')
  269. # inkex.errormsg( 'Styl: tStyl(%s)' % tStyl)
  270. s.fMSel( 'Styl: tStyl(%s)' % tStyl)
  271. tStDi = simplestyle.parseStyle( tStyl)
  272. tFrDi = {}
  273. if tStDi.has_key( 'stroke'): tFrDi[ 'stroke'] = str( tStDi[ 'stroke'])
  274. if tStDi.has_key( 'stroke-width'): tFrDi[ 'stroke-width'] = str( tStDi[ 'stroke-width'])
  275. tStyl = s.options.aStyl # style from inx
  276. tStDi = simplestyle.parseStyle( tStyl)
  277. for tStDK, tStDV in tStDi.items():
  278. tFrDi[ tStDK] = tStDV
  279. if tStrC != '': tFrDi[ 'stroke'] = str( tStrC)
  280. if tStrW > 0: tFrDi[ 'stroke-width'] = str( tStrW)
  281. if tFill != '': tFrDi[ 'fill'] = str( tFill)
  282. if not tFrDi.has_key( 'fill'): tFrDi[ 'fill'] = 'none'
  283. while tElem == None:
  284. if tNewT == 'rectangle': tElem = inkex.etree.SubElement( tObjP, inkex.addNS( 'rect', 'svg'), tFrDi ); break
  285. if tNewT == 'ellipse': tElem = inkex.etree.SubElement( tObjP, inkex.addNS( 'ellipse', 'svg'), tFrDi ); break
  286. # if tNewT == 'rectangle': tElem = inkex.etree.SubElement( tElPa, inkex.addNS( 'rect', 'svg'), tFrDi ); break
  287. # if tNewT == 'ellipse': tElem = inkex.etree.SubElement( tElPa, inkex.addNS( 'ellipse', 'svg'), tFrDi ); break
  288. if tNewT == 'none': tElem = tNoCu; break
  289. break;
  290. # If new node is filled, move it behind the selected node
  291. if tFrDi[ 'fill'] != 'none' and tSiDY > 0:
  292. tElPa = tNoCu.getparent()
  293. tElPI = tElPa.index( tNoCu)
  294. tElPa.insert( tElPI, tElem)
  295. # inkex.errormsg( 'Down: tElPI(%s) tElPa( %s)' % (tElPI, tElPa))
  296. s.fMSel( 'Down: tElPI(%s) tElPa( %s)' % (tElPI, tElPa))
  297. # Set the shape and size of the new node
  298. # : rect: simple case, margins constant, no adjustment needed
  299. # : ellipse: margin varies with the curve
  300. # : equation: (x/a)**2 + (y/b)**2 = 1
  301. # : let the size of the selected object be 2s wide and 2t high
  302. # then vertical and horizontal margins (mv and mh) decrease as the curve
  303. # approaches the corner of the selected object
  304. # : at x = 0: y = b and mv = y - t = b - t
  305. # : at y = 0: x = a and mh = x - s = a - s
  306. # : at x = s: y = b * sqrt( ( 1 - (s/a)**2) ) = t + Mv
  307. # : at y = t: x = a * sqrt( ( 1 - (t/b)**2) ) = s + Mh
  308. # : (t + Mv)**2 / b**2 = 1 - (s/a)**2
  309. # : (s + Mh)**2 / a**2 = 1 - (t/b)**2
  310. # : s**2 + (a*t + a*Mv)**2 = (a*b)**2
  311. # : t**2 + (b*s + b*Mh)**2 = (a*b)**2
  312. # : s**2 - t**2 = b**2 * (s+Mh)**2 - a**2 * (t+Mv)**2
  313. # : To solve, we need to assume that a nd b are proportional to s and t
  314. #
  315. while True:
  316. if tElem.tag == inkex.addNS( 'rect', 'svg'):
  317. tFrDi = {}
  318. tFrDi[ 'sy'] = tBBDi[ 'height'] + 2 * tSiDY
  319. tFrDi[ 'sx'] = tBBDi[ 'width'] + 2 * tSiDX
  320. tFrDi[ 'py'] = tBBDi[ 'y'] - tSiDY
  321. tFrDi[ 'px'] = tBBDi[ 'x'] - tSiDX
  322. tElem.set( 'height', str( tFrDi[ 'sy']))
  323. tElem.set( 'width', str( tFrDi[ 'sx']))
  324. tElem.set( 'y', str( tFrDi[ 'py']))
  325. tElem.set( 'x', str( tFrDi[ 'px']))
  326. tElem.set( 'ry', str( tCRad))
  327. tElem.set( 'rx', str( tCRad))
  328. break
  329. if tElem.tag == inkex.addNS( 'ellipse', 'svg'):
  330. tFrDi = {}
  331. tFrDi[ 'ry'] = tBBDi[ 'height'] / 2 + tSiDY
  332. tFrDi[ 'rx'] = tBBDi[ 'width'] / 2 + tSiDX
  333. tFrDi[ 'cy'] = tBBDi[ 'y'] + tBBDi[ 'height'] / 2
  334. tFrDi[ 'cx'] = tBBDi[ 'x'] + tBBDi[ 'width'] / 2
  335. tElem.set( 'ry', str( tFrDi[ 'ry']))
  336. tElem.set( 'rx', str( tFrDi[ 'rx']))
  337. tElem.set( 'cy', str( tFrDi[ 'cy']))
  338. tElem.set( 'cx', str( tFrDi[ 'cx']))
  339. break
  340. break
  341. # if tStrC != '': tElem.set( 'stroke', str( tStrC))
  342. # if tStrW > 0: tElem.set( 'stroke-width', str( tStrW))
  343. if __name__ == '__main__':
  344. tFram = oFram()
  345. tFram.affect()
  1. More ...
 
 

733

Frame

.91 extension: frame.py: Frames, circles or enlarges selected items

Inkscape Extensions

Lines
437
Words
2211
Size
16.3 KB
Created
Type
text/x-python
Public Domain (PD)
Please log in to leave a comment!