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