upload
This commit is contained in:
11
scripts/TEMPLATE.md
Normal file
11
scripts/TEMPLATE.md
Normal file
@ -0,0 +1,11 @@
|
||||
[](https://www.jsdelivr.com/package/gh/homarr-labs/dashboard-icons)
|
||||
[](https://www.jsdelivr.com/package/gh/walkxcode/dashboard-icons)
|
||||
|
||||
## Dashboard Icons
|
||||
|
||||
The best source for dashboard icons.<br />
|
||||
[**← Back to repository**](https://github.com/homarr-labs/dashboard-icons/)
|
||||
|
||||
<!-- ICONS -->
|
||||
|
||||
<!-- END ICONS -->
|
189
scripts/convert_svg_assets.py
Normal file
189
scripts/convert_svg_assets.py
Normal file
@ -0,0 +1,189 @@
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import cairosvg
|
||||
|
||||
# Define paths
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
SVG_DIR = ROOT_DIR / "svg"
|
||||
PNG_DIR = ROOT_DIR / "png"
|
||||
WEBP_DIR = ROOT_DIR / "webp"
|
||||
|
||||
# Ensure the output folders exist
|
||||
PNG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
WEBP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Track results
|
||||
failed_files = []
|
||||
converted_pngs = 0
|
||||
converted_webps = 0
|
||||
total_icons = 0
|
||||
png_only_icons = [] # List to store PNG-only icons
|
||||
|
||||
def file_size_readable(size_bytes):
|
||||
"""Convert bytes to a human-readable format."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes:.2f} {unit}"
|
||||
size_bytes /= 1024
|
||||
return f"{size_bytes:.2f} TB"
|
||||
|
||||
def hash_file(file_path):
|
||||
"""Generate an MD5 hash for a file."""
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
def convert_to_kebab_case(name):
|
||||
"""Convert a filename to kebab-case."""
|
||||
cleaned = re.sub(r'[^a-zA-Z0-9\s-]', '', name)
|
||||
kebab_case_name = re.sub(r'[\s_]+', '-', cleaned).lower()
|
||||
return kebab_case_name
|
||||
|
||||
def rename_if_needed(file_path):
|
||||
"""Ensure the filename is in kebab-case; rename if necessary."""
|
||||
new_name = convert_to_kebab_case(file_path.stem) + file_path.suffix
|
||||
new_path = file_path.parent / new_name
|
||||
|
||||
if new_path != file_path:
|
||||
if new_path.exists():
|
||||
raise FileExistsError(f"File conflict: {new_path} already exists.")
|
||||
file_path.rename(new_path)
|
||||
print(f"Renamed: {file_path} -> {new_path}")
|
||||
|
||||
return new_path
|
||||
|
||||
def needs_conversion(output_file, data=None):
|
||||
"""Check if a file needs to be converted or overwritten."""
|
||||
if output_file.exists():
|
||||
if data:
|
||||
existing_hash = hash_file(output_file)
|
||||
new_hash = hashlib.md5(data).hexdigest()
|
||||
return existing_hash != new_hash
|
||||
return False
|
||||
return True
|
||||
|
||||
def convert_svg_to_png(svg_path, png_path):
|
||||
"""Convert SVG to PNG."""
|
||||
global converted_pngs
|
||||
try:
|
||||
png_data = cairosvg.svg2png(url=str(svg_path), output_height=512)
|
||||
|
||||
if needs_conversion(png_path, png_data):
|
||||
with open(png_path, 'wb') as f:
|
||||
f.write(png_data)
|
||||
print(f"Converted PNG: {png_path} ({file_size_readable(png_path.stat().st_size)})")
|
||||
converted_pngs += 1
|
||||
else:
|
||||
print(f"PNG already up-to-date: {png_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to convert {svg_path} to PNG: {e}")
|
||||
failed_files.append(svg_path)
|
||||
|
||||
def convert_image_to_webp(image_path, webp_path):
|
||||
"""Convert an image (PNG or other) to WEBP."""
|
||||
global converted_webps
|
||||
try:
|
||||
image = Image.open(image_path).convert("RGBA")
|
||||
|
||||
if needs_conversion(webp_path):
|
||||
image.save(webp_path, format='WEBP')
|
||||
print(f"Converted WEBP: {webp_path} ({file_size_readable(webp_path.stat().st_size)})")
|
||||
converted_webps += 1
|
||||
else:
|
||||
print(f"WEBP already up-to-date: {webp_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to convert {image_path} to WEBP: {e}")
|
||||
failed_files.append(image_path)
|
||||
|
||||
def clean_up_files(folder, valid_basenames):
|
||||
"""Remove files that no longer have corresponding SVG or PNG files."""
|
||||
removed_files = 0
|
||||
for file_path in folder.glob('*'):
|
||||
if file_path.stem not in valid_basenames:
|
||||
file_path.unlink()
|
||||
print(f"Removed: {file_path}")
|
||||
removed_files += 1
|
||||
return removed_files
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Track valid basenames (from SVG and PNG files)
|
||||
valid_basenames = set()
|
||||
|
||||
# Process all SVG files
|
||||
for svg_file in SVG_DIR.glob("*.svg"):
|
||||
total_icons += 1
|
||||
|
||||
# Ensure the filename is in kebab-case
|
||||
try:
|
||||
svg_path = rename_if_needed(svg_file)
|
||||
except Exception as e:
|
||||
print(f"Error renaming {svg_file}: {e}")
|
||||
failed_files.append(svg_file)
|
||||
continue
|
||||
|
||||
valid_basenames.add(svg_path.stem)
|
||||
|
||||
# Set paths for PNG and WEBP
|
||||
png_path = PNG_DIR / f"{svg_path.stem}.png"
|
||||
webp_path = WEBP_DIR / f"{svg_path.stem}.webp"
|
||||
|
||||
# Convert SVG to PNG
|
||||
convert_svg_to_png(svg_path, png_path)
|
||||
|
||||
# Convert PNG to WEBP
|
||||
if png_path.exists():
|
||||
convert_image_to_webp(png_path, webp_path)
|
||||
|
||||
# Process PNG-only files
|
||||
for png_file in PNG_DIR.glob("*.png"):
|
||||
if png_file.stem not in valid_basenames:
|
||||
# Ensure the filename is in kebab-case
|
||||
try:
|
||||
png_path = rename_if_needed(png_file)
|
||||
except Exception as e:
|
||||
print(f"Error renaming {png_file}: {e}")
|
||||
failed_files.append(png_file)
|
||||
continue
|
||||
|
||||
valid_basenames.add(png_path.stem)
|
||||
|
||||
# Add the PNG-only icon to the list
|
||||
png_only_icons.append(png_path.stem)
|
||||
|
||||
# Set path for WEBP
|
||||
webp_path = WEBP_DIR / f"{png_path.stem}.webp"
|
||||
|
||||
# Convert PNG to WEBP
|
||||
convert_image_to_webp(png_path, webp_path)
|
||||
|
||||
# Clean up unused files in PNG and WEBP directories
|
||||
removed_pngs = clean_up_files(PNG_DIR, valid_basenames.union({p.stem for p in SVG_DIR.glob("*.svg")}))
|
||||
removed_webps = clean_up_files(WEBP_DIR, valid_basenames)
|
||||
|
||||
# Display summary
|
||||
if converted_pngs == 0 and converted_webps == 0 and removed_pngs == 0 and removed_webps == 0:
|
||||
print("\nAll icons are already up-to-date.")
|
||||
else:
|
||||
print(f"\nConverted {converted_pngs} PNGs and {converted_webps} WEBPs out of {total_icons} icons.")
|
||||
print(f"Removed {removed_pngs} PNGs and {removed_webps} WEBPs.")
|
||||
|
||||
# Display any failed conversions
|
||||
if failed_files:
|
||||
print("\nThe following files failed to convert:")
|
||||
for file in failed_files:
|
||||
print(file)
|
||||
|
||||
# Output PNG-only icons
|
||||
if png_only_icons:
|
||||
print("\nPNG-only icons (no SVG available):")
|
||||
for icon in png_only_icons:
|
||||
print(f"- {icon}")
|
||||
else:
|
||||
print("\nNo PNG-only icons found.")
|
79
scripts/generate_file_tree.py
Normal file
79
scripts/generate_file_tree.py
Normal file
@ -0,0 +1,79 @@
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import base64
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
def generate_folder_tree(paths):
|
||||
tree = {}
|
||||
webp_files = []
|
||||
for path in paths:
|
||||
resolved_path = Path(path).resolve()
|
||||
base_folder = resolved_path.name or Path.cwd().name
|
||||
for root, _, files in os.walk(resolved_path):
|
||||
relative_path = os.path.relpath(root, resolved_path)
|
||||
key = base_folder if relative_path == '.' else os.path.join(base_folder, relative_path)
|
||||
if files:
|
||||
tree[key] = sorted(files) # Sort the list of files alphabetically
|
||||
# Collect WebP files for XML generation
|
||||
for file in files:
|
||||
if file.lower().endswith('.webp'):
|
||||
webp_files.append(os.path.join(root, file))
|
||||
return tree, webp_files
|
||||
|
||||
def generate_webp_xml(webp_files):
|
||||
json_obj_list = []
|
||||
for path in webp_files:
|
||||
with open(path, "rb") as image_file:
|
||||
# Encode the WebP image to base64
|
||||
data = base64.b64encode(image_file.read()).decode('ascii')
|
||||
|
||||
# Build the base64 string for data URI
|
||||
data_string = f"data:image/webp;base64,{data}"
|
||||
|
||||
# Extract name from the path
|
||||
name = Path(path).stem
|
||||
|
||||
# Append the base64 encoded WebP data into the list
|
||||
json_obj_list.append({
|
||||
"data": data_string,
|
||||
"w": 48,
|
||||
"h": 48,
|
||||
"title": name,
|
||||
"aspect": "fixed"
|
||||
})
|
||||
|
||||
# Convert to JSON string with no extra spaces for XML output
|
||||
return json.dumps(json_obj_list, separators=(',', ':'))
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Adjust paths to be one level up
|
||||
folder_paths = sys.argv[1:]
|
||||
folder_paths = [str(Path(path).resolve()) for path in folder_paths]
|
||||
|
||||
if not folder_paths:
|
||||
print("Please provide at least one folder path.")
|
||||
sys.exit(1)
|
||||
|
||||
# Generate the folder tree and get WebP files
|
||||
folder_tree, webp_files = generate_folder_tree(folder_paths)
|
||||
|
||||
# Write the JSON structure to 'tree.json' in the parent folder
|
||||
root_dir = Path(__file__).resolve().parent
|
||||
tree_json_path = root_dir.parent / 'tree.json'
|
||||
with open(tree_json_path, 'w') as f:
|
||||
json.dump(folder_tree, f, indent=4, sort_keys=True) # Sort the keys in the JSON output
|
||||
print(f"Folder tree successfully written to '{tree_json_path}'.")
|
||||
|
||||
# Generate WebP XML
|
||||
if webp_files:
|
||||
webp_json = generate_webp_xml(webp_files)
|
||||
|
||||
# Write XML structure (with wrapped JSON) to 'tree.xml'
|
||||
xml_output_path = root_dir.parent / 'tree.xml'
|
||||
with open(xml_output_path, 'w') as xml_output:
|
||||
print(f"<mxlibrary>{webp_json}</mxlibrary>", file=xml_output)
|
||||
print(f"WebP assets successfully written to '{xml_output_path}'.")
|
||||
else:
|
||||
print("No WebP files found for XML generation.")
|
85
scripts/generate_icons_page.py
Normal file
85
scripts/generate_icons_page.py
Normal file
@ -0,0 +1,85 @@
|
||||
import json
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Read the JSON file
|
||||
def read_tree_json(file_path):
|
||||
with open(file_path, 'r', encoding='UTF-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
# Generate a table row with checkmarks for available formats and links
|
||||
def generate_table_row(icon_name, formats):
|
||||
# Prepare the checkmarks and links for each format if they exist
|
||||
webp_check = '✅' if formats['webp'] else '❌'
|
||||
png_check = '✅' if formats['png'] else '❌'
|
||||
svg_check = '✅' if formats['svg'] else '❌'
|
||||
|
||||
# Prepare the links for each format if they exist
|
||||
webp_link = f'<a href="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/webp/{icon_name}.webp">WebP</a>' if formats['webp'] else 'WebP'
|
||||
png_link = f'<a href="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/{icon_name}.png">PNG</a>' if formats['png'] else 'PNG'
|
||||
svg_link = f'<a href="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/{icon_name}.svg">SVG</a>' if formats['svg'] else 'SVG'
|
||||
|
||||
# Combine checkmarks and links (or just name if not available)
|
||||
webp_info = f'{webp_check} {webp_link}' if formats['webp'] else f'{webp_check} {webp_link}'
|
||||
png_info = f'{png_check} {png_link}' if formats['png'] else f'{png_check} {png_link}'
|
||||
svg_info = f'{svg_check} {svg_link}' if formats['svg'] else f'{svg_check} {svg_link}'
|
||||
|
||||
# Generate preview using WebP
|
||||
preview = f'<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/webp/{icon_name}.webp" height="50" alt="{icon_name}">'
|
||||
|
||||
return f"| {icon_name} | {webp_info} {png_info} {svg_info} | {preview} |"
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Define paths
|
||||
root = pathlib.Path(__file__).parent.resolve()
|
||||
tree_json_path = root.parent / "tree.json"
|
||||
template_path = root / "TEMPLATE.md"
|
||||
icons_md_path = root.parent / "ICONS.md"
|
||||
|
||||
# Load the tree.json data
|
||||
formats = read_tree_json(tree_json_path)
|
||||
|
||||
# Create a dictionary to hold icons by their base name (ignoring file extensions)
|
||||
icons_dict = {}
|
||||
|
||||
# Check the formats and group icons by their base name
|
||||
for ext, icons in formats.items():
|
||||
for icon in icons:
|
||||
base_name = icon.rsplit('.', 1)[0] # Get base name (without extension)
|
||||
if base_name not in icons_dict:
|
||||
icons_dict[base_name] = {'webp': False, 'png': False, 'svg': False}
|
||||
icons_dict[base_name][ext] = True
|
||||
|
||||
# Create table for all icons (unique names)
|
||||
table_rows = []
|
||||
|
||||
for icon_name in sorted(icons_dict.keys()):
|
||||
table_row = generate_table_row(icon_name, icons_dict[icon_name])
|
||||
table_rows.append(table_row)
|
||||
|
||||
# Prepare the table with header and rows
|
||||
table_header = "| Name | Links | Preview |"
|
||||
table_separator = "|------|-------|---------|"
|
||||
table_content = "\n".join(table_rows)
|
||||
table = f"{table_header}\n{table_separator}\n{table_content}"
|
||||
|
||||
# Read the template file
|
||||
with open(template_path, "r", encoding="UTF-8") as f:
|
||||
template = f.read()
|
||||
|
||||
# Find the line that starts with "<!-- ICONS -->"
|
||||
try:
|
||||
line_number = template.index("<!-- ICONS -->")
|
||||
except ValueError:
|
||||
print("<!-- ICONS --> placeholder not found in TEMPLATE.md")
|
||||
sys.exit(1)
|
||||
|
||||
# Insert the table after the placeholder
|
||||
updated_template = template[:line_number] + "<!-- ICONS -->\n" + table + template[line_number + len("<!-- ICONS -->"):]
|
||||
|
||||
# Write the new ICONS.md file
|
||||
with open(icons_md_path, "w", encoding="UTF-8") as f:
|
||||
f.write(updated_template)
|
||||
|
||||
print("ICONS.md has been successfully generated.")
|
Reference in New Issue
Block a user