1#!/usr/bin/python3
2#
3# Legal Stuff:
4#
5# This file is part of the Suru Icon Theme and is free software; you can redistribute it and/or modify it under
6# the terms of the GNU Lesser General Public License as published by the Free Software
7# Foundation; version 3.
8#
9# This file is part of the Suru Icon Theme and is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
12# details.
13#
14# You should have received a copy of the GNU General Public License along with
15# this program; if not, see <https://www.gnu.org/licenses/lgpl-3.0.txt>
16#
17#
18# Thanks to the GNOME icon developers for the original version of this script
19
20import os
21import sys
22import xml.sax
23import subprocess
24import argparse
25
26
27OPTIPNG = "optipng"
28MAINDIR = "../../Suru"
29SOURCES = (
30    "actions",
31    "apps",
32    "categories",
33    "devices",
34    "emblems",
35    "legacy",
36    "mimetypes",
37    "places",
38    "status",
39    "wip",
40)
41
42# DPI multipliers to render at
43DPIS = [1, 2]
44
45
46def main(args, SRC):
47    def optimize_png(png_file):
48        if os.path.exists(OPTIPNG):
49            process = subprocess.Popen([OPTIPNG, "-quiet", "-o7", png_file])
50            process.wait()
51
52    def wait_for_prompt(process, command=None):
53        if command is not None:
54            process.stdin.write((command + "\n").encode("utf-8"))
55
56        # This is kinda ugly ...
57        # Wait for just a '>', or '\n>' if some other char appearead first
58        output = process.stdout.read(1)
59        if output == b">":
60            return
61
62        output += process.stdout.read(1)
63        while output != b"\n>":
64            output += process.stdout.read(1)
65            output = output[1:]
66
67    def inkscape_render_rect(icon_file, rect, dpi, output_file):
68        cmd = [
69            "inkscape",
70            "--batch-process",
71            "--export-dpi={}".format(str(dpi)),
72            "-i",
73            rect,
74            "--export-filename={}".format(output_file),
75            icon_file,
76        ]
77        ret = subprocess.run(cmd, capture_output=True)
78        if ret.returncode != 0:
79            print("execution of")
80            print('  %s' % "".join(cmd))
81            print("returned with error %d" % ret.returncode)
82            print(5*"=", "stdout", 5*"=")
83            print(ret.stdout.decode())
84            print(5*"=", "stderr", 5*"=")
85            print(ret.stderr.decode())
86            return
87
88        optimize_png(output_file)
89
90    class ContentHandler(xml.sax.ContentHandler):
91        ROOT = 0
92        SVG = 1
93        LAYER = 2
94        OTHER = 3
95        TEXT = 4
96
97        def __init__(self, path, force=False, filter=None):
98            self.stack = [self.ROOT]
99            self.inside = [self.ROOT]
100            self.path = path
101            self.rects = []
102            self.state = self.ROOT
103            self.chars = ""
104            self.force = force
105            self.filter = filter
106
107        def endDocument(self):
108            pass
109
110        def startElement(self, name, attrs):
111            if self.inside[-1] == self.ROOT:
112                if name == "svg":
113                    self.stack.append(self.SVG)
114                    self.inside.append(self.SVG)
115                    return
116            elif self.inside[-1] == self.SVG:
117                if (
118                    name == "g"
119                    and ("inkscape:groupmode" in attrs)
120                    and ("inkscape:label" in attrs)
121                    and attrs["inkscape:groupmode"] == "layer"
122                    and attrs["inkscape:label"].startswith("Baseplate")
123                ):
124                    self.stack.append(self.LAYER)
125                    self.inside.append(self.LAYER)
126                    self.context = None
127                    self.icon_name = None
128                    self.rects = []
129                    return
130            elif self.inside[-1] == self.LAYER:
131                if (
132                    name == "text"
133                    and ("inkscape:label" in attrs)
134                    and attrs["inkscape:label"] == "context"
135                ):
136                    self.stack.append(self.TEXT)
137                    self.inside.append(self.TEXT)
138                    self.text = "context"
139                    self.chars = ""
140                    return
141                elif (
142                    name == "text"
143                    and ("inkscape:label" in attrs)
144                    and attrs["inkscape:label"] == "icon-name"
145                ):
146                    self.stack.append(self.TEXT)
147                    self.inside.append(self.TEXT)
148                    self.text = "icon-name"
149                    self.chars = ""
150                    return
151                elif name == "rect":
152                    self.rects.append(attrs)
153
154            self.stack.append(self.OTHER)
155
156        def endElement(self, name):
157            stacked = self.stack.pop()
158            if self.inside[-1] == stacked:
159                self.inside.pop()
160
161            if stacked == self.TEXT and self.text is not None:
162                assert self.text in ["context", "icon-name"]
163                if self.text == "context":
164                    self.context = self.chars
165                elif self.text == "icon-name":
166                    self.icon_name = self.chars
167                self.text = None
168            elif stacked == self.LAYER:
169                assert self.icon_name
170                assert self.context
171
172                if self.filter is not None and not self.icon_name in self.filter:
173                    return
174
175                print(self.context, self.icon_name)
176                for rect in self.rects:
177                    for dpi_factor in DPIS:
178                        width = int(float(rect["width"]))
179                        height = int(float(rect["height"]))
180                        id = rect["id"]
181                        dpi = 96 * dpi_factor
182
183                        size_str = "%sx%s" % (width, height)
184                        if dpi_factor != 1:
185                            size_str += "@%sx" % dpi_factor
186
187                        dir = os.path.join(MAINDIR, size_str, self.context)
188                        outfile = os.path.join(dir, self.icon_name + ".png")
189                        if not os.path.exists(dir):
190                            os.makedirs(dir)
191                        # Do a time based check!
192                        if self.force or not os.path.exists(outfile):
193                            inkscape_render_rect(self.path, id, dpi, outfile)
194                            sys.stdout.write(".")
195                        else:
196                            stat_in = os.stat(self.path)
197                            stat_out = os.stat(outfile)
198                            if stat_in.st_mtime > stat_out.st_mtime:
199                                inkscape_render_rect(self.path, id, dpi, outfile)
200                                sys.stdout.write(".")
201                            else:
202                                sys.stdout.write("-")
203                        sys.stdout.flush()
204                sys.stdout.write("\n")
205                sys.stdout.flush()
206
207        def characters(self, chars):
208            self.chars += chars.strip()
209
210    if not args.svg:
211        print("Rendering all SVGs in", SRC)
212        if not os.path.exists(MAINDIR):
213            os.mkdir(MAINDIR)
214
215        for file in os.listdir(SRC):
216            if file[-4:] == ".svg":
217                file = os.path.join(SRC, file)
218                handler = ContentHandler(file)
219                xml.sax.parse(open(file), handler)
220        print("")
221    else:
222        svg = args.svg + ".svg"
223        file = os.path.join(SRC, svg)
224
225        if os.path.exists(file):
226            print('Rendering SVG "%s" in %s' % (svg, SRC))
227            handler = ContentHandler(file, True, filter=args.filter)
228            xml.sax.parse(open(file), handler)
229        else:
230            print(
231                'Could not find SVG "%s" in %s, looking into the next one' % (svg, SRC)
232            )
233            # icon not in this directory, try the next one
234            pass
235
236
237parser = argparse.ArgumentParser(description="Render icons from SVG to PNG")
238
239parser.add_argument(
240    "svg",
241    type=str,
242    nargs="?",
243    metavar="SVG",
244    help="Optional SVG names (without extensions) to render. If not given, render all icons",
245)
246parser.add_argument(
247    "filter",
248    type=str,
249    nargs="?",
250    metavar="FILTER",
251    help="Optional filter for the SVG file",
252)
253
254args = parser.parse_args()
255
256for source in SOURCES:
257    SRC = os.path.join(".", source)
258    main(args, SRC)
259