From 2b737db3c9697e32c2bd2dd9c2028cf37af47123 Mon Sep 17 00:00:00 2001 From: David Rotermund Date: Sat, 28 Mar 2026 22:50:01 +0100 Subject: [PATCH] Logo Tools (#158) * Logo Tools * Moved to /tools/logo; Checks dependencies first and stops if they are not there; Added information about 3_remove_branding_from_projectpage.sh in README ; reordered the commands of 1_convert.sh to avoid the logo-horizontal.png problem --- tools/logo/.gitignore | 15 ++ tools/logo/1_convert.sh | 78 ++++++ tools/logo/2_install.sh | 16 ++ .../3_remove_branding_from_projectpage.sh | 28 ++ tools/logo/README.md | 26 ++ tools/logo/check_dependency.py | 74 ++++++ tools/logo/create_sw_versions.py | 178 +++++++++++++ tools/logo/generate_additional_logos.py | 89 +++++++ tools/logo/generate_favicons.py | 144 +++++++++++ tools/logo/generate_icons.py | 241 ++++++++++++++++++ tools/logo/logo.svg | 61 +++++ tools/logo/logo_full.svg | 66 +++++ 12 files changed, 1016 insertions(+) create mode 100644 tools/logo/.gitignore create mode 100755 tools/logo/1_convert.sh create mode 100755 tools/logo/2_install.sh create mode 100755 tools/logo/3_remove_branding_from_projectpage.sh create mode 100644 tools/logo/README.md create mode 100644 tools/logo/check_dependency.py create mode 100644 tools/logo/create_sw_versions.py create mode 100644 tools/logo/generate_additional_logos.py create mode 100644 tools/logo/generate_favicons.py create mode 100644 tools/logo/generate_icons.py create mode 100644 tools/logo/logo.svg create mode 100644 tools/logo/logo_full.svg diff --git a/tools/logo/.gitignore b/tools/logo/.gitignore new file mode 100644 index 0000000000..06e59a6e41 --- /dev/null +++ b/tools/logo/.gitignore @@ -0,0 +1,15 @@ +android-chrome-192x192.png +android-chrome-512x512.png +apple-touch-icon.png +favicon-16x16.png +favicon-32x32.png +favicon-compiled.svg +favicon-compiling.svg +favicon-error.svg +favicon.ico +favicon.svg +img +logo-horizontal.png +logo_sw.svg +mask-favicon.svg +overleaf_og_logo.png diff --git a/tools/logo/1_convert.sh b/tools/logo/1_convert.sh new file mode 100755 index 0000000000..93e23d87c6 --- /dev/null +++ b/tools/logo/1_convert.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status +set -e + +python3 check_dependency.py + +mkdir -p img/ol-brand + +echo "============================================================" +echo "Logo Conversion Script" +echo "============================================================" +echo "" + +# 1. Validation Checks +if [ ! -f "logo.svg" ] || [ ! -f "logo_full.svg" ]; then + echo "ERROR: Required SVG files (logo.svg or logo_full.svg) not found!" + exit 1 +fi + +echo "✓ Found source SVGs" + +# 2. Generate Favicons +cp logo.svg favicon.svg +python3 generate_favicons.py favicon.svg + +# 3. Create Black/White/Grey versions +echo "Creating black/white versions..." +python3 create_sw_versions.py logo.svg logo_sw.svg "#000000" +python3 create_sw_versions.py logo.svg mask-favicon.svg "#000000" +python3 create_sw_versions.py logo_full.svg img/ol-brand/overleaf-black.svg "#000000" +python3 create_sw_versions.py logo.svg img/ol-brand/overleaf-o-white.svg "#FFFFFF" +python3 create_sw_versions.py logo_full.svg img/ol-brand/overleaf-white.svg "#FFFFFF" +python3 create_sw_versions.py logo.svg img/ol-brand/overleaf-o-grey.svg "#808080" + +# 4. Generate standard icons (creates overleaf_og_logo.png) +echo "Generating standard icons from logo.svg..." +python3 generate_icons.py logo.svg + +# 5. Generate additional logos (FIX: Moved up so logo-horizontal.png exists before copying) +echo "Generating logo-horizontal.png from logo_full.svg..." +python3 generate_additional_logos.py logo_full.svg logo-horizontal.png 410 180 --export-area-drawing + +# 6. Final File Operations (Copying generated assets to destination) +echo "Finalizing assets..." +cp -f overleaf_og_logo.png img/ol-brand/ +cp -f logo-horizontal.png img/ol-brand/ +cp -f logo.svg img/ol-brand/overleaf-o.svg +cp -f logo_full.svg img/ol-brand/overleaf.svg +cp -f logo_full.svg img/ol-brand/overleaf-a-ds-solution-mallard.svg +cp -f logo_full.svg img/ol-brand/overleaf-green.svg +cp -f logo.svg img/ol-brand/overleaf-o-dark.svg + +# 7. Inline Python for Dark Mallard Variant +python3 - <<'PY' +from pathlib import Path +p = Path('img/ol-brand/overleaf-a-ds-solution-mallard.svg') +out = Path('img/ol-brand/overleaf-a-ds-solution-mallard-dark.svg') +if p.exists(): + s = p.read_text(encoding='utf-8') + s = s.replace('#0000ff', '#FFFFFF').replace('#0000FF', '#FFFFFF') + s = s.replace('#00aa00', '#13C965').replace('#00AA00', '#13C965') + s = s.replace('fill:#0000ff', 'fill:#FFFFFF').replace('fill:#0000FF', 'fill:#FFFFFF') + s = s.replace('stroke:#0000ff', 'stroke:#FFFFFF').replace('stroke:#0000FF', 'stroke:#FFFFFF') + s = s.replace('fill:#00aa00', 'fill:#13C965').replace('fill:#00AA00', 'fill:#13C965') + s = s.replace('stroke:#00aa00', 'stroke:#13C965').replace('stroke:#00AA00', 'stroke:#13C965') + out.write_text(s, encoding='utf-8') + print(f"✓ Created {out}") +else: + import sys + print(f"✗ Reference mallard file not found: {p}") + sys.exit(1) +PY + +echo "" +echo "============================================================" +echo "✓ All logos generated successfully!" +echo "============================================================" diff --git a/tools/logo/2_install.sh b/tools/logo/2_install.sh new file mode 100755 index 0000000000..a87a9631e3 --- /dev/null +++ b/tools/logo/2_install.sh @@ -0,0 +1,16 @@ +cp -f *.png ../../services/web/public/ +cp -f *.svg ../../services/web/public/ +cp -f *.ico ../../services/web/public/ + +cp -f img/ol-brand/*.png ../../services/web/public/img/ol-brand/ +cp -f img/ol-brand/*.svg ../../services/web/public/img/ol-brand/ + +# Explicitly include the dark Mallard variant +cp -f img/ol-brand/overleaf-a-ds-solution-mallard-dark.svg ../../services/web/public/img/ol-brand/ + +cp -f img/ol-brand/overleaf.svg ../../services/web/frontend/js/shared/svgs +cp -f img/ol-brand/overleaf-*.svg ../../services/web/frontend/js/shared/svgs + +# Explicitly copy the dark Mallard variant into shared svgs as well +cp -f img/ol-brand/overleaf-a-ds-solution-mallard-dark.svg ../../services/web/frontend/js/shared/svgs + diff --git a/tools/logo/3_remove_branding_from_projectpage.sh b/tools/logo/3_remove_branding_from_projectpage.sh new file mode 100755 index 0000000000..6f1638f250 --- /dev/null +++ b/tools/logo/3_remove_branding_from_projectpage.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Removes the ds-nav branding block from the project page sidebar component. +# Usage: bash 3_remove_branding_from_projectpage.sh + +TARGET="../../services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx" +BACKUP="$TARGET.bak" + +if [ ! -f "$TARGET" ]; then + echo "ERROR: target file not found: $TARGET" + exit 1 +fi + +echo "Creating backup: $BACKUP" +cp -a "$TARGET" "$BACKUP" + +# Use Perl to remove the
...
block across multiple lines. +# This will match regardless of attribute order or whitespace and removes the entire div including trailing newline. +perl -0777 -pe "s#]*className\\s*=\\s*[\'\"]ds-nav-ds-name[\'\"][^>]*>.*?\\n?##gis" "$BACKUP" > "$TARGET" + +if cmp -s "$TARGET" "$BACKUP"; then + echo "No ds-nav-ds-name block found; no changes made." + exit 0 +else + echo "✓ Removed ds-nav-ds-name block from $TARGET" + exit 0 +fi diff --git a/tools/logo/README.md b/tools/logo/README.md new file mode 100644 index 0000000000..79c0d15bfe --- /dev/null +++ b/tools/logo/README.md @@ -0,0 +1,26 @@ +# Overview + +We assume that two files exist: + +* logo.svg (short version of the logo) +* logo_full.svg (wide version of the logo with e.g. University of Somewhere) + +# 1_convert.sh + +It does a lot of converting logos from one format to another and scales & changes colors of the logo for producing the required set of files. + +In the moment it mainly uses inkscape and imagemagick as tools. + +# 2_install.sh + +This script copies the files to the correct positions before compilation. I seperated the scripts, since you may want to check the logos beforehand. Or do changes by hand. +e.g. I don't like overleaf-a-ds-solution-mallard.svg as a long logo. + +# 3_remove_branding_from_projectpage.sh + +This script is a maintenance utility designed to modify the project list sidebar by removing specific branding elements from the frontend source code. + +Target File: services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx + +Action: Removes the
...
block + diff --git a/tools/logo/check_dependency.py b/tools/logo/check_dependency.py new file mode 100644 index 0000000000..e48e08eccf --- /dev/null +++ b/tools/logo/check_dependency.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import subprocess +import sys +import os +from pathlib import Path + +# Detected ImageMagick command (either 'magick' for IM v7 or 'convert' for IM v6) +IMAGEMAGICK_CMD = None + +def check_dependencies(): + """Check if required tools are installed.""" + print("Checking dependencies...") + + # Core dependency we always require + dependencies = { + "inkscape": "Inkscape (for PNG generation)" + } + + missing = [] + + # Check inkscape + for cmd, description in dependencies.items(): + try: + subprocess.run( + [cmd, "--version"], + check=True, + capture_output=True, + text=True + ) + print(f" ✓ {description} - found") + except (subprocess.CalledProcessError, FileNotFoundError): + print(f" ✗ {description} - NOT FOUND") + missing.append(cmd) + + # Detect ImageMagick: prefer 'magick' (ImageMagick 7), fall back to 'convert' (ImageMagick 6) + global IMAGEMAGICK_CMD + IMAGEMAGICK_CMD = None + for cmd in ("magick", "convert"): + try: + subprocess.run([cmd, "--version"], check=True, capture_output=True, text=True) + IMAGEMAGICK_CMD = cmd + print(f" ✓ ImageMagick ({cmd}) - found") + break + except (subprocess.CalledProcessError, FileNotFoundError): + continue + + if IMAGEMAGICK_CMD is None: + print(f" ✗ ImageMagick - NOT FOUND (tried 'magick' and 'convert')") + missing.append("ImageMagick ('magick' or 'convert')") + + if missing: + print(f"\n{'='*60}") + print("ERROR: Missing required dependencies!") + print(f"{'='*60}") + print("\nThe following tools are required but not found:") + for tool in missing: + print(f" - {tool}") + print("\nInstallation instructions:") + print("\n Ubuntu/Debian:") + print(" sudo apt install inkscape imagemagick") + print("\n macOS:") + print(" brew install inkscape imagemagick") + print("\n Windows:") + print(" Download from:") + print(" - https://inkscape.org/") + print(" - https://imagemagick.org/") + print(f"{'='*60}\n") + sys.exit(1) + + print("✓ All dependencies found\n") + return True + +if __name__ == "__main__": + check_dependencies() diff --git a/tools/logo/create_sw_versions.py b/tools/logo/create_sw_versions.py new file mode 100644 index 0000000000..8077e0fc44 --- /dev/null +++ b/tools/logo/create_sw_versions.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Create black/white versions of SVG logos. + +This script converts all non-transparent and non-white colors to a specified color +(default: black), creating modified versions of the logos. + +Usage: + python create_sw_versions.py [color] + python create_sw_versions.py logo.svg logo_sw.svg + python create_sw_versions.py logo.svg logo_sw.svg "#FF0000" +""" + +import re +import sys +import os + + +def style_to_color(style_content, target_color): + """Convert colors in style attributes to target color.""" + # Replace fill colors + style_content = re.sub( + r'fill:\s*#[0-9a-fA-F]{6}', + f'fill:{target_color}', + style_content + ) + style_content = re.sub( + r'fill:\s*rgb\([^)]+\)', + f'fill:{target_color}', + style_content + ) + + # Replace stroke colors + style_content = re.sub( + r'stroke:\s*#[0-9a-fA-F]{6}', + f'stroke:{target_color}', + style_content + ) + style_content = re.sub( + r'stroke:\s*rgb\([^)]+\)', + f'stroke:{target_color}', + style_content + ) + + return style_content + + +def convert_to_target_color(svg_content, target_color="#000000"): + """ + Convert SVG colors to target color, keeping transparent and white as-is. + + Args: + svg_content: String containing SVG XML content + target_color: Target color (default: #000000 for black) + + Returns: + Modified SVG content with colors converted to target color + """ + # Handle fill and stroke attributes + svg_content = re.sub( + r'(fill|stroke)="(#(?!ffffff|FFFFFF|fff|FFF)[0-9a-fA-F]{6}|#(?!fff|FFF)[0-9a-fA-F]{3}|rgb\([^)]+\))"', + rf'\1="{target_color}"', + svg_content + ) + + # Handle style attributes + def replace_style(match): + style_content = match.group(1) + style_content = style_to_color(style_content, target_color) + return f'style="{style_content}"' + + svg_content = re.sub( + r'style="([^"]+)"', + replace_style, + svg_content + ) + + # Handle color attributes (less common) + svg_content = re.sub( + r'color="(#(?!ffffff|FFFFFF|fff|FFF)[0-9a-fA-F]{6}|rgb\([^)]+\))"', + f'color="{target_color}"', + svg_content + ) + + return svg_content + + +def process_svg_file(input_file, output_file, target_color="#000000"): + """ + Read SVG file, convert colors to target color, and save. + + Args: + input_file: Path to input SVG file + output_file: Path to output SVG file + target_color: Target color (default: #000000) + """ + try: + with open(input_file, 'r', encoding='utf-8') as f: + svg_content = f.read() + + # Convert colors + modified_content = convert_to_target_color(svg_content, target_color) + + # Write output + with open(output_file, 'w', encoding='utf-8') as f: + f.write(modified_content) + + print(f"✓ Created {output_file} with color {target_color}") + return True + + except FileNotFoundError: + print(f"✗ Error: {input_file} not found") + return False + except Exception as e: + print(f"✗ Error processing {input_file}: {e}") + return False + + +def main(): + """Main entry point.""" + if len(sys.argv) < 3: + print("=" * 60) + print("ERROR: Invalid usage!") + print("=" * 60) + print() + print("Usage:") + print(" python create_sw_versions.py [color]") + print() + print("Examples:") + print(" python create_sw_versions.py logo.svg logo_sw.svg") + print(" python create_sw_versions.py logo.svg logo_red.svg \"#FF0000\"") + print(" python create_sw_versions.py logo_full.svg logo_full_sw.svg \"#000000\"") + print() + print("Default color: #000000 (black)") + print("=" * 60) + sys.exit(1) + + input_file = sys.argv[1] + output_file = sys.argv[2] + target_color = sys.argv[3] if len(sys.argv) > 3 else "#000000" + + # Check if input file exists + if not os.path.isfile(input_file): + print("=" * 60) + print("ERROR: Input file not found!") + print("=" * 60) + print() + print(f"The file '{input_file}' does not exist.") + print() + print(f"Current directory: {os.getcwd()}") + print("=" * 60) + sys.exit(1) + + # Validate color format + if not re.match(r'^#[0-9a-fA-F]{6}$', target_color): + print("=" * 60) + print("WARNING: Invalid color format!") + print("=" * 60) + print() + print(f"Color '{target_color}' may not be valid.") + print("Expected format: #RRGGBB (e.g., #000000, #FF0000)") + print() + print("Proceeding anyway...") + print("=" * 60) + print() + + print(f"Converting: {input_file} -> {output_file}") + print(f"Target color: {target_color}") + print() + + if process_svg_file(input_file, output_file, target_color): + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/logo/generate_additional_logos.py b/tools/logo/generate_additional_logos.py new file mode 100644 index 0000000000..086f5c6986 --- /dev/null +++ b/tools/logo/generate_additional_logos.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Generate logo PNG from SVG using Inkscape. + +This script generates a PNG file from an SVG with specified dimensions and options. + +Usage: + python generate_additional_logos.py [options] + +Examples: + python generate_additional_logos.py logo_full.svg logo-horizontal.png 410 180 --export-area-drawing +""" + +import subprocess +import sys +import os + + +def main(): + """Main entry point.""" + if len(sys.argv) < 5: + print("=" * 60) + print("ERROR: Invalid usage!") + print("=" * 60) + print() + print("Usage:") + print(" python generate_additional_logos.py [options]") + print() + print("Example:") + print(" python generate_additional_logos.py logo_full.svg logo-horizontal.png 410 180 --export-area-drawing") + print() + print("Common options:") + print(" --export-area-drawing Export the bounding box of all objects") + print(" --export-area-page Export the page area (default)") + print("=" * 60) + sys.exit(1) + + input_file = sys.argv[1] + output_file = sys.argv[2] + + # Parse dimensions + try: + width = int(sys.argv[3]) + height = int(sys.argv[4]) + except ValueError: + print(f"ERROR: Invalid dimensions - width='{sys.argv[3]}', height='{sys.argv[4]}'") + print("Width and height must be integers.") + sys.exit(1) + + # Check if input file exists + if not os.path.isfile(input_file): + print(f"ERROR: Input file '{input_file}' not found") + print(f"Current directory: {os.getcwd()}") + sys.exit(1) + + # Build Inkscape command + command = [ + "inkscape", + f"--export-filename={output_file}", + f"--export-width={width}", + f"--export-height={height}", + "--export-background-opacity=0" + ] + + # Add extra options (e.g., --export-area-drawing) + if len(sys.argv) > 5: + command.extend(sys.argv[5:]) + + # Add input file + command.append(input_file) + + # Run Inkscape + print(f"Generating {output_file} ({width}×{height}) from {input_file}") + + try: + subprocess.run(command, check=True, capture_output=True, text=True) + print(f"✓ {output_file} created successfully") + sys.exit(0) + except subprocess.CalledProcessError as e: + print(f"✗ Error: {e.stderr}") + sys.exit(1) + except FileNotFoundError: + print("✗ Error: Inkscape not found") + print("Install: sudo apt install inkscape (or brew install inkscape)") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/logo/generate_favicons.py b/tools/logo/generate_favicons.py new file mode 100644 index 0000000000..d2e55e5c20 --- /dev/null +++ b/tools/logo/generate_favicons.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +Generate favicon variants with status indicators (compiled, compiling, error) +Usage: python generate_favicons.py favicon.svg +""" + +import sys +from pathlib import Path + +def create_compiled_icon(size): + """Create a green checkmark icon""" + circle = f'' + check_path = f'' + return f'{circle}{check_path}' + +def create_compiling_icon(size): + """Create rotating arrows icon (circular progress)""" + circle = f'' + r = size * 0.3 + cx, cy = size/2, size/2 + stroke_w = size * 0.06 + arrow_len = stroke_w * 2 + + # Two SHORT arcs with arrow heads, forming an open circle + # Arc 1: short arc at top-right (about 90 degrees) + arc1 = f'' + # Arrow head for arc1 (pointing downward/clockwise at right side) + arrow1 = f'' + + # Arc 2: short arc at bottom-left (about 90 degrees) + arc2 = f'' + # Arrow head for arc2 (pointing upward/clockwise at left side) + arrow2 = f'' + + return f'{circle}{arc1}{arrow1}{arc2}{arrow2}' + +def create_error_icon(size): + """Create a red X icon""" + circle = f'' + x1 = f'' + x2 = f'' + return f'{circle}{x1}{x2}' + +def extract_dimensions(svg_content): + """Extract width and height from SVG content""" + # Try to find viewBox + if 'viewBox=' in svg_content: + start = svg_content.find('viewBox=') + quote = svg_content[start + 8] + start = start + 9 + end = svg_content.find(quote, start) + viewbox = svg_content[start:end] + parts = viewbox.split() + return float(parts[2]), float(parts[3]) + + # Try to find width and height attributes + width = 100 + height = 100 + if 'width=' in svg_content: + start = svg_content.find('width=') + quote = svg_content[start + 6] + start = start + 7 + end = svg_content.find(quote, start) + width = float(svg_content[start:end].replace('px', '')) + + if 'height=' in svg_content: + start = svg_content.find('height=') + quote = svg_content[start + 7] + start = start + 8 + end = svg_content.find(quote, start) + height = float(svg_content[start:end].replace('px', '')) + + return width, height + +def add_overlay_to_svg(input_file, output_file, overlay_creator): + """Add an overlay icon to the SVG in the lower right corner""" + with open(input_file, 'r') as f: + svg_content = f.read() + + # Extract dimensions + width, height = extract_dimensions(svg_content) + + # Calculate overlay size and position (1/3 of the icon size) + overlay_size = min(width, height) / 3 + overlay_x = width - overlay_size - (overlay_size * 0.1) + overlay_y = height - overlay_size - (overlay_size * 0.1) + + # Create overlay group + overlay_svg = f'\n {overlay_creator(overlay_size)}\n' + + # Find the closing tag and insert overlay before it + closing_tag = '' + insert_pos = svg_content.rfind(closing_tag) + + if insert_pos == -1: + print(f"Error: Could not find closing tag in {input_file}") + return + + # Insert the overlay + new_content = svg_content[:insert_pos] + overlay_svg + '\n' + svg_content[insert_pos:] + + # Write output + with open(output_file, 'w') as f: + f.write(new_content) + + print(f"Created: {output_file}") + +def main(): + if len(sys.argv) != 2: + print("Usage: python generate_favicons.py favicon.svg") + sys.exit(1) + + input_file = Path(sys.argv[1]) + + if not input_file.exists(): + print(f"Error: {input_file} not found") + sys.exit(1) + + base_name = input_file.stem + output_dir = input_file.parent + + # Generate the three variants + add_overlay_to_svg( + input_file, + output_dir / f"{base_name}-compiled.svg", + create_compiled_icon + ) + + add_overlay_to_svg( + input_file, + output_dir / f"{base_name}-compiling.svg", + create_compiling_icon + ) + + add_overlay_to_svg( + input_file, + output_dir / f"{base_name}-error.svg", + create_error_icon + ) + + print("\nAll favicon variants created successfully!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/logo/generate_icons.py b/tools/logo/generate_icons.py new file mode 100644 index 0000000000..f9de5a36d7 --- /dev/null +++ b/tools/logo/generate_icons.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +""" +Icon Generator Tool + +This script generates various icon sizes from an SVG file using Inkscape, +and creates a favicon.ico file using ImageMagick. + +Usage: + python generate_icons.py + python generate_icons.py logo.svg +""" + +import subprocess +import sys +import os +from pathlib import Path + +# Detected ImageMagick command (either 'magick' for IM v7 or 'convert' for IM v6) +IMAGEMAGICK_CMD = None + + +def run_command(command, description): + """ + Execute a shell command and handle errors. + + Args: + command: List of command arguments + description: Human-readable description of the command + """ + print(f"Running: {description}") + try: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True + ) + print(f"✓ {description} completed successfully") + return True + except subprocess.CalledProcessError as e: + print(f"✗ Error during {description}:") + print(f" {e.stderr}") + return False + except FileNotFoundError: + print(f"✗ Error: Required tool not found for {description}") + print(f" Make sure the command '{command[0]}' is installed and in your PATH") + return False + + +def check_file_exists(filepath): + """Check if a file exists.""" + if not os.path.isfile(filepath): + print(f"\n{'='*60}") + print("ERROR: Input file not found!") + print(f"{'='*60}") + print(f"\nThe file '{filepath}' does not exist.\n") + print("Requirements:") + print(f" 1. Create or place '{filepath}' in the current directory") + print(f" 2. Ensure the file is a valid SVG file") + print(f"\nCurrent directory: {os.getcwd()}") + print(f"{'='*60}\n") + return False + return True + + +def check_dependencies(): + """Check if required tools are installed.""" + print("Checking dependencies...") + + # Core dependency we always require + dependencies = { + "inkscape": "Inkscape (for PNG generation)" + } + + missing = [] + + # Check inkscape + for cmd, description in dependencies.items(): + try: + subprocess.run( + [cmd, "--version"], + check=True, + capture_output=True, + text=True + ) + print(f" ✓ {description} - found") + except (subprocess.CalledProcessError, FileNotFoundError): + print(f" ✗ {description} - NOT FOUND") + missing.append(cmd) + + # Detect ImageMagick: prefer 'magick' (ImageMagick 7), fall back to 'convert' (ImageMagick 6) + global IMAGEMAGICK_CMD + IMAGEMAGICK_CMD = None + for cmd in ("magick", "convert"): + try: + subprocess.run([cmd, "--version"], check=True, capture_output=True, text=True) + IMAGEMAGICK_CMD = cmd + print(f" ✓ ImageMagick ({cmd}) - found") + break + except (subprocess.CalledProcessError, FileNotFoundError): + continue + + if IMAGEMAGICK_CMD is None: + print(f" ✗ ImageMagick - NOT FOUND (tried 'magick' and 'convert')") + missing.append("ImageMagick ('magick' or 'convert')") + + if missing: + print(f"\n{'='*60}") + print("ERROR: Missing required dependencies!") + print(f"{'='*60}") + print("\nThe following tools are required but not found:") + for tool in missing: + print(f" - {tool}") + print("\nInstallation instructions:") + print("\n Ubuntu/Debian:") + print(" sudo apt install inkscape imagemagick") + print("\n macOS:") + print(" brew install inkscape imagemagick") + print("\n Windows:") + print(" Download from:") + print(" - https://inkscape.org/") + print(" - https://imagemagick.org/") + print(f"{'='*60}\n") + return False + + print("✓ All dependencies found\n") + return True + + +def generate_icons(svg_file): + """ + Generate all icon sizes from an SVG file. + + Args: + svg_file: Path to the input SVG file + """ + print(f"Generating icons from: {svg_file}\n") + + # Define icon configurations + # Format: (filename, width, height, background) + icons = [ + ("android-chrome-512x512.png", 512, 512, "--export-background-opacity=0"), + ("android-chrome-192x192.png", 192, 192, "--export-background-opacity=0"), + ("apple-touch-icon.png", 180, 180, "--export-background=#FFFFFF"), + ("favicon-16x16.png", 16, 16, "--export-background-opacity=0"), + ("favicon-32x32.png", 32, 32, "--export-background-opacity=0"), + ("overleaf_og_logo.png", 256, 256, "--export-background-opacity=0"), + ] + + success_count = 0 + + # Generate PNG files using Inkscape + for filename, width, height, background in icons: + command = [ + "inkscape", + f"--export-filename={filename}", + f"--export-width={width}", + f"--export-height={height}", + background, + svg_file + ] + + if run_command(command, f"Generating {filename}"): + success_count += 1 + else: + print(f"Warning: Failed to generate {filename}") + + print(f"\n{success_count}/{len(icons)} PNG icons generated successfully\n") + + # Generate favicon.ico using ImageMagick + if check_file_exists("favicon-32x32.png"): + if IMAGEMAGICK_CMD is None: + print("\n✗ Cannot generate favicon.ico: no ImageMagick command detected") + return False + + command = [ + IMAGEMAGICK_CMD, + "favicon-32x32.png", + "favicon.ico" + ] + + if run_command(command, "Generating favicon.ico"): + print("\n✓ All icons generated successfully!") + return True + else: + print("\nWarning: PNG icons generated, but favicon.ico creation failed") + return False + else: + print("\n✗ Cannot generate favicon.ico: favicon-32x32.png not found") + return False + + +def main(): + """Main entry point.""" + if len(sys.argv) != 2: + print(f"\n{'='*60}") + print("ERROR: Invalid usage!") + print(f"{'='*60}") + print("\nUsage: python generate_icons.py ") + print("\nExample:") + print(" python generate_icons.py logo.svg") + print(f"{'='*60}\n") + sys.exit(1) + + svg_file = sys.argv[1] + + print("=" * 60) + print("Icon Generator Tool") + print("=" * 60) + print() + + # Check if input file exists FIRST + if not check_file_exists(svg_file): + sys.exit(1) + + # Check if required tools are installed + if not check_dependencies(): + sys.exit(1) + + # Proceed with icon generation + success = generate_icons(svg_file) + + print("=" * 60) + + if success: + print("\nGenerated files:") + print(" - android-chrome-512x512.png (512×512, transparent)") + print(" - android-chrome-192x192.png (192×192, transparent)") + print(" - apple-touch-icon.png (180×180, white background)") + print(" - favicon-16x16.png (16×16, transparent)") + print(" - favicon-32x32.png (32×32, transparent)") + print(" - overleaf_og_logo.png (256×256, transparent)") + print(" - favicon.ico") + sys.exit(0) + else: + print("\n✗ Icon generation completed with errors") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/logo/logo.svg b/tools/logo/logo.svg new file mode 100644 index 0000000000..1109a6d918 --- /dev/null +++ b/tools/logo/logo.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + diff --git a/tools/logo/logo_full.svg b/tools/logo/logo_full.svg new file mode 100644 index 0000000000..8afa264606 --- /dev/null +++ b/tools/logo/logo_full.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + +