File indexing completed on 2024-12-01 03:38:19
0001 #!/usr/bin/env python3 0002 """ 0003 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0004 SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com> 0005 SPDX-FileCopyrightText: 2020 Niccolò Venerandi <niccolo@venerandi.com> 0006 """ 0007 import os 0008 import re 0009 import sys 0010 from pathlib import Path 0011 from lxml import etree 0012 """ 0013 This script generates 24px icons based on 22px icons 0014 """ 0015 0016 # The BEGIN/END stuff is a Kate/KDevelop feature. Please don't remove it unless you have a good reason. 0017 0018 # BEGIN globals 0019 0020 # These are needed to prevent nonsense namespaces like ns0 from being 0021 # added to otherwise perfectly fine svg elements and attributes 0022 NAMESPACES = { 0023 "svg": "http://www.w3.org/2000/svg", 0024 "xlink": "http://www.w3.org/1999/xlink", 0025 "inkscape": "http://www.inkscape.org/namespaces/inkscape", 0026 "dc": "http://purl.org/dc/elements/1.1/", 0027 "cc": "http://creativecommons.org/ns#", 0028 "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 0029 "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", 0030 } 0031 for prefix, uri in NAMESPACES.items(): 0032 etree.register_namespace(prefix, uri) 0033 0034 # END globals 0035 0036 0037 # BEGIN defs 0038 0039 0040 def strip_split(s: str): 0041 """ 0042 Strip whitespace from the start and end, then split into a list of strings. 0043 0044 re.split() RegEx: match comma with [0,inf) whitespace characters after it OR [1,inf) whitespace characters. 0045 """ 0046 # Ignore warnings about invalid escape sequences, this works fine. 0047 return re.split(",\s*|\s+", s.strip()) 0048 0049 0050 def get_renderable_elements(root: etree.Element): 0051 """ 0052 Get renderable elements that are children of the root of the SVG. 0053 0054 See the Renderable Elements section of the SVG documentation on MDN web docs: 0055 https://developer.mozilla.org/en-US/docs/Web/SVG/Element#SVG_elements_by_category 0056 """ 0057 return root.xpath( 0058 "./svg:a | ./svg:circle | ./svg:ellipse | ./svg:foreignObject | ./svg:g " 0059 + "| ./svg:image | ./svg:line | ./svg:mesh | ./svg:path | ./svg:polygon " 0060 + "| ./svg:polyline | ./svg:rect | ./svg:switch | ./svg:svg | ./svg:symbol " 0061 + "| ./svg:text | ./svg:textPath | ./svg:tspan | ./svg:unknown | ./svg:use", 0062 namespaces=NAMESPACES 0063 ) 0064 0065 0066 def make_dir(input_dir, output_dir, path): 0067 if not path.endswith('/22'): 0068 return 0069 0070 folder24_destination = path.replace(input_dir, output_dir, 1).replace('/22', '/24') 0071 0072 # Make 24/ 0073 Path(folder24_destination).mkdir(parents=True, exist_ok=True) 0074 0075 # Make 24@2x/ and 24@3x/ 0076 for scale in (2, 3): 0077 folder24_scaled_destination = folder24_destination.replace('/24', f'/24@{scale}x') 0078 if os.path.islink(folder24_scaled_destination): 0079 os.remove(folder24_scaled_destination) 0080 os.symlink("24", folder24_scaled_destination, target_is_directory=True) 0081 0082 0083 def make_file(input_dir, output_dir, path): 0084 # Filter out files 0085 if not (path.endswith('.svg') and '/22/' in path): 0086 return 0087 0088 file_destination = path.replace(input_dir, output_dir, 1).replace('/22/', '/24/') 0089 0090 # Regenerate symlinks or edit SVGs 0091 if os.path.islink(path): 0092 symlink_source = os.readlink(path).replace('/22/', '/24/') 0093 if os.path.islink(file_destination): 0094 os.remove(file_destination) 0095 if not os.path.exists(file_destination): 0096 os.symlink(symlink_source, file_destination) 0097 else: 0098 etree.set_default_parser(etree.XMLParser(remove_blank_text=True)) 0099 tree = etree.parse(path) 0100 root = tree.getroot() 0101 0102 viewBox_is_none = root.get('viewBox') is None 0103 width_is_none = root.get('width') is None 0104 height_is_none = root.get('height') is None 0105 0106 """ 0107 NOTE: 0108 - Using strip and split because the amount of whitespace and usage of commas can vary. 0109 - Checking against real values because string values can have leading zeros. 0110 - Replacing "px" with nothing so that values can be converted to real numbers and because px is the default unit type 0111 - If another unit type is used in the <svg> element, this script will fail, but icons shouldn't use other unit types anyway 0112 """ 0113 0114 # This is used to prevent SVGs with non-square or incorrect but valid viewBoxes from being converted to 24x24. 0115 # If viewBox is None, but the SVG still has width and height, the SVG is still fine. 0116 viewBox_matched_or_none = viewBox_is_none 0117 if not viewBox_is_none: 0118 viewBox_matched_or_none = ( 0119 list(map(float, strip_split(root.get('viewBox').strip('px')))) 0120 == [0.0, 0.0, 22.0, 22.0] 0121 ) 0122 0123 # This is used to prevent SVGs that aren't square or are missing only height or only width from being converted to 24x24. 0124 # If width and height are None, but the SVG still has a viewBox, the SVG is still fine. 0125 width_height_matched_or_none = width_is_none and height_is_none 0126 if not (width_is_none or height_is_none): 0127 width_height_matched_or_none = ( 0128 float(root.get('width').strip('px').strip()) == 22.0 and 0129 float(root.get('height').strip('px').strip()) == 22.0 0130 ) 0131 0132 if (width_height_matched_or_none and viewBox_matched_or_none 0133 and not (viewBox_is_none and (width_is_none or height_is_none))): 0134 # Resize to 24x24 0135 root.set('viewBox', "0 0 24 24") 0136 root.set('width', "24") 0137 root.set('height', "24") 0138 # Put content in a group that moves content down 1px, right 1px 0139 group = etree.Element('g', attrib={'transform': "translate(1,1)"}) 0140 group.extend(get_renderable_elements(root)) 0141 root.append(group) 0142 0143 # print(file_destination) 0144 tree.write(file_destination, method="xml", pretty_print=True, exclusive=True) 0145 else: 0146 skipped_message = " SKIPPED: " 0147 if not viewBox_matched_or_none: 0148 skipped_message += "not square or incorrect viewBox\nviewBox=\"" + root.get('viewBox') + "\"" 0149 elif not width_height_matched_or_none: 0150 skipped_message += "not square or incorrect width and height\nwidth=\"" + root.get('width') + "height=\"" + root.get('height') + "\"" 0151 elif viewBox_is_none and (width_is_none or height_is_none): 0152 skipped_message += "viewBox and width/height are missing" 0153 else: 0154 skipped_message += "You shouldn't be seeing this. Please fix " + os.path.basename(sys.argv[0]) 0155 0156 print(path.lstrip(input_dir) + skipped_message) 0157 0158 0159 def main(input_dirs, output_dir): 0160 for input_dir in input_dirs: 0161 for dirpath, dirnames, filenames in os.walk(input_dir): 0162 for d in dirnames: 0163 make_dir(input_dir, output_dir, os.path.join(dirpath, d)) 0164 for f in filenames: 0165 make_file(input_dir, output_dir, os.path.join(dirpath, f)) 0166 0167 # END defs 0168 0169 0170 # I've structured the program like this in case I want to do multiprocessing later 0171 if __name__ == '__main__': 0172 argv_len = len(sys.argv) 0173 if argv_len < 3: 0174 print("missing arguments") 0175 sys.exit(1) 0176 input_dirs: list = [] 0177 for i in range(1, argv_len-1): 0178 if Path(sys.argv[i]).is_dir(): 0179 input_dirs.append(sys.argv[i]) 0180 if len(input_dirs) < 1: 0181 print("No valid input folders") 0182 sys.exit(1) 0183 output_dir: str = sys.argv[argv_len-1] 0184 output_path = Path(output_dir) 0185 if output_path.exists() and not output_path.is_dir(): 0186 print("Output is not a folder") 0187 sys.exit(1) 0188 0189 sys.exit(main(input_dirs, output_dir))