Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyzer for devicon.json and icon directories #1623

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/scripts/analyzer_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#
# These requirements were autogenerated by pipenv
# To regenerate from the project's Pipfile, run:
#
# pipenv lock --requirements
#

-i https://pypi.org/simple
async-generator==1.10; python_version >= '3.5'
attrs==22.2.0; python_version >= '3.6'
certifi==2022.12.7; python_version >= '3.6'
exceptiongroup==1.1.0; python_version < '3.11'
h11==0.14.0; python_version >= '3.7'
idna==3.4; python_version >= '3.5'
outcome==1.2.0; python_version >= '3.7'
pysocks==1.7.1
selenium==4.7.2
sniffio==1.3.0; python_version >= '3.7'
sortedcontainers==2.4.0
tqdm==4.64.1
trio-websocket==0.9.2; python_version >= '3.5'
trio==0.22.0; python_version >= '3.7'
urllib3[socks]==1.26.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
wsproto==1.2.0; python_version >= '3.7'
320 changes: 320 additions & 0 deletions .github/scripts/devicon_json_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
import json
import os
import re
from math import isclose
from pprint import pprint
from typing import Any, Dict

import click

from logging import ERROR

from tqdm import tqdm
from svglib.svglib import svg2rlg, logger as svglib_logger

DEVICON_JSON_PATH = "devicon.json"

svglib_logger.setLevel(ERROR)

# Functions
## Utility functions
def update_issues(key: str, value: Any, issues: dict):
"""Utility function to update dictionary or add new key."""
if key in issues:
issues[key].update(value)
else:
issues[key] = value


def get_all_version_refs(icon: dict):
"""Utility function to get all the version references of an icon."""
versions = icon["versions"]["svg"].copy()
versions.extend(icon["versions"]["font"])
return set(versions)


def get_icon_filename(icons_dir, icon_name, version):
if os.path.exists(f"{icons_dir}/{icon_name}"):
filename = f"{icons_dir}/{icon_name}/{icon_name}-{version}.svg"
else:
filename = f"{icons_dir}/{icon_name}-{version}.svg"
return filename


def read_icon_svg(icons_dir: str, icon_name: str, version: str):
filename = get_icon_filename(icons_dir, icon_name, version)
with open(filename, "r") as svg_file:
svg_content = svg_file.read()
return svg_content


def get_bbox(svg_path):
svg = svg2rlg(
svg_path,
)
bounds = svg.getBounds()
width = bounds[2] - bounds[0]
height = bounds[3] - bounds[1]
return {"width": width, "height": height}


def get_svg_file_versions(icons_dir: str, icon_name: str):
"""Get all the svg versions of an icon given its name."""
if os.path.exists(f"{icons_dir}/{icon_name}"):
# If the icons_dir is the root icons dir
svg_filenames = [
fn for fn in os.listdir(f"{icons_dir}/{icon_name}") if fn.endswith(".svg")
]
else:
# if this is the directory for a single technology
svg_filenames = [
fn for fn in os.listdir(f"{icons_dir}/") if fn.endswith(".svg")
]
return list(
map(lambda fn: re.sub(rf"{icon_name}-(.+)\.svg", r"\1", fn), svg_filenames)
)


def sort_json(icons: list):
"""Sort the json files contents."""
icons = sorted(icons, key=lambda k: k["name"])
for icon in icons:
icon["versions"]["svg"] = sorted(icon["versions"]["svg"])
icon["versions"]["font"] = sorted(icon["versions"]["font"])
icon["aliases"] = sorted(icon["aliases"], key=lambda k: k["base"])
icon["tags"] = sorted(icon["tags"])


## Process Step Functions
def fix_svg_versions(icon: dict, svg_versions: list, issues: dict = {}):
"""Fix missing and extra `svg` versions in devicon.json for an icon."""
missing_versions = []
extra_versions = []
for svg_version in svg_versions:
if svg_version not in icon["versions"]["svg"]:
missing_versions.append(svg_version)

for ref in icon["versions"]["svg"]:
if ref not in svg_versions:
extra_versions.append(ref)

# Update the icon's versions.
icon["versions"]["svg"] = [
version for version in icon["versions"]["svg"] if version not in extra_versions
]
icon["versions"]["svg"].extend(missing_versions)
icon["versions"]["svg"] = sorted(icon["versions"]["svg"])

if len(missing_versions) > 0:
update_issues(icon["name"], {"missing_svg_versions": missing_versions}, issues)

if len(extra_versions) > 0:
update_issues(icon["name"], {"extra_svg_versions": extra_versions}, issues)

return icon


def remove_extra_font_references(icon: dict, issues: dict = {}):
"""Checks for extra reference(s) in versions.svg[] and removes them."""
extra_versions = []
for ref in icon["versions"]["font"]:
if ref not in icon["versions"]["svg"]:
extra_versions.append(ref)

icon["versions"]["font"] = [
version for version in icon["versions"]["font"] if version not in extra_versions
]

if len(extra_versions) > 0:
update_issues(icon["name"], {"extra_font_versions": extra_versions}, issues)
return icon


def fix_aliases(icon: dict, issues: dict = {}):
"""Fix swapped references in aliases and remove redundant aliases."""
if len(icon["aliases"]) > 0:
versions = get_all_version_refs(icon)
missing_base = list(
filter(lambda f: f["base"] not in versions, icon["aliases"])
)
repeated_alias = list(filter(lambda f: f["alias"] in versions, icon["aliases"]))

