1 #!/usr/bin/env python
2 '''
3 replace_font.py
5 Copyright (C) 2010 Craig Marshall, craig9 [at] gmail.com
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 -----------------------
23 This script finds all fonts in the current drawing that match the
24 specified find font, and replaces them with the specified replacement
25 font.
27 It can also replace all fonts indiscriminately, and list all fonts
28 currently being used.
29 '''
31 import os
32 import sys
33 import inkex
34 import simplestyle
35 import gettext
36 _ = gettext.gettext
38 text_tags = ['{http://www.w3.org/2000/svg}tspan',
39 '{http://www.w3.org/2000/svg}text',
40 '{http://www.w3.org/2000/svg}flowRoot',
41 '{http://www.w3.org/2000/svg}flowPara',
42 '{http://www.w3.org/2000/svg}flowSpan']
43 font_attributes = ['font-family', '-inkscape-font-specification']
45 def set_font(node, new_font, style=None):
46 '''
47 Sets the font attribute in the style attribute of node, using the
48 font name stored in new_font. If the style dict is open already,
49 it can be passed in, otherwise it will be optned anyway.
51 Returns a dirty boolean flag
52 '''
53 dirty = False
54 if not style:
55 style = get_style(node)
56 if style:
57 for att in font_attributes:
58 if att in style:
59 style[att] = new_font
60 set_style(node, style)
61 dirty = True
62 return dirty
64 def find_replace_font(node, find, replace):
65 '''
66 Searches the relevant font attributes/styles of node for find, and
67 replaces them with replace.
69 Returns a dirty boolean flag
70 '''
71 dirty = False
72 style = get_style(node)
73 if style:
74 for att in font_attributes:
75 if att in style and style[att].strip().lower() == find:
76 set_font(node, replace, style)
77 dirty = True
78 return dirty
80 def is_styled_text(node):
81 '''
82 Returns true if the tag in question is a "styled" element that
83 can hold text.
84 '''
85 return node.tag in text_tags and 'style' in node.attrib
87 def is_text(node):
88 '''
89 Returns true if the tag in question is an element that
90 can hold text.
91 '''
92 return node.tag in text_tags
95 def get_style(node):
96 '''
97 Sugar coated way to get style dict from a node
98 '''
99 if 'style' in node.attrib:
100 return simplestyle.parseStyle(node.attrib['style'])
102 def set_style(node, style):
103 '''
104 Sugar coated way to set the style dict, for node
105 '''
106 node.attrib['style'] = simplestyle.formatStyle(style)
108 def get_fonts(node):
109 '''
110 Given a node, returns a list containing all the fonts that
111 the node is using.
112 '''
113 fonts = []
114 s = get_style(node)
115 if not s:
116 return fonts
117 for a in font_attributes:
118 if a in s:
119 fonts.append(s[a])
120 return fonts
122 def die(msg = "Dying!"):
123 inkex.errormsg(msg)
124 sys.exit(0)
126 def report_replacements(num):
127 '''
128 Sends a message to the end user showing success of failure
129 of the font replacement
130 '''
131 if num == 0:
132 die(_('Couldn\'t find anything using that font, please ensure the spelling and spacing is correct.'))
134 def report_findings(findings):
135 '''
136 Tells the user which fonts were found, if any
137 '''
138 if len(findings) == 0:
139 inkex.errormsg(_("Didn't find any fonts in this document/selection."))
140 else:
141 if len(findings) == 1:
142 inkex.errormsg(_("Found the following font only: %s") % findings[0])
143 else:
144 inkex.errormsg(_("Found the following fonts:\n%s") % '\n'.join(findings))
146 class ReplaceFont(inkex.Effect):
147 '''
148 Replaces all instances of one font with another
149 '''
150 def __init__(self):
151 inkex.Effect.__init__(self)
152 self.OptionParser.add_option("--fr_find", action="store",
153 type="string", dest="fr_find",
154 default=None, help="")
156 self.OptionParser.add_option("--fr_replace", action="store",
157 type="string", dest="fr_replace",
158 default=None, help="")
160 self.OptionParser.add_option("--r_replace", action="store",
161 type="string", dest="r_replace",
162 default=None, help="")
164 self.OptionParser.add_option("--action", action="store",
165 type="string", dest="action",
166 default=None, help="")
168 self.OptionParser.add_option("--scope", action="store",
169 type="string", dest="scope",
170 default=None, help="")
172 def find_child_text_items(self, node):
173 '''
174 Recursive method for appending all text-type elements
175 to self.selected_items
176 '''
177 if is_text(node):
178 self.selected_items.append(node)
179 for child in node:
180 self.find_child_text_items(child)
182 def relevant_items(self, scope):
183 '''
184 Depending on the scope, returns all text elements, or all
185 selected text elements including nested children
186 '''
187 items = []
188 to_return = []
189 if scope == "selection_only":
190 self.selected_items = []
191 for item in self.selected.iteritems():
192 self.find_child_text_items(item[1])
193 items = self.selected_items
194 if len(items) == 0:
195 die(_("There was nothing selected"))
196 else:
197 items = self.document.getroot().getiterator()
198 to_return.extend(filter(is_text, items))
199 return to_return
201 def find_replace(self, nodes, find, replace):
202 '''
203 Walks through nodes, replacing fonts as it goes according
204 to find and replace
205 '''
206 replacements = 0
207 for node in nodes:
208 if find_replace_font(node, find, replace):
209 replacements += 1
210 report_replacements(replacements)
212 def replace_all(self, nodes, replace):
213 '''
214 Walks through nodes, setting fonts indiscriminately.
215 '''
216 replacements = 0
217 for node in nodes:
218 if set_font(node, replace):
219 replacements += 1
220 report_replacements(replacements)
222 def list_all(self, nodes):
223 '''
224 Walks through nodes, building a list of all fonts found, then
225 reports to the user with that list
226 '''
227 fonts_found = []
228 for node in nodes:
229 for f in get_fonts(node):
230 if not f in fonts_found:
231 fonts_found.append(f)
232 report_findings(sorted(fonts_found))
234 def effect(self):
235 action = self.options.action.strip("\"") # TODO Is this a bug? (Extra " characters)
236 scope = self.options.scope
238 relevant_items = self.relevant_items(scope)
240 if action == "find_replace":
241 find = self.options.fr_find
242 if find is None or find == "":
243 die(_("Please enter a search string in the find box."));
244 find = find.strip().lower()
245 replace = self.options.fr_replace
246 if replace is None or replace == "":
247 die(_("Please enter a replacement font in the replace with box."));
248 self.find_replace(relevant_items, find, replace)
249 elif action == "replace_all":
250 replace = self.options.r_replace
251 if replace is None or replace == "":
252 die(_("Please enter a replacement font in the replace all box."));
253 self.replace_all(relevant_items, replace)
254 elif action == "list_only":
255 self.list_all(relevant_items)
256 sys.exit(0)
258 if __name__ == "__main__":
259 e = ReplaceFont()
260 e.affect()