From 6ef6a5330fee95098fa88ed5e1d9d3aad6be07ec Mon Sep 17 00:00:00 2001 From: Nick Romano Date: Mon, 11 Sep 2023 13:29:10 -0700 Subject: [PATCH 1/2] Update iOS importer to use new metadata --- importer/package.json | 3 +- importer/process_ios_assets.py | 257 +++++++++++++++++---------------- 2 files changed, 136 insertions(+), 124 deletions(-) diff --git a/importer/package.json b/importer/package.json index 044e4825b2..24254ee1c8 100644 --- a/importer/package.json +++ b/importer/package.json @@ -15,11 +15,10 @@ "finalize:android": "replace '#212121' '@color/fluent_default_icon_tint' ./dist --exclude=\"*.selector\" --recursive --quiet && replace '\"http://schemas.android.com/apk/res/android\"' '\"http://schemas.android.com/apk/res/android\" android:autoMirrored=\"true\"' $(awk '$0=\"./dist/\"$0\".xml\"' rtl.txt)", "create:android": "npm run generate:svg-android && find ./dist/ -type d -exec sh -c 'tools/vd-tool/bin/vd-tool -c -in {} -out {}' \\;", "optimize:android": "find ./dist/ -type f -name '*.svg' -delete && find ./dist/ -type d ! -name '*_selector.xml' -exec sh -c 'avocado -q {}' \\;", - "build:ios": "npm run generate:svg", "build:react": "npm run generate:react", "deploy:android": "npm run build:android && rm -rf ../android/library/src/main/res/drawable && mkdir ../android/library/src/main/res/drawable && find ./dist/ -type f -name \"*.xml\" -maxdepth 1 -exec cp {} ../android/library/src/main/res/drawable \\; && npm run clean", "deploy:react": "npm run build:react && rm -rf ../react/src/components && mkdir ../react/src/components && find ./dist/ -type f -name \"*.tsx\" -exec cp {} ../react/src/components \\; && npm run clean", - "deploy:ios": "npm run build:ios && python3 process_ios_assets.py && npm run clean", + "deploy:ios": "python3 process_ios_assets.py", "generate:font-regular": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Regular --codepoints=../fonts/FluentSystemIcons-Regular.json", "generate:font-filled": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Filled --codepoints=../fonts/FluentSystemIcons-Filled.json", "generate:font-resizable": "node generateFont.js --source=dist --dest=dist/fonts --iconType=Resizable", diff --git a/importer/process_ios_assets.py b/importer/process_ios_assets.py index 2c89d55f13..30b0115703 100644 --- a/importer/process_ios_assets.py +++ b/importer/process_ios_assets.py @@ -5,6 +5,9 @@ import os import shutil from collections import defaultdict +from collections import namedtuple + +FluentIconAsset = namedtuple('FluentIconAsset', ['name', 'path', 'metadata']) LIBRARY_NAME = 'FluentIcon' @@ -17,10 +20,10 @@ def to_camel_case(snake_str): RESERVED_WORDS = ['repeat', 'import', 'class'] ICON_PREFIX = "ic_fluent_" -IMAGE_FORMAT = ".svg" +IMAGE_FORMAT = "svg" def get_icon_name(file_name): - return file_name.replace(IMAGE_FORMAT, '').replace('__', '_').replace('_ltr_', '_').replace('_rtl_', '_') + return file_name.replace(f".{IMAGE_FORMAT}", '').replace('__', '_').replace('_ltr_', '_').replace('_rtl_', '_').replace('_ltr', '').replace('_rtl', '') def bucket_array(array, bucket_size): @@ -39,7 +42,7 @@ def bucket_array(array, bucket_size): return output -def xc_image_data_for_file_name(file_name, locale): +def xc_image_data_for_file_name(file_name, locale, metadata): output = { "idiom": "universal", "filename": file_name @@ -47,128 +50,140 @@ def xc_image_data_for_file_name(file_name, locale): if locale is not None: output["locale"] = locale - if "_ltr_" in file_name: + if metadata.get("directionType") == "mirror": + # iOS will mirror in this configuration from ltr + output["language-direction"] = "left-to-right" + elif metadata.get("directionType") == "unique": + if metadata["singleton"] == "LTR": + output["language-direction"] = "left-to-right" + elif metadata["singleton"] == "RTL": + output["language-direction"] = "right-to-left" + + # These icons are being replaced by new icons with metadata + # for backward compatibility we add the language-direction + if "_ltr_" in file_name and not metadata: output["language-direction"] = "left-to-right" - elif "_rtl_" in file_name: + if "_rtl_" in file_name and not metadata: output["language-direction"] = "right-to-left" return output -# from an array of lang/loc directory names, insert the localization information for a particular icon -# Copies an image to the directory for that asset and then inserts the locale information in Contents.json -def add_localized_set(lang_locs, original_icon_names, icon_assets_path): - for lang_loc in lang_locs: - file_names = os.listdir(os.path.join("dist", lang_loc)) - for file_name in file_names: + +def create_icon_set(fluent_icon_assets, original_icon_names, icon_assets_path): + for fluent_icon_asset in fluent_icon_assets: + language_metadata = fluent_icon_asset.metadata.get("language") + languages = [] + if language_metadata: + for language_group in language_metadata: + for language in language_group["locale"]: + languages.append(language) + + # If we only have one localized icon, disable localization support + if languages == ["en"]: + languages = [] + + for file_name in os.listdir(os.path.join(fluent_icon_asset.path, IMAGE_FORMAT)): icon_name = get_icon_name(file_name) - imageset_path = os.path.join(icon_assets_path, "{icon_name}.imageset".format(icon_name=icon_name)) - if not os.path.exists(imageset_path): - print(f"WARNING: No base localized icon {icon_name}") - os.mkdir(imageset_path) - - # Xcode is specific about the capitalization used for locales. - # "bg" -> "bg" - # "bg-bg" -> "bg-BG" - # "sr-latn" -> "sr-Latn" - # If it is too specific it won't be supported by iOS or Mac so we can ignore it. - # "sr-latn-rs" - locale_components = lang_loc.split("-") - if len(locale_components) == 3: - print(f"DEBUG: Unused localization {icon_name} {lang_loc}") + + if icon_name in original_icon_names: continue - elif len(locale_components) == 2: - if len(locale_components[1]) == 2: - asset_locale = locale_components[0] + "-" + locale_components[1].upper() + + original_icon_names.add(icon_name) + + imageset_path = os.path.join(icon_assets_path, "{icon_name}.imageset".format(icon_name=icon_name)) + os.mkdir(imageset_path) + shutil.copyfile(os.path.join(fluent_icon_asset.path, IMAGE_FORMAT, file_name), os.path.join(imageset_path, file_name)) + + supported_languages = [] + for language in languages: + if language in supported_languages: + # Ignore duplicates + continue + + # Xcode is specific about the capitalization used for locales. + # "bg" -> "bg" + # "bg-bg" -> "bg-BG" + # "sr-latn" -> "sr-Latn" + # If it is too specific it won't be supported by iOS or Mac so we can ignore it. + # "sr-latn-rs" + locale_components = language.split("-") + if len(locale_components) == 3: + print(f"DEBUG: Unused localization {icon_name} {language}") + continue + elif len(locale_components) == 2: + if len(locale_components[1]) == 2: + asset_locale = locale_components[0] + "-" + locale_components[1].upper() + else: + asset_locale = locale_components[0] + "-" + locale_components[1].title() else: - asset_locale = locale_components[0] + "-" + locale_components[1].title() - else: - asset_locale = lang_loc + asset_locale = language + + localized_icon_path = os.path.join(fluent_icon_asset.path, language, IMAGE_FORMAT, file_name) + if os.path.exists(localized_icon_path): + shutil.copyfile(localized_icon_path, os.path.join(imageset_path, language + "_" + file_name)) + supported_languages.append(language) - shutil.copyfile(os.path.join("dist", lang_loc, file_name), os.path.join(imageset_path, lang_loc + "_" + file_name)) imageset_contents_path = os.path.join(imageset_path, "Contents.json") - contents_json = json.load(open(imageset_contents_path)) - - loc_image_data = [] - if lang_loc == "zh": - # For the Chinese locale, explicitly differentiate between Simplified Chinese (zh_CN) and Traditional Chinese (zh_TW) - # While Apple's Automatic Localization in its Asset Catalogs support a superset Chinese locale id `zh`, we're adding two - - # even more explicit - locale ids pertaining to this language. But this doesn't mean that we need two separate .svg assets. - # Instead add the same zh_ic_fluent_ file under the two available Chinese Locale sets by adjusting the Contents.json metadata file. - loc_image_data.append(xc_image_data_for_file_name(lang_loc + "_" + file_name, locale="zh_CN")) - loc_image_data.append(xc_image_data_for_file_name(lang_loc + "_" + file_name, locale="zh_TW")) - else: - loc_image_data.append(xc_image_data_for_file_name(lang_loc + "_" + file_name, locale=lang_loc)) - - contents_json["properties"]["localizable"] = True - contents_json["images"].extend(loc_image_data) with open(imageset_contents_path, 'w') as imageset: - imageset.write(json.dumps(contents_json, indent=2, sort_keys=True)) - -def create_icon_set(file_names, original_icon_names, icon_assets_path): - for file_name in file_names: - icon_name = get_icon_name(file_name) + rendering_intent = "template" + if icon_name.endswith("_color") or icon_name.startswith("ic_fluent_flag_pride"): + rendering_intent = "original" + + images = [ + xc_image_data_for_file_name(file_name, locale=None, metadata=fluent_icon_asset.metadata) + ] + + contents = { + "images": images, + "info": { + "version": 1, + "author": "xcode" + }, + "properties": { + "template-rendering-intent": rendering_intent + } + } - if icon_name in original_icon_names: - continue + if len(supported_languages): + contents["properties"]["localizable"] = True - original_icon_names.add(icon_name) + for language in sorted(supported_languages): + loc_image_data = [] + if language == "zh": + # For the Chinese locale, explicitly differentiate between Simplified Chinese (zh_CN) and Traditional Chinese (zh_TW) + # While Apple's Automatic Localization in its Asset Catalogs support a superset Chinese locale id `zh`, we're adding two - + # even more explicit - locale ids pertaining to this language. But this doesn't mean that we need two separate .svg assets. + # Instead add the same zh_ic_fluent_ file under the two available Chinese Locale sets by adjusting the Contents.json metadata file. + loc_image_data.append(xc_image_data_for_file_name(language + "_" + file_name, locale="zh_CN", metadata=fluent_icon_asset.metadata)) + loc_image_data.append(xc_image_data_for_file_name(language + "_" + file_name, locale="zh_TW", metadata=fluent_icon_asset.metadata)) + else: + loc_image_data.append(xc_image_data_for_file_name(language + "_" + file_name, locale=language, metadata=fluent_icon_asset.metadata)) + contents["images"].extend(loc_image_data) - sibling_file_name = None - if "_ltr_" in file_name: - sibling_file_name = file_name.replace("_ltr_", "_rtl_") - elif "_rtl_" in file_name: - sibling_file_name = file_name.replace("_rtl_", "_ltr_") - - if sibling_file_name is not None: - if not os.path.exists(os.path.join("dist", sibling_file_name)): - print(f"WARNING: No corresponding localized icon {sibling_file_name}") - sibling_file_name = None - - imageset_path = os.path.join(icon_assets_path, "{icon_name}.imageset".format(icon_name=icon_name)) - os.mkdir(imageset_path) - shutil.copyfile(os.path.join("dist", file_name), os.path.join(imageset_path, file_name)) - if sibling_file_name is not None: - shutil.copyfile(os.path.join("dist", sibling_file_name), os.path.join(imageset_path, sibling_file_name)) - - imageset_contents_path = os.path.join(imageset_path, "Contents.json") - - with open(imageset_contents_path, 'w') as imageset: - rendering_intent = "template" - if icon_name.endswith("_color") or icon_name.startswith("ic_fluent_flag_pride"): - rendering_intent = "original" - - images = [ - xc_image_data_for_file_name(file_name, locale=None) - ] - if sibling_file_name is not None: - images.append(xc_image_data_for_file_name(sibling_file_name, locale=None)) - - contents = { - "images": images, - "info": { - "version": 1, - "author": "xcode" - }, - "properties": { - "template-rendering-intent": rendering_intent - } - } + imageset.write(json.dumps(contents, indent=2, sort_keys=True)) - imageset.write(json.dumps(contents, indent=2, sort_keys=True)) def process_assets(): project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + assets_dir = os.path.join(project_root, "assets") - file_names = [] - loc_names = [] - for file_name in os.listdir("dist"): - if file_name.endswith(IMAGE_FORMAT): - file_names.append(file_name) - elif os.path.isdir(os.path.join("dist", file_name)): - loc_names.append(file_name) - - file_names.sort() + icon_assets = [] + for file_name in os.listdir(assets_dir): + if file_name == ".DS_Store": + continue + icon_assets_path = os.path.join(assets_dir, file_name) + metadata_path = os.path.join(icon_assets_path, "metadata.json") + metadata = {} + if os.path.exists(metadata_path): + with open(metadata_path) as metadata_json: + metadata = json.loads(metadata_json.read()) + + icon_assets.append(FluentIconAsset( + name=file_name, + path=icon_assets_path, + metadata=metadata + )) ios_directory = os.path.join(project_root, "ios") @@ -178,14 +193,15 @@ def process_assets(): os.mkdir(icon_assets_path) original_icon_names = set() - create_icon_set(file_names, original_icon_names, icon_assets_path) - add_localized_set(loc_names, original_icon_names, icon_assets_path) + create_icon_set(icon_assets, original_icon_names, icon_assets_path) # Generate BUILD.gn for GN build system gn_path = os.path.join(ios_directory, "BUILD.gn") if os.path.exists(gn_path): os.remove(gn_path) + imagesets = sorted(os.listdir(icon_assets_path)) + with open(gn_path, 'w+') as gn_file: gn_file.write("#\n") gn_file.write("# Copyright (c) Microsoft Corporation. All rights reserved.\n") @@ -197,23 +213,17 @@ def process_assets(): gn_file.write("import(\"//build/config/ios/asset_catalog.gni\")\n\n") - imageset_names = set() - for file_name in file_names: - imageset_name = get_icon_name(file_name) - # GN targets do not allow duplicate names - if imageset_name in imageset_names: + for imageset in imagesets: + if imageset == ".DS_Store": continue - imageset_names.add(imageset_name) - imageset_folder_path = ios_directory + '/FluentIcons/Assets/IconAssets.xcassets/' + imageset_name + '.imageset' - - gn_file.write("imageset(\"{}\")".format(imageset_name) + " {\n") + imageset_folder_path = os.path.join(icon_assets_path, imageset) + gn_file.write("imageset(\"{}\")".format(imageset.replace(".imageset", "")) + " {\n") gn_file.write(" sources = [\n") - gn_file.write(" \"FluentIcons/Assets/IconAssets.xcassets/{}.imageset/Contents.json\"".format(imageset_name) + ",\n") - for imageset_file in os.listdir(imageset_folder_path): - if os.path.splitext(imageset_file)[1] == IMAGE_FORMAT: - gn_file.write(" \"FluentIcons/Assets/IconAssets.xcassets/{imageset_name}.imageset/{icon_file_name}\"".format(imageset_name=imageset_name, icon_file_name=imageset_file) + ",\n") + for imageset_file in sorted(os.listdir(imageset_folder_path)): + gn_file.write(f" \"FluentIcons/Assets/IconAssets.xcassets/{imageset}/{imageset_file}\",\n") gn_file.write(" ]\n") gn_file.write("}\n\n") + swift_enum_path = os.path.join(ios_directory, LIBRARY_NAME + "s", "Classes", LIBRARY_NAME + ".swift") if os.path.exists(swift_enum_path): os.remove(swift_enum_path) @@ -222,14 +232,17 @@ def process_assets(): all_sizes = set() original_icon_names = set() - for file_name in file_names: + for imageset in imagesets: """ Remove first and last two components Before: ic_fluent_flash_off_24_regular.svg After: flash_off_24 """ - icon_name = get_icon_name(file_name).replace(ICON_PREFIX, '') + if imageset == ".DS_Store": + continue + + icon_name = get_icon_name(imageset.replace(".imageset", "")).replace(ICON_PREFIX, "") if icon_name in original_icon_names: continue From 2923911a70c5ced991d76f56365285c052c57312 Mon Sep 17 00:00:00 2001 From: Nick Romano Date: Mon, 11 Sep 2023 13:39:29 -0700 Subject: [PATCH 2/2] Handle case sensitive filesystems --- importer/process_ios_assets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importer/process_ios_assets.py b/importer/process_ios_assets.py index 30b0115703..48a1c1e73a 100644 --- a/importer/process_ios_assets.py +++ b/importer/process_ios_assets.py @@ -82,7 +82,7 @@ def create_icon_set(fluent_icon_assets, original_icon_names, icon_assets_path): if languages == ["en"]: languages = [] - for file_name in os.listdir(os.path.join(fluent_icon_asset.path, IMAGE_FORMAT)): + for file_name in os.listdir(os.path.join(fluent_icon_asset.path, IMAGE_FORMAT.upper())): icon_name = get_icon_name(file_name) if icon_name in original_icon_names: @@ -92,7 +92,7 @@ def create_icon_set(fluent_icon_assets, original_icon_names, icon_assets_path): imageset_path = os.path.join(icon_assets_path, "{icon_name}.imageset".format(icon_name=icon_name)) os.mkdir(imageset_path) - shutil.copyfile(os.path.join(fluent_icon_asset.path, IMAGE_FORMAT, file_name), os.path.join(imageset_path, file_name)) + shutil.copyfile(os.path.join(fluent_icon_asset.path, IMAGE_FORMAT.upper(), file_name), os.path.join(imageset_path, file_name)) supported_languages = [] for language in languages: @@ -118,7 +118,7 @@ def create_icon_set(fluent_icon_assets, original_icon_names, icon_assets_path): else: asset_locale = language - localized_icon_path = os.path.join(fluent_icon_asset.path, language, IMAGE_FORMAT, file_name) + localized_icon_path = os.path.join(fluent_icon_asset.path, language, IMAGE_FORMAT.upper(), file_name) if os.path.exists(localized_icon_path): shutil.copyfile(localized_icon_path, os.path.join(imageset_path, language + "_" + file_name)) supported_languages.append(language)