Skip to content

Commit

Permalink
Update iOS importer to use new metadata (#639)
Browse files Browse the repository at this point in the history
* Update iOS importer to use new metadata

* Handle case sensitive filesystems

---------

Co-authored-by: spencer-nelson <[email protected]>
  • Loading branch information
nickromano and spencer-nelson authored Sep 21, 2023
1 parent 0f1a93c commit 2845b21
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 124 deletions.
3 changes: 1 addition & 2 deletions importer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
257 changes: 135 additions & 122 deletions importer/process_ios_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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):
Expand All @@ -39,136 +42,148 @@ 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
}
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.upper())):
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.upper(), 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.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)

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")

Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit 2845b21

Please sign in to comment.