new_aliases = []
redundant_aliases = []
# Check for swapped references in aliases.
if len(missing_base) > 0 and len(repeated_alias) > 0:
for svg_version in missing_base:
icon["aliases"].remove(svg_version)
icon["aliases"].append(
{"base": svg_version["alias"], "alias": svg_version["base"]}
)
new_aliases.append(
{"base": svg_version["alias"], "alias": svg_version["base"]}
)
# Check for unnecessary aliases.
elif len(repeated_alias) > 0:
for svg_version in repeated_alias:
redundant_aliases.append(svg_version)
icon["aliases"].remove(svg_version)

if len(new_aliases) > 0:
update_issues(icon["name"], {"new_aliases": new_aliases}, issues)

if len(redundant_aliases) > 0:
update_issues(
icon["name"], {"redundant_aliases": redundant_aliases}, issues
)

return icon


def remove_multicolor_font_versions(icons_dir: str, icon: dict, issues: dict = {}):
"""
Remove multicolor font versions from devicon.json.

Note: Currently it does not remove anything it just reports it.
"""
multicolor_versions = []
for version in icon["versions"]["font"]:
svg_content = read_icon_svg(icons_dir, icon["name"], version)
colors = re.findall(r"fill=\"[#]([A-Fa-f0-9]{6})\"", svg_content)
colors = [color.lower() for color in colors]
unique_colors = set(colors)
if len(unique_colors) > 1 or re.search(r"[gG]radient", svg_content):
multicolor_versions.append(version)
# TODO: Ask the team if theses versions should be removed.
# icon["versions"]["font"].remove(version)

if len(multicolor_versions) > 0:
update_issues(
icon["name"], {"multicolor_font_version": multicolor_versions}, issues
)
return icon


def check_view_port(icons_dir: str, icon: dict, issues: dict):
"""Check if the view port is set to 0 0 128 128"""
bad_view_port = []
for version in icon["versions"]["svg"]:
svg_content = read_icon_svg(icons_dir, icon["name"], version)
if not re.search(r"viewBox=\"0 0 128 128\"", svg_content):
bad_view_port.append(version)

if len(bad_view_port) > 0:
update_issues(icon["name"], {"bad_view_port": bad_view_port}, issues)


def check_icon_bbox(icons_dir, icon: dict, issues: dict):
"""Check if the maximum height and width of the icon is 128."""
larger = []
smaller = []
for version in icon["versions"]["svg"]:
filename = get_icon_filename(icons_dir, icon["name"], version)
bbox = get_bbox(filename)
if bbox["width"] > 128.0 or bbox["height"] > 128.0:
larger.append(version)
continue

width_in_tol = isclose(bbox["width"], 128.0, abs_tol=2.0)
height_in_tol = isclose(bbox["height"], 128.0, abs_tol=2.0)
if not width_in_tol and not height_in_tol:
smaller.append(version)

if len(larger) > 0:
update_issues(icon["name"], {"larger_bbox": larger}, issues)

if len(smaller) > 0:
update_issues(icon["name"], {"smaller_bbox": smaller}, issues)


def generate_markdown_report(issues: dict):
"""Generate a markdown report of the issues."""
report = ""
for icon_name, icon_issues in issues.items():
report += f"## {icon_name}\n"
for issue_type, issue in icon_issues.items():
report += f" - {issue_type}\n"
for issue in issue:
report += f" - {issue}\n"
report += "\n"

with open("report.md", "w") as report_file:
report_file.write(report)


@click.option("--update-json", "-u", is_flag=True, help="Update devicon.json file.")
@click.option("--print-issues", "-p", is_flag=True, help="Print issues.")
@click.option("--generate-report", "-g", is_flag=True, help="Generate markdown report.")
@click.option(
"--devicon-json-path",
"-d",
default=DEVICON_JSON_PATH,
help="Path to devicon.json file.",
)
@click.argument(
"icons_dir",
type=click.Path(exists=True, file_okay=False),
default="icons/",
)
@click.command(
help="Analyze devicon.json file.\n\nThe default ICONS_DIR is `icons/` which contains all icons. When running for a PR a single-technology directory can be selected instead."
)
def main(
icons_dir: str,
devicon_json_path: str,
update_json: bool,
print_issues: bool,
generate_report: bool,
):
with open(devicon_json_path) as devicon_json_file:
devicon_json = json.load(devicon_json_file)

issues: Dict[Any, Any] = {}
for i, icon in enumerate(tqdm(devicon_json)):
if not any(
[
True if filesindir.startswith(icon["name"]) else False
for filesindir in os.listdir(icons_dir)
]
):
continue

# Get existing icon versions
svg_file_versions = get_svg_file_versions(icons_dir, icon["name"])

# Fix missing and extra `svg` versions in devicon.json for an icon.
icon = fix_svg_versions(icon, svg_file_versions, issues)
devicon_json[i] = icon

# Check for extra reference(s) in versions.svg[]
icon = remove_extra_font_references(icon, issues)
devicon_json[i] = icon

# Fix swapped references in aliases and remove redundant aliases.
icon = fix_aliases(icon, issues)
devicon_json[i] = icon

# Remove multicolor font versions from devicon.json.
icon = remove_multicolor_font_versions(icons_dir, icon, issues)
devicon_json[i] = icon

# Check if the view port is set to 0 0 128 128
check_view_port(icons_dir, icon, issues)

# Check if the maximum height or width of the icon is 128.
check_icon_bbox(icons_dir, icon, issues)

if print_issues:
pprint(issues)

if generate_report:
generate_markdown_report(issues)

if update_json:
sort_json(devicon_json)
with open(devicon_json_path, "w") as devicon_json_file:
json.dump(devicon_json, devicon_json_file, indent=4)


if __name__ == "__main__":
main()