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