File indexing completed on 2024-12-08 09:35:21
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 OUTPUT_DIR: str = sys.argv[1] 0035 INPUT_DIR: str = "./" 0036 0037 # END globals 0038 0039 0040 # BEGIN defs 0041 0042 def strip_split(s: str): 0043 """ 0044 Strip whitespace from the start and end, then split into a list of strings. 0045 0046 re.split() RegEx: match comma with [0,inf) whitespace characters after it OR [1,inf) whitespace characters. 0047 """ 0048 # Ignore warnings about invalid escape sequences, this works fine. 0049 return re.split(",\s*|\s+", s.strip()) 0050 0051 0052 def get_renderable_elements(root: etree.Element): 0053 """ 0054 Get renderable elements that are children of the root of the SVG. 0055 0056 See the Renderable Elements section of the SVG documentation on MDN web docs: 0057 https://developer.mozilla.org/en-US/docs/Web/SVG/Element#SVG_elements_by_category 0058 """ 0059 return root.xpath( 0060 "./svg:a | ./svg:circle | ./svg:ellipse | ./svg:foreignObject | ./svg:g " 0061 + "| ./svg:image | ./svg:line | ./svg:mesh | ./svg:path | ./svg:polygon " 0062 + "| ./svg:polyline | ./svg:rect | ./svg:switch | ./svg:svg | ./svg:symbol " 0063 + "| ./svg:text | ./svg:textPath | ./svg:tspan | ./svg:unknown | ./svg:use", 0064 namespaces=NAMESPACES 0065 ) 0066 0067 0068 def main(): 0069 for dirpath, dirnames, filenames in os.walk(INPUT_DIR): 0070 folder24_destination = os.path.join(dirpath, "24").replace(INPUT_DIR, OUTPUT_DIR, 1) 0071 for d in dirnames: 0072 if d != '22': 0073 continue 0074 0075 # Make 24/ 0076 Path(folder24_destination).mkdir(parents=True, exist_ok=True) 0077 # print(folder24_destination) 0078 0079 # Make 24@2x/ and 24@3x/ 0080 for scale in (2, 3): 0081 folder24_scaled_destination = folder24_destination.replace('/24', f'/24@{scale}x') 0082 if os.path.islink(folder24_scaled_destination): 0083 os.remove(folder24_scaled_destination) 0084 os.symlink("24", folder24_scaled_destination, target_is_directory=True) 0085 # print(folder24_scaled_destination + " -> " + os.readlink(folder24_scaled_destination)) 0086 0087 for f in filenames: 0088 filepath = os.path.join(dirpath, f) 0089 0090 # Filter out files 0091 if not (f.endswith('.svg') and '/22' in filepath): 0092 continue 0093 0094 file_destination = filepath.replace(INPUT_DIR, OUTPUT_DIR, 1).replace('/22', '/24') 0095 # print(file_destination) 0096 0097 # Regenerate symlinks or edit SVGs 0098 if os.path.islink(filepath): 0099 symlink_source = os.readlink(filepath).replace('/22', '/24') 0100 if os.path.exists(file_destination): 0101 os.remove(file_destination) 0102 os.symlink(symlink_source, file_destination) 0103 # print(file_destination + " -> " + os.readlink(file_destination)) 0104 else: 0105 etree.set_default_parser(etree.XMLParser(remove_blank_text=True)) 0106 tree = etree.parse(filepath) 0107 root = tree.getroot() 0108 0109 viewBox_is_none = root.get('viewBox') is None 0110 width_is_none = root.get('width') is None 0111 height_is_none = root.get('height') is None 0112 0113 """ 0114 NOTE: 0115 - Using strip and split because the amount of whitespace and usage of commas can vary. 0116 - Checking against real values because string values can have leading zeros. 0117 - Replacing "px" with nothing so that values can be converted to real numbers and because px is the default unit type 0118 - If another unit type is used in the <svg> element, this script will fail, but icons shouldn't use other unit types anyway 0119 """ 0120 0121 # This is used to prevent SVGs with non-square or incorrect but valid viewBoxes from being converted to 24x24. 0122 # If viewBox is None, but the SVG still has width and height, the SVG is still fine. 0123 viewBox_matched_or_none = viewBox_is_none 0124 if not viewBox_is_none: 0125 viewBox_matched_or_none = ( 0126 list(map(float, strip_split(root.get('viewBox').strip('px')))) 0127 == [0.0, 0.0, 22.0, 22.0] 0128 ) 0129 0130 # This is used to prevent SVGs that aren't square or are missing only height or only width from being converted to 24x24. 0131 # If width and height are None, but the SVG still has a viewBox, the SVG is still fine. 0132 width_height_matched_or_none = width_is_none and height_is_none 0133 if not (width_is_none or height_is_none): 0134 width_height_matched_or_none = ( 0135 float(root.get('width').strip('px').strip()) == 22.0 and 0136 float(root.get('height').strip('px').strip()) == 22.0 0137 ) 0138 0139 if (width_height_matched_or_none and viewBox_matched_or_none 0140 and not (viewBox_is_none and (width_is_none or height_is_none))): 0141 # Resize to 24x24 0142 root.set('viewBox', "0 0 24 24") 0143 root.set('width', "24") 0144 root.set('height', "24") 0145 # Put content in a group that moves content down 1px, right 1px 0146 group = etree.Element('g', attrib={'transform': "translate(1,1)"}) 0147 group.extend(get_renderable_elements(root)) 0148 root.append(group) 0149 0150 # print(file_destination) 0151 tree.write(file_destination, method="xml", pretty_print=True, exclusive=True) 0152 else: 0153 skipped_message = " SKIPPED: " 0154 if not viewBox_matched_or_none: 0155 skipped_message += "not square or incorrect viewBox\nviewBox=\"" + root.get('viewBox') + "\"" 0156 elif not width_height_matched_or_none: 0157 skipped_message += "not square or incorrect width and height\nwidth=\"" + root.get('width') + "height=\"" + root.get('height') + "\"" 0158 elif viewBox_is_none and (width_is_none or height_is_none): 0159 skipped_message += "viewBox and width/height are missing" 0160 else: 0161 skipped_message += "You shouldn't be seeing this. Please fix " + os.path.basename(sys.argv[0]) 0162 0163 print(filepath.lstrip(INPUT_DIR) + skipped_message) 0164 0165 # END defs 0166 0167 0168 # I've structured the program like this in case I want to do multiprocessing later 0169 if __name__ == '__main__': 0170 sys.exit(main())