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