#!/usr/bin/env python3 """ Bootstrap Icons Subset Generator Genera CSS y fuente optimizados solo con los iconos usados en roi-theme """ import re import subprocess import os # Directorio base BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # Iconos usados en roi-theme (extraidos con grep) USED_ICONS = [ "bi-arrow-counterclockwise", "bi-arrow-down-circle", "bi-arrow-right", "bi-arrows-angle-expand", "bi-arrows-expand", "bi-arrows-fullscreen", "bi-arrows-move", "bi-arrows-vertical", "bi-arrow-up", "bi-aspect-ratio", "bi-badge-ad", "bi-bar-chart", "bi-bootstrap", "bi-bounding-box", "bi-box-arrow-up-right", "bi-calendar3", "bi-calendar-check", "bi-card-image", "bi-card-text", "bi-c-circle", "bi-chat-dots", "bi-chat-dots-fill", "bi-chat-quote", "bi-chat-text", "bi-check-circle", "bi-chevron-down", "bi-clock", "bi-code", "bi-code-slash", "bi-cursor", "bi-display", "bi-envelope", "bi-envelope-fill", "bi-envelope-paper", "bi-exclamation-octagon", "bi-exclamation-triangle", "bi-eye", "bi-facebook", "bi-file-earmark-text", "bi-file-text", "bi-filetype-css", "bi-filetype-js", "bi-folder-fill", "bi-fonts", "bi-gear", "bi-geo-alt-fill", "bi-globe", "bi-graph-up", "bi-grid", "bi-grid-3x3-gap", "bi-hand-index", "bi-hourglass", "bi-hourglass-split", "bi-image", "bi-info-circle", "bi-input-cursor", "bi-input-cursor-text", "bi-instagram", "bi-key", "bi-layout-sidebar", "bi-layout-text-window-reverse", "bi-lightning", "bi-lightning-charge-fill", "bi-link", "bi-link-45deg", "bi-linkedin", "bi-list", "bi-list-nested", "bi-list-ol", "bi-list-ul", "bi-magic", "bi-megaphone", "bi-megaphone-fill", "bi-menu-button-wide", "bi-mouse", "bi-palette", "bi-person", "bi-person-lines-fill", "bi-phone", "bi-pin", "bi-power", "bi-send-fill", "bi-share", "bi-shield-check", "bi-shield-lock", "bi-shield-x", "bi-slash-circle", "bi-sliders", "bi-square", "bi-star", "bi-star-fill", "bi-stars", "bi-tablet", "bi-tag", "bi-tags", "bi-telephone-fill", "bi-text-center", "bi-text-paragraph", "bi-three-dots", "bi-toggle-on", "bi-twitter-x", "bi-type", "bi-whatsapp", "bi-x-circle", ] def read_original_css(): """Lee el CSS original de Bootstrap Icons""" css_path = os.path.join(BASE_DIR, "bootstrap-icons.min.css") with open(css_path, "r", encoding="utf-8") as f: return f.read() def extract_unicode_codes(css_content, icons): """Extrae los codigos unicode de los iconos usados""" codes = {} for icon in icons: # Buscar .bi-nombre::before{content:"\XXXX"} pattern = rf'\.{re.escape(icon)}::before\{{content:"\\([a-f0-9]+)"\}}' match = re.search(pattern, css_content, re.IGNORECASE) if match: codes[icon] = match.group(1) else: print(f"WARNING: No se encontro codigo para {icon}") return codes def generate_subset_css(codes): """Genera el CSS subset con solo los iconos usados""" css_lines = [] # Header css_lines.append("/*!") css_lines.append(" * Bootstrap Icons Subset - ROI Theme") css_lines.append(f" * {len(codes)} iconos de 2050 originales ({len(codes)/2050*100:.1f}%)") css_lines.append(" * Generado automaticamente - NO EDITAR") css_lines.append(" */") # @font-face con font-display: swap css_lines.append('@font-face{') css_lines.append(' font-display:swap;') css_lines.append(' font-family:bootstrap-icons;') css_lines.append(' src:url("fonts/bootstrap-icons-subset.woff2") format("woff2"),') css_lines.append(' url("fonts/bootstrap-icons-subset.woff") format("woff")') css_lines.append('}') # Estilos base para .bi css_lines.append('.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{') css_lines.append(' display:inline-block;') css_lines.append(' font-family:bootstrap-icons!important;') css_lines.append(' font-style:normal;') css_lines.append(' font-weight:400!important;') css_lines.append(' font-variant:normal;') css_lines.append(' text-transform:none;') css_lines.append(' line-height:1;') css_lines.append(' vertical-align:-.125em;') css_lines.append(' -webkit-font-smoothing:antialiased;') css_lines.append(' -moz-osx-font-smoothing:grayscale') css_lines.append('}') # Reglas para cada icono for icon, code in sorted(codes.items()): css_lines.append(f'.{icon}::before{{content:"\\{code}"}}') return '\n'.join(css_lines) def generate_minified_css(codes): """Genera CSS minificado""" parts = [] # @font-face parts.append('@font-face{font-display:swap;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons-subset.woff2") format("woff2"),url("fonts/bootstrap-icons-subset.woff") format("woff")}') # Estilos base parts.append('.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}') # Iconos icon_rules = [f'.{icon}::before{{content:"\\{code}"}}' for icon, code in sorted(codes.items())] parts.append(''.join(icon_rules)) return ''.join(parts) def create_font_subset(codes): """Crea subset de la fuente usando pyftsubset""" # Crear archivo con unicodes unicodes = ','.join([f'U+{code}' for code in codes.values()]) # Paths woff2_src = os.path.join(BASE_DIR, "fonts", "bootstrap-icons.woff2") woff2_dst = os.path.join(BASE_DIR, "fonts", "bootstrap-icons-subset.woff2") woff_src = os.path.join(BASE_DIR, "fonts", "bootstrap-icons.woff") woff_dst = os.path.join(BASE_DIR, "fonts", "bootstrap-icons-subset.woff") # Crear subset woff2 print(f"Creando subset woff2 con {len(codes)} glyphs...") cmd_woff2 = [ "pyftsubset", woff2_src, f"--unicodes={unicodes}", f"--output-file={woff2_dst}", "--flavor=woff2" ] subprocess.run(cmd_woff2, check=True) # Crear subset woff print(f"Creando subset woff con {len(codes)} glyphs...") cmd_woff = [ "pyftsubset", woff_src, f"--unicodes={unicodes}", f"--output-file={woff_dst}", "--flavor=woff" ] subprocess.run(cmd_woff, check=True) # Mostrar tamanos orig_size = os.path.getsize(woff2_src) / 1024 new_size = os.path.getsize(woff2_dst) / 1024 print(f"woff2: {orig_size:.1f} KB -> {new_size:.1f} KB ({(1-new_size/orig_size)*100:.1f}% reduccion)") def main(): print("=== Bootstrap Icons Subset Generator ===\n") # 1. Leer CSS original print("1. Leyendo CSS original...") css_content = read_original_css() # 2. Extraer codigos unicode print(f"2. Extrayendo codigos de {len(USED_ICONS)} iconos...") codes = extract_unicode_codes(css_content, USED_ICONS) print(f" Encontrados: {len(codes)} iconos") # 3. Generar CSS subset print("3. Generando CSS subset...") subset_css = generate_subset_css(codes) css_path = os.path.join(BASE_DIR, "bootstrap-icons-subset.css") with open(css_path, "w", encoding="utf-8") as f: f.write(subset_css) print(f" Guardado: bootstrap-icons-subset.css") # 4. Generar CSS minificado print("4. Generando CSS minificado...") min_css = generate_minified_css(codes) min_path = os.path.join(BASE_DIR, "bootstrap-icons-subset.min.css") with open(min_path, "w", encoding="utf-8") as f: f.write(min_css) orig_css_size = os.path.getsize(os.path.join(BASE_DIR, "bootstrap-icons.min.css")) / 1024 new_css_size = os.path.getsize(min_path) / 1024 print(f" CSS: {orig_css_size:.1f} KB -> {new_css_size:.1f} KB ({(1-new_css_size/orig_css_size)*100:.1f}% reduccion)") # 5. Crear font subset print("5. Creando font subset...") try: create_font_subset(codes) except Exception as e: print(f" ERROR: {e}") print(" Intenta ejecutar: pip install fonttools brotli") return print("\n=== COMPLETADO ===") print("Archivos generados:") print(" - bootstrap-icons-subset.css (legible)") print(" - bootstrap-icons-subset.min.css (minificado)") print(" - fonts/bootstrap-icons-subset.woff2") print(" - fonts/bootstrap-icons-subset.woff") if __name__ == "__main__": main()