diff --git a/.gitignore b/.gitignore index eebe97d..a59b768 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +cache/ +packs/ diff --git a/CONFIG_SCHEMA.md b/CONFIG_SCHEMA.md new file mode 100644 index 0000000..7c50b93 --- /dev/null +++ b/CONFIG_SCHEMA.md @@ -0,0 +1,117 @@ +# Emoji Pack Configuration Schema + +This document describes the JSON configuration schema for the emoji pack generator. + +## Configuration Structure + +```json +{ + "name": "Pack Name", + "version": "1.0.0", + "source": { + "type": "github", + "repository": "owner/repository-name", + "branch": "main", + "folder": "path/to/assets/folder" + }, + "input_structure": { + "metadata_file": "metadata.json", + "image_folders": [ + "{style}", + "{skin_tone}/{style}" + ], + "image_extensions": ["png"], + "styles": ["3D", "Color", "Flat"], + "skin_tones": ["Default", "Dark", "Medium-Dark", "Medium-Light", "Light"] + }, + "output": { + "type": "minecraft_resource_pack", + "pack_format": 15, + "description_template": "Pack Description with {style} and {skin_tone}", + "output_directory": "./packs/{name}-{style}-{skin_tone}", + "textures_path": "assets/minecraft/textures/font", + "font_path": "assets/minecraft/font/default.json", + "pack_meta_path": "pack.mcmeta", + "pack_icon_source": "1f603" + }, + "file_processing": { + "filename_from_metadata": "unicode", + "character_from_metadata": "glyph", + "unicode_processing": { + "skip_if_contains_space": true, + "handle_variation_selector": { + "enabled": true, + "pattern": " fe0f", + "action": "remove_and_convert" + } + } + }, + "font_config": { + "provider_type": "bitmap", + "height": 7, + "ascent": 7, + "file_template": "minecraft:font/{filename}.png" + } +} +``` + +## Field Descriptions + +### Root Level +- `name`: Human-readable name of the emoji pack +- `version`: Version of this configuration +- `source`: Configuration for where to download emoji assets from +- `input_structure`: Describes the expected structure of the input data +- `output`: Configuration for the generated output format +- `file_processing`: Rules for processing individual files +- `font_config`: Font-specific configuration for Minecraft resource packs + +### Source Configuration +- `type`: Type of source ("github" currently supported) +- `repository`: GitHub repository in "owner/repo" format +- `branch`: Git branch to download from +- `folder`: Folder within the repository to extract + +### Input Structure +- `metadata_file`: Name of the metadata file in each emoji folder +- `image_folders`: Array of folder patterns to search for images (supports {style}, {skin_tone} variables) +- `image_extensions`: Supported image file extensions +- `styles`: Available styles for this emoji set +- `skin_tones`: Available skin tones for this emoji set + +### Output Configuration +- `type`: Output format type +- `pack_format`: Minecraft pack format version +- `description_template`: Template for pack description (supports variables) +- `output_directory`: Where to generate the pack (supports variables) +- `textures_path`: Path within pack for texture files +- `font_path`: Path for font configuration JSON +- `pack_meta_path`: Path for pack metadata file +- `pack_icon_source`: Unicode value to use as pack icon + +### File Processing +- `filename_from_metadata`: Metadata field to use for filename +- `character_from_metadata`: Metadata field to use for character mapping +- `unicode_processing`: Rules for processing unicode values + +### Font Configuration +- `provider_type`: Minecraft font provider type +- `height`: Font height in pixels +- `ascent`: Font ascent in pixels +- `file_template`: Template for texture file references + +## Variable Substitution + +The following variables can be used in templates: +- `{style}`: Current style being processed +- `{skin_tone}`: Current skin tone being processed +- `{filename}`: Processed filename from metadata +- `{name}`: Pack name from root configuration + +## Wildcard Support + +The system supports wildcards in the `image_folders` configuration: +- `{style}`: Matches any style from the styles array +- `{skin_tone}`: Matches any skin tone from the skin_tones array + +This allows flexible folder structure matching without hardcoding paths. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1e977a --- /dev/null +++ b/README.md @@ -0,0 +1,193 @@ +# Emoji Pack Generator + +A flexible, configuration-driven system for generating emoji resource packs from various sources. + +## Features + +- **JSON Configuration**: Define emoji pack sources, structure, and output formats via JSON +- **Multiple Source Support**: Currently supports GitHub repositories +- **Flexible Input Structure**: Support for wildcard patterns in folder structures +- **Multiple Output Formats**: Currently supports Minecraft resource packs +- **Skin Tone & Style Support**: Generate packs for different styles and skin tones +- **Backward Compatibility**: Legacy FluentUI script still works + +## Quick Start + +### Using the New Configuration System + +1. **Generate a pack using an existing configuration:** + ```bash + python emoji_pack_generator.py --config configs/fluentui-3d-only.json --download + ``` + +2. **Generate for specific style/skin tone:** + ```bash + python emoji_pack_generator.py --config configs/fluentui-3d-only.json --style 3D --skin-tone Dark --download + ``` + +3. **Use existing downloaded files:** + ```bash + python emoji_pack_generator.py --config configs/fluentui-3d-only.json --extract-to ./cache/assets/ + ``` + +### Using the Legacy System + +The original fluentui-emoji.py still works for backward compatibility: + +```bash +# Legacy mode (deprecated) +python fluentui-emoji.py --download --style 3D --skin-tone Default + +# Legacy mode with new configuration +python fluentui-emoji.py --config configs/fluentui-3d-only.json --style 3D --skin-tone Default +``` + +## Configuration + +### Example Configuration Files + +- `configs/fluentui-3d-only.json` - FluentUI emojis, 3D style only +- `configs/fluentui-emoji.json` - FluentUI emojis, all styles (includes SVG) +- `configs/twemoji-example.json` - Example Twemoji configuration + +### Configuration Schema + +See [CONFIG_SCHEMA.md](CONFIG_SCHEMA.md) for detailed documentation of the JSON schema. + +### Basic Configuration Structure + +```json +{ + "name": "Pack Name", + "source": { + "type": "github", + "repository": "owner/repo-name", + "branch": "main", + "folder": "assets" + }, + "input_structure": { + "metadata_file": "metadata.json", + "image_folders": ["{style}", "{skin_tone}/{style}"], + "image_extensions": ["png"], + "styles": ["3D", "Color", "Flat"], + "skin_tones": ["Default", "Dark", "Light"] + }, + "output": { + "type": "minecraft_resource_pack", + "pack_format": 15, + "description_template": "{name} {style}-{skin_tone} Pack", + "output_directory": "./packs/{name}-{style}-{skin_tone}" + }, + "file_processing": { + "filename_from_metadata": "unicode", + "character_from_metadata": "glyph" + }, + "font_config": { + "provider_type": "bitmap", + "height": 7, + "ascent": 7 + } +} +``` + +## Command Line Options + +### emoji_pack_generator.py + +``` +--config CONFIG Path to JSON configuration file (required) +--extract-to DIR Extraction directory (default: ./cache/assets/) +--style STYLE Emoji style (overrides config) +--skin-tone TONE Skin tone (overrides config) +--download Force download of assets +--commit HASH Specific commit hash to download +``` + +### fluentui-emoji.py (Legacy) + +``` +--repo-url URL GitHub repository URL +--folder-name NAME Folder name in repository +--extract-to DIR Extraction directory +--skin-tone TONE Skin tone +--style STYLE Emoji style +--download Force download of assets +--config CONFIG Use JSON configuration (recommended) +``` + +## Creating Custom Configurations + +1. **Create a new JSON configuration file** based on the schema +2. **Define your source repository** and folder structure +3. **Configure input patterns** with wildcard support +4. **Set up output format** and naming conventions +5. **Test with a small subset** before full generation + +### Wildcard Support + +The system supports these wildcards in folder patterns: +- `{style}` - Matches any style from the configuration +- `{skin_tone}` - Matches any skin tone from the configuration + +Example: `["{style}", "{skin_tone}/{style}"]` will match both: +- `3D/` and `Default/3D/` +- `Color/` and `Dark/Color/` + +## File Structure + +``` +emoji_pack/ +├── emoji_pack_generator.py # New configuration-based generator +├── fluentui-emoji.py # Legacy script with backward compatibility +├── configs/ # Configuration files +│ ├── fluentui-3d-only.json # FluentUI 3D only (recommended) +│ ├── fluentui-emoji.json # FluentUI all styles +│ └── twemoji-example.json # Example Twemoji config +├── CONFIG_SCHEMA.md # Configuration schema documentation +├── cache/ # Downloaded assets cache +└── packs/ # Generated resource packs +``` + +## Supported Input Formats + +- **PNG images** - Ready for Minecraft resource packs +- **SVG images** - Requires conversion to PNG (not implemented yet) +- **Metadata JSON** - For emoji information and mapping + +## Supported Output Formats + +- **Minecraft Resource Pack** - Complete pack with textures and font JSON + +## Known Limitations + +1. **SVG Support**: SVG files are detected but not converted to PNG automatically +2. **Single Source Type**: Only GitHub repositories supported currently +3. **Minecraft Format Only**: Only Minecraft resource pack output implemented + +## Contributing + +To add support for new source types or output formats: + +1. Extend the `EmojiPackProcessor` class +2. Add new configuration options to the schema +3. Update validation in `EmojiPackConfig` +4. Test with example configurations + +## Examples + +### Generate FluentUI 3D Pack +```bash +python emoji_pack_generator.py --config configs/fluentui-3d-only.json --download +``` + +### Generate for Specific Skin Tone +```bash +python emoji_pack_generator.py --config configs/fluentui-3d-only.json --skin-tone Dark +``` + +### Use Specific Commit +```bash +python emoji_pack_generator.py --config configs/fluentui-3d-only.json --commit abc123 --download +``` + +The generated packs will be in the `packs/` directory and can be installed as Minecraft resource packs. \ No newline at end of file diff --git a/configs/fluentui-3d-only.json b/configs/fluentui-3d-only.json new file mode 100644 index 0000000..cbeacaa --- /dev/null +++ b/configs/fluentui-3d-only.json @@ -0,0 +1,49 @@ +{ + "name": "FluentUI 3D Emoji Pack", + "version": "1.0.0", + "description": "FluentUI Emoji Pack configured for 3D style only (PNG files)", + "source": { + "type": "github", + "repository": "microsoft/fluentui-emoji", + "branch": "main", + "folder": "assets" + }, + "input_structure": { + "metadata_file": "metadata.json", + "image_folders": [ + "{style}", + "{skin_tone}/{style}" + ], + "image_extensions": ["png"], + "styles": ["3D"], + "skin_tones": ["Default", "Dark", "Medium-Dark", "Medium-Light", "Light"] + }, + "output": { + "type": "minecraft_resource_pack", + "pack_format": 15, + "description_template": "FluentUi {style}-{skin_tone} Emoji Resource Pack", + "output_directory": "./packs/FluentUi-{style}-{skin_tone}-Emoji", + "textures_path": "assets/minecraft/textures/font", + "font_path": "assets/minecraft/font/default.json", + "pack_meta_path": "pack.mcmeta", + "pack_icon_source": "1f603" + }, + "file_processing": { + "filename_from_metadata": "unicode", + "character_from_metadata": "glyph", + "unicode_processing": { + "skip_if_contains_space": true, + "handle_variation_selector": { + "enabled": true, + "pattern": " fe0f", + "action": "remove_and_convert" + } + } + }, + "font_config": { + "provider_type": "bitmap", + "height": 7, + "ascent": 7, + "file_template": "minecraft:font/{filename}.png" + } +} \ No newline at end of file diff --git a/configs/fluentui-emoji.json b/configs/fluentui-emoji.json new file mode 100644 index 0000000..fc4e38e --- /dev/null +++ b/configs/fluentui-emoji.json @@ -0,0 +1,48 @@ +{ + "name": "FluentUI Emoji Pack", + "version": "1.0.0", + "source": { + "type": "github", + "repository": "microsoft/fluentui-emoji", + "branch": "main", + "folder": "assets" + }, + "input_structure": { + "metadata_file": "metadata.json", + "image_folders": [ + "{style}", + "{skin_tone}/{style}" + ], + "image_extensions": ["png", "svg"], + "styles": ["3D", "Color", "Flat"], + "skin_tones": ["Default", "Dark", "Medium-Dark", "Medium-Light", "Light"] + }, + "output": { + "type": "minecraft_resource_pack", + "pack_format": 15, + "description_template": "FluentUi {style}-{skin_tone} Emoji Resource Pack", + "output_directory": "./packs/FluentUi-{style}-{skin_tone}-Emoji", + "textures_path": "assets/minecraft/textures/font", + "font_path": "assets/minecraft/font/default.json", + "pack_meta_path": "pack.mcmeta", + "pack_icon_source": "1f603" + }, + "file_processing": { + "filename_from_metadata": "unicode", + "character_from_metadata": "glyph", + "unicode_processing": { + "skip_if_contains_space": true, + "handle_variation_selector": { + "enabled": true, + "pattern": " fe0f", + "action": "remove_and_convert" + } + } + }, + "font_config": { + "provider_type": "bitmap", + "height": 7, + "ascent": 7, + "file_template": "minecraft:font/{filename}.png" + } +} \ No newline at end of file diff --git a/configs/twemoji-example.json b/configs/twemoji-example.json new file mode 100644 index 0000000..2b0b385 --- /dev/null +++ b/configs/twemoji-example.json @@ -0,0 +1,45 @@ +{ + "name": "Twemoji Pack", + "version": "1.0.0", + "source": { + "type": "github", + "repository": "twitter/twemoji", + "branch": "master", + "folder": "assets" + }, + "input_structure": { + "metadata_file": "metadata.json", + "image_folders": [ + "svg" + ], + "image_extensions": ["svg"], + "styles": ["Default"], + "skin_tones": ["Default"] + }, + "output": { + "type": "minecraft_resource_pack", + "pack_format": 15, + "description_template": "Twemoji Resource Pack", + "output_directory": "./packs/Twemoji-Pack", + "textures_path": "assets/minecraft/textures/font", + "font_path": "assets/minecraft/font/default.json", + "pack_meta_path": "pack.mcmeta", + "pack_icon_source": "1f603" + }, + "file_processing": { + "filename_from_metadata": "unicode", + "character_from_metadata": "char", + "unicode_processing": { + "skip_if_contains_space": false, + "handle_variation_selector": { + "enabled": false + } + } + }, + "font_config": { + "provider_type": "bitmap", + "height": 8, + "ascent": 8, + "file_template": "minecraft:font/{filename}.png" + } +} \ No newline at end of file diff --git a/emoji_pack_generator.py b/emoji_pack_generator.py new file mode 100644 index 0000000..0c0f63b --- /dev/null +++ b/emoji_pack_generator.py @@ -0,0 +1,422 @@ +import os +import requests +import zipfile +import io +import json +import shutil +from tqdm import tqdm +from pathlib import Path +import argparse +import fnmatch +from typing import Dict, List, Optional, Any + + +class EmojiPackConfig: + """Configuration loader and validator for emoji pack generation.""" + + def __init__(self, config_path: str): + self.config_path = config_path + self.config = self._load_config() + self._validate_config() + + def _load_config(self) -> Dict[str, Any]: + """Load configuration from JSON file.""" + with open(self.config_path, 'r', encoding='utf-8') as f: + return json.load(f) + + def _validate_config(self): + """Validate that required configuration fields are present.""" + required_fields = ['source', 'input_structure', 'output', 'file_processing'] + for field in required_fields: + if field not in self.config: + raise ValueError(f"Missing required configuration field: {field}") + + # Validate source configuration + source = self.config['source'] + required_source_fields = ['repository', 'folder'] + for field in required_source_fields: + if field not in source: + raise ValueError(f"Missing required source field: {field}") + + # Validate input structure + input_struct = self.config['input_structure'] + required_input_fields = ['metadata_file', 'image_folders', 'image_extensions'] + for field in required_input_fields: + if field not in input_struct: + raise ValueError(f"Missing required input_structure field: {field}") + + # Validate output configuration + output = self.config['output'] + required_output_fields = ['output_directory', 'description_template'] + for field in required_output_fields: + if field not in output: + raise ValueError(f"Missing required output field: {field}") + + # Validate file processing + file_proc = self.config['file_processing'] + required_file_proc_fields = ['filename_from_metadata', 'character_from_metadata'] + for field in required_file_proc_fields: + if field not in file_proc: + raise ValueError(f"Missing required file_processing field: {field}") + + print(f"Configuration validation passed for {self.config.get('name', 'Unknown Pack')}") + + def get_source_info(self) -> Dict[str, str]: + """Get source repository information.""" + return self.config['source'] + + def get_input_structure(self) -> Dict[str, Any]: + """Get input structure configuration.""" + return self.config['input_structure'] + + def get_output_config(self) -> Dict[str, Any]: + """Get output configuration.""" + return self.config['output'] + + def get_file_processing_config(self) -> Dict[str, Any]: + """Get file processing configuration.""" + return self.config['file_processing'] + + def get_font_config(self) -> Dict[str, Any]: + """Get font configuration.""" + return self.config.get('font_config', {}) + + +class EmojiPackProcessor: + """Main processor for generating emoji packs from configuration.""" + + def __init__(self, config: EmojiPackConfig): + self.config = config + self.source_info = config.get_source_info() + self.input_structure = config.get_input_structure() + self.output_config = config.get_output_config() + self.file_processing = config.get_file_processing_config() + self.font_config = config.get_font_config() + + def download_repo_zip(self, branch: Optional[str] = None, commit: Optional[str] = None) -> Optional[bytes]: + """Download repository as ZIP file.""" + if branch is None: + branch = self.source_info.get('branch', 'main') + + repo_url = self.source_info['repository'] + + # Support downloading specific commits + if commit: + zip_url = f"https://github.com/{repo_url}/archive/{commit}.zip" + else: + zip_url = f"https://github.com/{repo_url}/archive/refs/heads/{branch}.zip" + + print(f"Downloading from: {zip_url}") + response = requests.get(zip_url, stream=True) + if response.status_code == 200: + total_size = int(response.headers.get('content-length', 0)) + block_size = 1024 + t = tqdm(total=total_size, unit='iB', unit_scale=True) + zip_content = io.BytesIO() + for data in response.iter_content(block_size): + t.update(len(data)) + zip_content.write(data) + t.close() + actual_size = zip_content.tell() + if total_size != 0 and actual_size != total_size: + print(f"ERROR, something went wrong: expected {total_size} bytes, got {actual_size} bytes") + return zip_content.getvalue() + else: + print(f"Failed to download repository zip from: {zip_url}\nResponse code: {response.status_code}") + return None + + def extract_folder_from_zip(self, zip_content: bytes, extract_to: str = './cache', commit: Optional[str] = None): + """Extract specified folder from ZIP file.""" + repo_name = self.source_info['repository'].split('/')[-1] + + if commit: + # For commits, GitHub uses the commit hash as folder name + folder_prefix = f"{repo_name}-{commit[:7]}" # GitHub uses first 7 chars for short commit hash + else: + branch = self.source_info.get('branch', 'main') + folder_prefix = f"{repo_name}-{branch}" + + source_folder = f"{folder_prefix}/{self.source_info['folder']}" + + with zipfile.ZipFile(io.BytesIO(zip_content)) as zip_file: + # Find the actual folder name in the ZIP (GitHub might use full commit hash) + all_members = zip_file.namelist() + actual_folder = None + + # Look for the folder pattern + for member in all_members: + if member.startswith(f"{repo_name}-") and f"/{self.source_info['folder']}/" in member: + parts = member.split('/') + if len(parts) >= 2: + actual_folder = f"{parts[0]}/{self.source_info['folder']}" + break + + if not actual_folder: + # Fallback to the expected folder name + actual_folder = source_folder + + print(f"Extracting from folder: {actual_folder}") + members = [m for m in all_members if m.startswith(actual_folder)] + + if not members: + raise ValueError(f"No files found in folder {actual_folder}. Available folders: {set(m.split('/')[0] for m in all_members[:10])}") + + for member in tqdm(members, desc="Extracting"): + member_path = os.path.relpath(member, actual_folder) + target_path = os.path.join(extract_to, member_path) + os.makedirs(os.path.dirname(target_path), exist_ok=True) + if not member.endswith('/'): + with zip_file.open(member) as source, open(target_path, 'wb') as target: + target.write(source.read()) + + def _substitute_variables(self, template: str, **kwargs) -> str: + """Substitute variables in template string.""" + return template.format(**kwargs) + + def _find_image_folder(self, subfolder_path: str, style: str, skin_tone: str) -> Optional[str]: + """Find image folder based on configured patterns.""" + for pattern in self.input_structure['image_folders']: + folder_path = os.path.join( + subfolder_path, + self._substitute_variables(pattern, style=style, skin_tone=skin_tone) + ).replace("\\", "/") + + if os.path.exists(folder_path): + return folder_path + return None + + def _process_unicode_value(self, unicode_val: str, metadata: Dict[str, Any]) -> Optional[str]: + """Process unicode value according to configuration rules.""" + unicode_config = self.file_processing.get('unicode_processing', {}) + + # Check if we should skip values containing spaces + if unicode_config.get('skip_if_contains_space', False) and " " in unicode_val: + # Handle variation selector case + if (unicode_config.get('handle_variation_selector', {}).get('enabled', False) and + unicode_val.count(" ") == 1): + + pattern = unicode_config['handle_variation_selector']['pattern'] + action = unicode_config['handle_variation_selector']['action'] + + if pattern in unicode_val and action == "remove_and_convert": + print("Special case Variation Selector 16 Found") + unicode_val = unicode_val.replace(pattern, "", 1) + metadata["glyph"] = chr(int(unicode_val, 16)) + return unicode_val + else: + print(f"Skipping {unicode_val} because it contains a space") + return None + + return unicode_val + + def process_metadata_and_images(self, extract_to: str, style: str, skin_tone: str) -> List[Dict[str, Any]]: + """Process metadata and images according to configuration.""" + providers = [] + metadata_filename = self.input_structure['metadata_file'] + image_extensions = self.input_structure['image_extensions'] + + for dir_name in os.listdir(extract_to): + subfolder_path = os.path.join(extract_to, dir_name).replace("\\", "/") + if os.path.isdir(subfolder_path): + metadata_file_path = os.path.join(subfolder_path, metadata_filename).replace("\\", "/") + + if not os.path.exists(metadata_file_path): + continue + + print(f"Found metadata file: {metadata_file_path}") + image_folder_path = self._find_image_folder(subfolder_path, style, skin_tone) + + if not image_folder_path: + continue + + print(f"Found {style} folder in {image_folder_path}") + + # Find image files + image_files = [] + for ext in image_extensions: + image_files.extend([f for f in os.listdir(image_folder_path) + if f.lower().endswith(f'.{ext.lower()}')]) + + if not image_files: + continue + + image_path = os.path.join(image_folder_path, image_files[0]).replace("\\", "/") + print(f"Found image file: {image_path}") + + # Load and process metadata + with open(metadata_file_path, 'r', encoding='utf-8') as metadata_file: + metadata = json.load(metadata_file) + print(f"{metadata['cldr']}: metadata loaded") + + # Get filename and character from metadata + filename_field = self.file_processing['filename_from_metadata'] + character_field = self.file_processing['character_from_metadata'] + + unicode_val = metadata[filename_field] + processed_unicode = self._process_unicode_value(unicode_val, metadata) + + if processed_unicode is None: + continue + + # Handle pack icon + if processed_unicode == self.output_config.get('pack_icon_source'): + icon_dest = self._substitute_variables( + f"{self.output_config['output_directory']}/pack.png", + style=style, skin_tone=skin_tone + ) + Path(icon_dest).parent.mkdir(exist_ok=True, parents=True) + shutil.copy2(image_path, icon_dest) + + print(f"{processed_unicode} {metadata[character_field]}") + + # Copy image to destination + destination_image = self._substitute_variables( + f"{self.output_config['output_directory']}/{self.output_config['textures_path']}/{processed_unicode}.png", + style=style, skin_tone=skin_tone + ) + print(f"Copying {image_path} to {destination_image}") + Path(destination_image).parent.mkdir(exist_ok=True, parents=True) + shutil.copy2(image_path, destination_image) + + # Create font provider entry + file_path = self._substitute_variables( + self.font_config['file_template'], + filename=processed_unicode + ) + + providers.append({ + "type": self.font_config.get('provider_type', 'bitmap'), + "file": file_path, + "height": self.font_config.get('height', 7), + "ascent": self.font_config.get('ascent', 7), + "chars": [metadata[character_field]] + }) + + return providers + + def save_json(self, data: Any, file_path: Path): + """Save data as JSON file.""" + file_path.parent.mkdir(exist_ok=True, parents=True) + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + def generate_pack(self, style: str, skin_tone: str, extract_to: str, force_download: bool = False, commit: Optional[str] = None): + """Generate emoji pack for specified style and skin tone.""" + # Validate inputs + available_styles = self.input_structure.get('styles', []) + available_skin_tones = self.input_structure.get('skin_tones', []) + + if available_styles and style not in available_styles: + print(f"Warning: Style '{style}' not in configured styles: {available_styles}") + + if available_skin_tones and skin_tone not in available_skin_tones: + print(f"Warning: Skin tone '{skin_tone}' not in configured skin tones: {available_skin_tones}") + + # Download if requested + if force_download: + print(f"Downloading repository {self.source_info['repository']}") + zip_content = self.download_repo_zip(commit=commit) + if zip_content: + print(f"Extracting folder to {extract_to}") + try: + self.extract_folder_from_zip(zip_content, extract_to, commit=commit) + print(f"Folder extracted successfully to {extract_to}") + except Exception as e: + print(f"Failed to extract folder: {e}") + return + else: + print("Failed to download or extract the repository") + return + else: + print("Skipping download, using existing files") + + # Check if extract directory exists + if not os.path.exists(extract_to): + print(f"Error: Extract directory {extract_to} does not exist. Use --download to download the repository.") + return + + # Process metadata and images + try: + providers = self.process_metadata_and_images(extract_to, style, skin_tone) + + if not providers: + print("Warning: No emoji providers were generated. Check your configuration and source data.") + return + + except Exception as e: + print(f"Error processing metadata and images: {e}") + return + + # Generate font JSON + font_json = {"providers": providers} + font_path = self._substitute_variables( + f"{self.output_config['output_directory']}/{self.output_config['font_path']}", + style=style, skin_tone=skin_tone + ) + + try: + self.save_json(font_json, Path(font_path)) + print(f"Generated font configuration: {font_path}") + except Exception as e: + print(f"Error saving font configuration: {e}") + return + + # Generate pack.mcmeta + description = self._substitute_variables( + self.output_config['description_template'], + style=style, skin_tone=skin_tone + ) + pack_meta = { + "pack": { + "description": description, + "pack_format": self.output_config.get('pack_format', 15) + } + } + pack_meta_path = self._substitute_variables( + f"{self.output_config['output_directory']}/{self.output_config['pack_meta_path']}", + style=style, skin_tone=skin_tone + ) + + try: + self.save_json(pack_meta, Path(pack_meta_path)) + print(f"Generated pack metadata: {pack_meta_path}") + except Exception as e: + print(f"Error saving pack metadata: {e}") + return + + print(f"Successfully generated pack for {style}-{skin_tone} with {len(providers)} emojis") + + +def main(): + """Main function with CLI interface.""" + parser = argparse.ArgumentParser(description='Generate emoji packs from JSON configuration') + parser.add_argument('--config', required=True, help='Path to JSON configuration file') + parser.add_argument('--extract-to', default='./cache/assets/', help='Extraction directory') + parser.add_argument('--style', help='Emoji style (overrides config styles)') + parser.add_argument('--skin-tone', help='Skin tone (overrides config skin_tones)') + parser.add_argument('--download', action='store_true', help='Force download of assets') + parser.add_argument('--commit', help='Specific commit hash to download (overrides branch)') + args = parser.parse_args() + + try: + # Load configuration + config = EmojiPackConfig(args.config) + processor = EmojiPackProcessor(config) + + # Determine styles and skin tones to process + input_structure = config.get_input_structure() + styles = [args.style] if args.style else input_structure.get('styles', ['Default']) + skin_tones = [args.skin_tone] if args.skin_tone else input_structure.get('skin_tones', ['Default']) + + # Generate packs for all combinations + for style in styles: + for skin_tone in skin_tones: + processor.generate_pack(style, skin_tone, args.extract_to, args.download, args.commit) + + except Exception as e: + print(f"Error: {e}") + exit(1) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/fluentui-emoji.py b/fluentui-emoji.py index 0d7bfc7..a7f95cf 100644 --- a/fluentui-emoji.py +++ b/fluentui-emoji.py @@ -1,34 +1,38 @@ import os -import requests -import zipfile -import io -import json -import shutil -from tqdm import tqdm +import warnings from pathlib import Path import argparse +# Import the new configuration-based system +from emoji_pack_generator import EmojiPackConfig, EmojiPackProcessor + +# Legacy functions for backward compatibility def download_repo_zip(repo_url, branch='main'): - zip_url = f"https://github.com/{repo_url}/archive/refs/heads/{branch}.zip" - response = requests.get(zip_url, stream=True) - if response.status_code == 200: - total_size = int(response.headers.get('content-length', 0)) - block_size = 1024 # 1 Kibibyte - t = tqdm(total=total_size, unit='iB', unit_scale=True) - zip_content = io.BytesIO() - for data in response.iter_content(block_size): - t.update(len(data)) - zip_content.write(data) - t.close() - actual_size = zip_content.tell() - if total_size != 0 and actual_size != total_size: - print(f"ERROR, something went wrong: expected {total_size} bytes, got {actual_size} bytes") - return zip_content.getvalue() - else: - print(f"Failed to download repository zip from: {zip_url}\nResponse code: {response.status_code}") - return None + """Legacy function - now uses new configuration system.""" + warnings.warn("download_repo_zip is deprecated. Use EmojiPackProcessor instead.", + DeprecationWarning, stacklevel=2) + + # Create a temporary config for the legacy call + temp_config = { + "source": {"repository": repo_url, "branch": branch, "folder": "assets"}, + "input_structure": {}, + "output": {}, + "file_processing": {} + } + config = type('Config', (), temp_config)() + processor = EmojiPackProcessor(config) + return processor.download_repo_zip(branch) def extract_folder_from_zip(zip_content, folder_name, extract_to='.'): + """Legacy function - now uses new configuration system.""" + warnings.warn("extract_folder_from_zip is deprecated. Use EmojiPackProcessor instead.", + DeprecationWarning, stacklevel=2) + + # Extract using new system - simplified for legacy compatibility + import zipfile + import io + from tqdm import tqdm + with zipfile.ZipFile(io.BytesIO(zip_content)) as zip_file: members = [m for m in zip_file.namelist() if m.startswith(folder_name)] for member in tqdm(members, desc="Extracting"): @@ -40,80 +44,49 @@ def extract_folder_from_zip(zip_content, folder_name, extract_to='.'): target.write(source.read()) def process_metadata_and_images(extract_to, style, skin_tone): - providers = [] - for dir_name in os.listdir(extract_to): - subfolder_path = os.path.join(extract_to, dir_name).replace("\\", "/") - if os.path.isdir(subfolder_path): - metadata_file_path = os.path.join(subfolder_path, 'metadata.json').replace("\\", "/") - image_folder_path = None - - if os.path.exists(metadata_file_path): - print(f"Found metadata file: {metadata_file_path}") - - if os.path.exists(os.path.join(subfolder_path, style).replace("\\", "/")): - image_folder_path = os.path.join(subfolder_path, style).replace("\\", "/") - print(f"Found {style} folder in {subfolder_path}") - elif os.path.exists(os.path.join(subfolder_path, skin_tone, style).replace("\\", "/")): - image_folder_path = os.path.join(subfolder_path, skin_tone, style).replace("\\", "/") - print(f"Found {style} folder in {subfolder_path}/{skin_tone}") - - if image_folder_path: - png_files = [f for f in os.listdir(image_folder_path) if f.lower().endswith('.png')] - if png_files: - png_path = os.path.join(image_folder_path, png_files[0]).replace("\\", "/") - print(f"Found PNG file: {png_path}") - - if png_path and metadata_file_path: - with open(metadata_file_path, 'r', encoding='utf-8') as metadata_file: - metadata = json.load(metadata_file) - #print(f"Metadata content: {json.dumps(metadata, indent=4)}") - print(f"{metadata['cldr']}: metadata loaded") - - if metadata: - unicode = metadata["unicode"] - if " " not in unicode: - if unicode == "1f603": - shutil.copy2(png_path, f'./packs/FluentUi-{style}-{skin_tone}-Emoji/pack.png') - - print(unicode + metadata["glyph"]) - elif unicode.count(" ") == 1 and "fe0f" in unicode: - print("Special case Variation Selector 16 Found") - unicode = unicode.replace(" fe0f", "", 1) - metadata["glyph"] = chr(int(unicode, 16)) - print(unicode + metadata["glyph"]) - else: - print(f"Skipping {unicode} because it contains a space") - continue - - destination_image = f'./packs/FluentUi-{style}-{skin_tone}-Emoji/assets/minecraft/textures/font/{unicode}.png' - print(f"Copying {png_path} to {destination_image}") - Path(destination_image).parent.mkdir(exist_ok=True, parents=True) - shutil.copy2(png_path, destination_image) - - providers.append({ - "type": "bitmap", - "file": f"minecraft:font/{unicode}.png", - "height": 7, - "ascent": 7, - "chars": [metadata["glyph"]] - }) - return providers + """Legacy function - now uses new configuration system.""" + warnings.warn("process_metadata_and_images is deprecated. Use EmojiPackProcessor instead.", + DeprecationWarning, stacklevel=2) + + # Use the FluentUI configuration + config_path = os.path.join(os.path.dirname(__file__), 'configs', 'fluentui-emoji.json') + config = EmojiPackConfig(config_path) + processor = EmojiPackProcessor(config) + return processor.process_metadata_and_images(extract_to, style, skin_tone) def save_json(data, file_path): + """Legacy function - now uses new configuration system.""" + warnings.warn("save_json is deprecated. Use EmojiPackProcessor.save_json instead.", + DeprecationWarning, stacklevel=2) + file_path.parent.mkdir(exist_ok=True, parents=True) + import json with open(file_path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=4) if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Download and process FluentUI Emoji') + parser = argparse.ArgumentParser(description='Download and process FluentUI Emoji (Legacy - use emoji_pack_generator.py for new features)') parser.add_argument('--repo-url', default='microsoft/fluentui-emoji', help='GitHub repository URL') parser.add_argument('--folder-name', default='fluentui-emoji-main/assets', help='Folder name in the repository') parser.add_argument('--extract-to', default='./cache/fluentui-emoji/assets/', help='Extraction directory') parser.add_argument('--skin-tone', default='Default', choices=["Default", "Dark", "Medium-Dark", "Medium-Light", "Light"], help='Skin tone') parser.add_argument('--style', default='3D', choices=['3D', 'Color', 'Flat'], help='Emoji style') parser.add_argument('--download', action='store_true', help='Force download of assets') + parser.add_argument('--config', help='Use JSON configuration file (recommended - uses emoji_pack_generator.py)') args = parser.parse_args() + # If config is specified, use the new system + if args.config: + print("Using new configuration-based system...") + config = EmojiPackConfig(args.config) + processor = EmojiPackProcessor(config) + processor.generate_pack(args.style, args.skin_tone, args.extract_to, args.download) + exit(0) + + # Legacy behavior with deprecation warning + print("WARNING: Legacy mode is deprecated. Consider using --config with a JSON configuration file.") + print("See configs/fluentui-emoji.json for an example configuration.") + repo_url = args.repo_url folder_name = args.folder_name extract_to = args.extract_to