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#
\\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 @@
+
+
+
+