Merge pull request #286 from presenton/development

New templates and fields in /generate endpoint
This commit is contained in:
Saurav Niraula 2025-09-12 02:42:40 +05:45 committed by GitHub
commit c1671c4380
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3084 changed files with 3027 additions and 1560 deletions

View file

@ -6,9 +6,7 @@ RUN apt-get update && apt-get install -y \
curl \
libreoffice \
fontconfig \
imagemagick
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
chromium
# Install Node.js 20 using NodeSource repository
@ -22,7 +20,7 @@ WORKDIR /app
# Set environment variables
ENV APP_DATA_DIRECTORY=/app_data
ENV TEMP_DIRECTORY=/tmp/presenton
# ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Install ollama
@ -31,7 +29,7 @@ RUN curl -fsSL https://ollama.com/install.sh | sh
# Install dependencies for FastAPI
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
pathvalidate pdfplumber chromadb sqlmodel \
anthropic google-genai openai fastmcp
anthropic google-genai openai fastmcp dirtyjson
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
# Install dependencies for Next.js
@ -39,8 +37,6 @@ WORKDIR /app/servers/nextjs
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome@136.0.7103.92 --install-deps
# Copy Next.js app
COPY servers/nextjs/ /app/servers/nextjs/

View file

@ -6,10 +6,7 @@ RUN apt-get update && apt-get install -y \
curl \
libreoffice \
fontconfig \
imagemagick
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
chromium
# Install Node.js 20 using NodeSource repository
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
@ -24,7 +21,7 @@ RUN ls -a
# Set environment variables
ENV APP_DATA_DIRECTORY=/app_data
ENV TEMP_DIRECTORY=/tmp/presenton
# ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Install ollama
RUN curl -fsSL http://ollama.com/install.sh | sh
@ -32,7 +29,7 @@ RUN curl -fsSL http://ollama.com/install.sh | sh
# Install dependencies for FastAPI
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
pathvalidate pdfplumber chromadb sqlmodel \
anthropic google-genai openai fastmcp
anthropic google-genai openai fastmcp dirtyjson
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
# Install dependencies for Next.js
@ -40,9 +37,6 @@ WORKDIR /node_dependencies
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install --verbose
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome@138.0.7204.94 --install-deps
RUN chmod -R 777 /node_dependencies
# Copy nginx configuration

View file

@ -165,11 +165,19 @@ Content-Type: `application/json`
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| prompt | string | Yes | The main topic or prompt for generating the presentation |
| n_slides | integer | No | Number of slides to generate (default: 8, min: 5, max: 15) |
| content | string | Yes | The content for generating the presentation |
| slides_markdown | string[] \| null | No | The markdown for the slides |
| instructions | string \| null | No | The instruction for generating the presentation |
| tone | string | No | The tone to use for the text (default: "default"). Available options: "default", "casual", "professional", "funny", "educational", "sales_pitch" |
| verbosity | string | No | How verbose the text should be (default: "standard"). Available options: "concise", "standard", "text-heavy" |
| web_search | boolean | No | Whether to enable web search (default: false) |
| n_slides | integer | No | Number of slides to generate (default: 8) |
| language | string | No | Language for the presentation (default: "English") |
| template | string | No | Presentation template (default: "general"). Available options: "classic", "general", "modern", "professional" + Custom templates |
| export_as | string | No | Export format ("pptx" or "pdf", default: "pptx") |
| template | string | No | Template to use for the presentation (default: "general") |
| include_table_of_contents | boolean | No | Whether to include a table of contents (default: false) |
| include_title_slide | boolean | No | Whether to include a title slide (default: true) |
| files | string[] \| null | No | Files to use for the presentation. Use /api/v1/ppt/files/upload to upload files |
| export_as | string | No | Export format (default: "pptx"). Available options: "pptx", "pdf" |
#### Response
@ -187,7 +195,7 @@ Content-Type: `application/json`
curl -X POST http://localhost:5000/api/v1/ppt/presentation/generate \
-H "Content-Type: application/json" \
-d '{
"prompt": "Introduction to Machine Learning",
"content": "Introduction to Machine Learning",
"n_slides": 5,
"language": "English",
"template": "general",
@ -200,7 +208,7 @@ curl -X POST http://localhost:5000/api/v1/ppt/presentation/generate \
```json
{
"presentation_id": "d3000f96-096c-4768-b67b-e99aed029b57",
"path": "/static/user_data/d3000f96-096c-4768-b67b-e99aed029b57/Introduction_to_Machine_Learning.pptx",
"path": "/app_data/d3000f96-096c-4768-b67b-e99aed029b57/Introduction_to_Machine_Learning.pptx",
"edit_path": "/presentation?id=d3000f96-096c-4768-b67b-e99aed029b57"
}
```

View file

@ -8,6 +8,8 @@ events {
}
http {
include /etc/nginx/mime.types; # Required for SVG mime type
default_type application/octet-stream; # Required for SVG mime type
client_max_body_size 100M;
server {

View file

@ -1,7 +1,9 @@
import asyncio
import json
import math
import traceback
import uuid
import dirtyjson
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
@ -64,7 +66,8 @@ async def stream_outlines(
presentation.tone,
presentation.verbosity,
presentation.instructions,
True,
presentation.include_title_slide,
presentation.web_search,
):
# Give control to the event loop
await asyncio.sleep(0)
@ -81,8 +84,11 @@ async def stream_outlines(
presentation_outlines_text += chunk
try:
presentation_outlines_json = json.loads(presentation_outlines_text)
presentation_outlines_json = dict(
dirtyjson.loads(presentation_outlines_text)
)
except Exception as e:
traceback.print_exc()
raise HTTPException(
status_code=400,
detail="Failed to generate presentation outlines. Please try again.",

View file

@ -6,6 +6,7 @@ from typing import List, Optional
from fastapi import APIRouter, UploadFile, File, HTTPException
from pydantic import BaseModel
from services.documents_loader import DocumentsLoader
from utils.asset_directory_utils import get_images_directory
import uuid
from constants.documents import PDF_MIME_TYPES
@ -31,28 +32,32 @@ async def process_pdf_slides(
):
"""
Process a PDF file to extract slide screenshots.
This endpoint:
1. Validates the uploaded PDF file
2. Uses ImageMagick to convert PDF pages to PNG images
3. Returns screenshot URLs for each slide/page
Note: Font installation is not needed since PDFs already have fonts embedded.
"""
# Validate PDF file
if pdf_file.content_type not in PDF_MIME_TYPES:
raise HTTPException(
status_code=400,
detail=f"Invalid file type. Expected PDF file, got {pdf_file.content_type}"
detail=f"Invalid file type. Expected PDF file, got {pdf_file.content_type}",
)
# Enforce 100MB size limit
if hasattr(pdf_file, "size") and pdf_file.size and pdf_file.size > (100 * 1024 * 1024):
if (
hasattr(pdf_file, "size")
and pdf_file.size
and pdf_file.size > (100 * 1024 * 1024)
):
raise HTTPException(
status_code=400,
detail="PDF file exceeded max upload size of 100 MB",
)
# Create temporary directory for processing
with tempfile.TemporaryDirectory() as temp_dir:
try:
@ -61,120 +66,51 @@ async def process_pdf_slides(
with open(pdf_path, "wb") as f:
pdf_content = await pdf_file.read()
f.write(pdf_content)
# Generate screenshots from PDF using ImageMagick
screenshot_paths = await _generate_pdf_screenshots(pdf_path, temp_dir)
screenshot_paths = await DocumentsLoader.get_page_images_from_pdf_async(
pdf_path, temp_dir
)
print(f"Generated {len(screenshot_paths)} PDF screenshots")
# Move screenshots to images directory and generate URLs
images_dir = get_images_directory()
presentation_id = uuid.uuid4()
presentation_images_dir = os.path.join(images_dir, str(presentation_id))
os.makedirs(presentation_images_dir, exist_ok=True)
slides_data = []
for i, screenshot_path in enumerate(screenshot_paths, 1):
# Move screenshot to permanent location
screenshot_filename = f"slide_{i}.png"
permanent_screenshot_path = os.path.join(presentation_images_dir, screenshot_filename)
if os.path.exists(screenshot_path) and os.path.getsize(screenshot_path) > 0:
permanent_screenshot_path = os.path.join(
presentation_images_dir, screenshot_filename
)
if (
os.path.exists(screenshot_path)
and os.path.getsize(screenshot_path) > 0
):
# Use shutil.copy2 instead of os.rename to handle cross-device moves
shutil.copy2(screenshot_path, permanent_screenshot_path)
screenshot_url = f"/app_data/images/{presentation_id}/{screenshot_filename}"
screenshot_url = (
f"/app_data/images/{presentation_id}/{screenshot_filename}"
)
else:
# Fallback if screenshot generation failed or file is empty placeholder
screenshot_url = "/static/images/placeholder.jpg"
slides_data.append(PdfSlideData(
slide_number=i,
screenshot_url=screenshot_url
))
slides_data.append(
PdfSlideData(slide_number=i, screenshot_url=screenshot_url)
)
return PdfSlidesResponse(
success=True,
slides=slides_data,
total_slides=len(slides_data)
success=True, slides=slides_data, total_slides=len(slides_data)
)
except Exception as e:
print(f"Error processing PDF slides: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Failed to process PDF: {str(e)}"
status_code=500, detail=f"Failed to process PDF: {str(e)}"
)
async def _generate_pdf_screenshots(pdf_path: str, temp_dir: str) -> List[str]:
"""Generate PNG screenshots of PDF pages using ImageMagick (same approach as PPTX endpoint)."""
screenshots_dir = os.path.join(temp_dir, "screenshots")
os.makedirs(screenshots_dir, exist_ok=True)
try:
# Convert PDF to individual PNG images using ImageMagick
print("Starting ImageMagick PNG conversion...")
try:
result = subprocess.run([
"convert",
"-density", "150", # Same DPI as PPTX endpoint
pdf_path,
os.path.join(screenshots_dir, "slide_%03d.png")
], check=True, capture_output=True, text=True, timeout=500)
print(f"ImageMagick conversion output: {result.stdout}")
if result.stderr:
print(f"ImageMagick conversion warnings: {result.stderr}")
except subprocess.TimeoutExpired:
raise Exception("ImageMagick PNG conversion timed out after 500 seconds")
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else str(e)
raise Exception(f"ImageMagick PNG conversion failed: {error_msg}")
# Find generated PNG files (ImageMagick creates slide_000.png, slide_001.png, etc.)
print("Checking for generated PNG files...")
png_files = sorted([f for f in os.listdir(screenshots_dir) if f.startswith("slide_") and f.endswith('.png')])
print(f"Generated PNG files: {png_files}")
if not png_files:
raise Exception("ImageMagick failed to generate any PNG files")
# Determine page count from generated files
page_count = len(png_files)
print(f"Determined {page_count} pages from ImageMagick output")
# Rename files from slide_000.png format to slide_1.png format expected by the API
# (Same renaming logic as PPTX endpoint)
print("Renaming PNG files to expected format...")
screenshot_paths = []
for i in range(page_count):
# ImageMagick generates slide_000.png, slide_001.png, etc.
source_file = f"slide_{i:03d}.png"
source_path = os.path.join(screenshots_dir, source_file)
# We need slide_1.png, slide_2.png, etc.
target_file = f"slide_{i+1}.png"
target_path = os.path.join(screenshots_dir, target_file)
if os.path.exists(source_path):
# Rename to expected format
shutil.move(source_path, target_path)
screenshot_paths.append(target_path)
print(f"✓ Renamed {source_file} to {target_file}")
else:
print(f"⚠ Warning: Expected file {source_file} not found, creating placeholder")
# Create empty placeholder
with open(target_path, 'w') as f:
f.write("")
screenshot_paths.append(target_path)
print(f"Successfully generated {len(screenshot_paths)} PDF page screenshots")
return screenshot_paths
except Exception as e:
# Re-raise the specific exceptions we've already handled
if "timed out" in str(e) or "failed:" in str(e):
raise
# Handle any other unexpected exceptions
raise Exception(f"PDF screenshot generation failed: {str(e)}")

View file

@ -12,6 +12,7 @@ import asyncio
import xml.etree.ElementTree as ET
import re
from services.documents_loader import DocumentsLoader
from utils.asset_directory_utils import get_images_directory
import uuid
from constants.documents import POWERPOINT_TYPES
@ -28,7 +29,9 @@ class SlideData(BaseModel):
class FontAnalysisResult(BaseModel):
internally_supported_fonts: List[Dict[str, str]] # [{"name": "Open Sans", "google_fonts_url": "..."}]
internally_supported_fonts: List[
Dict[str, str]
] # [{"name": "Open Sans", "google_fonts_url": "..."}]
not_supported_fonts: List[str] # ["Custom Font Name"]
@ -38,30 +41,62 @@ class PptxSlidesResponse(BaseModel):
total_slides: int
fonts: Optional[FontAnalysisResult] = None
# NEW: Fonts-only router and response for PPTX
class PptxFontsResponse(BaseModel):
success: bool
fonts: FontAnalysisResult
PPTX_FONTS_ROUTER = APIRouter(prefix="/pptx-fonts", tags=["PPTX Fonts"])
# NEW: Normalize font family names by removing style/weight/stretch descriptors and splitting camel case
_STYLE_TOKENS = {
# styles
"italic", "italics", "ital", "oblique", "roman",
"italic",
"italics",
"ital",
"oblique",
"roman",
# combined style shortcuts
"bolditalic", "bolditalics",
"bolditalic",
"bolditalics",
# weights
"thin", "hairline", "extralight", "ultralight", "light", "demilight", "semilight", "book",
"regular", "normal", "medium", "semibold", "demibold", "bold", "extrabold", "ultrabold",
"black", "extrablack", "ultrablack", "heavy",
"thin",
"hairline",
"extralight",
"ultralight",
"light",
"demilight",
"semilight",
"book",
"regular",
"normal",
"medium",
"semibold",
"demibold",
"bold",
"extrabold",
"ultrabold",
"black",
"extrablack",
"ultrablack",
"heavy",
# width/stretch
"narrow", "condensed", "semicondensed", "extracondensed", "ultracondensed",
"expanded", "semiexpanded", "extraexpanded", "ultraexpanded",
"narrow",
"condensed",
"semicondensed",
"extracondensed",
"ultracondensed",
"expanded",
"semiexpanded",
"extraexpanded",
"ultraexpanded",
}
# Modifiers commonly used with style tokens
_STYLE_MODIFIERS = {"semi", "demi", "extra", "ultra"}
def _insert_spaces_in_camel_case(value: str) -> str:
# Insert space before capital letters preceded by lowercase or digits (e.g., MontserratBold -> Montserrat Bold)
value = re.sub(r"(?<=[a-z0-9])([A-Z])", r" \1", value)
@ -69,6 +104,7 @@ def _insert_spaces_in_camel_case(value: str) -> str:
value = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", value)
return value
def normalize_font_family_name(raw_name: str) -> str:
if not raw_name:
return raw_name
@ -111,69 +147,69 @@ def normalize_font_family_name(raw_name: str) -> str:
def extract_fonts_from_oxml(xml_content: str) -> List[str]:
"""
Extract font names from OXML content.
Args:
xml_content: OXML content as string
Returns:
List of unique font names found in the OXML
"""
fonts = set()
try:
# Parse the XML content
root = ET.fromstring(xml_content)
# Define namespaces commonly used in OXML
namespaces = {
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
# Search for font references in various OXML elements
# Look for latin fonts
for font_elem in root.findall('.//a:latin', namespaces):
if 'typeface' in font_elem.attrib:
fonts.add(font_elem.attrib['typeface'])
for font_elem in root.findall(".//a:latin", namespaces):
if "typeface" in font_elem.attrib:
fonts.add(font_elem.attrib["typeface"])
# Look for east asian fonts
for font_elem in root.findall('.//a:ea', namespaces):
if 'typeface' in font_elem.attrib:
fonts.add(font_elem.attrib['typeface'])
for font_elem in root.findall(".//a:ea", namespaces):
if "typeface" in font_elem.attrib:
fonts.add(font_elem.attrib["typeface"])
# Look for complex script fonts
for font_elem in root.findall('.//a:cs', namespaces):
if 'typeface' in font_elem.attrib:
fonts.add(font_elem.attrib['typeface'])
for font_elem in root.findall(".//a:cs", namespaces):
if "typeface" in font_elem.attrib:
fonts.add(font_elem.attrib["typeface"])
# Look for font references in theme elements
for font_elem in root.findall('.//a:font', namespaces):
if 'typeface' in font_elem.attrib:
fonts.add(font_elem.attrib['typeface'])
for font_elem in root.findall(".//a:font", namespaces):
if "typeface" in font_elem.attrib:
fonts.add(font_elem.attrib["typeface"])
# Look for rPr (run properties) font references
for rpr_elem in root.findall('.//a:rPr', namespaces):
for font_elem in rpr_elem.findall('.//a:latin', namespaces):
if 'typeface' in font_elem.attrib:
fonts.add(font_elem.attrib['typeface'])
for rpr_elem in root.findall(".//a:rPr", namespaces):
for font_elem in rpr_elem.findall(".//a:latin", namespaces):
if "typeface" in font_elem.attrib:
fonts.add(font_elem.attrib["typeface"])
# Also search without namespace prefix for compatibility
for font_elem in root.findall('.//latin'):
if 'typeface' in font_elem.attrib:
fonts.add(font_elem.attrib['typeface'])
for font_elem in root.findall(".//latin"):
if "typeface" in font_elem.attrib:
fonts.add(font_elem.attrib["typeface"])
# Regex fallback for fonts that might be missed
font_pattern = r'typeface="([^"]+)"'
regex_fonts = re.findall(font_pattern, xml_content)
fonts.update(regex_fonts)
# Filter out system fonts and empty values
system_fonts = {'+mn-lt', '+mj-lt', '+mn-ea', '+mj-ea', '+mn-cs', '+mj-cs', ''}
system_fonts = {"+mn-lt", "+mj-lt", "+mn-ea", "+mj-ea", "+mn-cs", "+mj-cs", ""}
fonts = {font for font in fonts if font not in system_fonts and font.strip()}
return list(fonts)
except Exception as e:
print(f"Error extracting fonts from OXML: {e}")
return []
@ -182,21 +218,23 @@ def extract_fonts_from_oxml(xml_content: str) -> List[str]:
async def check_google_font_availability(font_name: str) -> bool:
"""
Check if a font is available in Google Fonts.
Args:
font_name: Name of the font to check
Returns:
True if font is available in Google Fonts, False otherwise
"""
try:
formatted_name = font_name.replace(' ', '+')
formatted_name = font_name.replace(" ", "+")
url = f"https://fonts.googleapis.com/css2?family={formatted_name}&display=swap"
async with aiohttp.ClientSession() as session:
async with session.head(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
async with session.head(
url, timeout=aiohttp.ClientTimeout(total=10)
) as response:
return response.status == 200
except Exception as e:
print(f"Error checking Google Font availability for {font_name}: {e}")
return False
@ -205,10 +243,10 @@ async def check_google_font_availability(font_name: str) -> bool:
async def analyze_fonts_in_all_slides(slide_xmls: List[str]) -> FontAnalysisResult:
"""
Analyze fonts across all slides and determine Google Fonts availability.
Args:
slide_xmls: List of OXML content strings from all slides
Returns:
FontAnalysisResult with supported and unsupported fonts
"""
@ -222,45 +260,40 @@ async def analyze_fonts_in_all_slides(slide_xmls: List[str]) -> FontAnalysisResu
normalized_fonts = {normalize_font_family_name(f) for f in raw_fonts}
# Remove empties if any
normalized_fonts = {f for f in normalized_fonts if f}
if not normalized_fonts:
return FontAnalysisResult(
internally_supported_fonts=[],
not_supported_fonts=[]
)
return FontAnalysisResult(internally_supported_fonts=[], not_supported_fonts=[])
# Check each normalized font's availability in Google Fonts concurrently
tasks = [check_google_font_availability(font) for font in normalized_fonts]
results = await asyncio.gather(*tasks)
internally_supported_fonts = []
not_supported_fonts = []
for font, is_available in zip(normalized_fonts, results):
if is_available:
formatted_name = font.replace(' ', '+')
formatted_name = font.replace(" ", "+")
google_fonts_url = f"https://fonts.googleapis.com/css2?family={formatted_name}&display=swap"
internally_supported_fonts.append({
"name": font,
"google_fonts_url": google_fonts_url
})
internally_supported_fonts.append(
{"name": font, "google_fonts_url": google_fonts_url}
)
else:
not_supported_fonts.append(font)
return FontAnalysisResult(
internally_supported_fonts=internally_supported_fonts,
not_supported_fonts=[]
internally_supported_fonts=internally_supported_fonts, not_supported_fonts=[]
)
@PPTX_SLIDES_ROUTER.post("/process", response_model=PptxSlidesResponse)
async def process_pptx_slides(
pptx_file: UploadFile = File(..., description="PPTX file to process"),
fonts: Optional[List[UploadFile]] = File(None, description="Optional font files")
fonts: Optional[List[UploadFile]] = File(None, description="Optional font files"),
):
"""
Process a PPTX file to extract slide screenshots and XML content.
This endpoint:
1. Validates the uploaded PPTX file
2. Installs any provided font files
@ -268,20 +301,24 @@ async def process_pptx_slides(
4. Uses LibreOffice to generate slide screenshots
5. Returns both screenshot URLs and XML content for each slide
"""
# Validate PPTX file
if pptx_file.content_type not in POWERPOINT_TYPES:
raise HTTPException(
status_code=400,
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}"
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}",
)
# Enforce 100MB size limit
if hasattr(pptx_file, "size") and pptx_file.size and pptx_file.size > (100 * 1024 * 1024):
if (
hasattr(pptx_file, "size")
and pptx_file.size
and pptx_file.size > (100 * 1024 * 1024)
):
raise HTTPException(
status_code=400,
detail="PPTX file exceeded max upload size of 100 MB",
)
# Create temporary directory for processing
with tempfile.TemporaryDirectory() as temp_dir:
if True:
@ -290,61 +327,82 @@ async def process_pptx_slides(
with open(pptx_path, "wb") as f:
pptx_content = await pptx_file.read()
f.write(pptx_content)
# Install fonts if provided
if fonts:
await _install_fonts(fonts, temp_dir)
# Extract slide XMLs from PPTX
slide_xmls = _extract_slide_xmls(pptx_path, temp_dir)
# Convert PPTX to PDF
pdf_path = await _convert_pptx_to_pdf(pptx_path, temp_dir)
# Generate screenshots using LibreOffice
screenshot_paths = await _generate_screenshots(pptx_path, temp_dir)
screenshot_paths = await DocumentsLoader.get_page_images_from_pdf_async(
pdf_path, temp_dir
)
print(f"Screenshot paths: {screenshot_paths}")
# Analyze fonts across all slides
font_analysis = await analyze_fonts_in_all_slides(slide_xmls)
print(f"Font analysis completed: {len(font_analysis.internally_supported_fonts)} supported, {len(font_analysis.not_supported_fonts)} not supported")
print(
f"Font analysis completed: {len(font_analysis.internally_supported_fonts)} supported, {len(font_analysis.not_supported_fonts)} not supported"
)
# Move screenshots to images directory and generate URLs
images_dir = get_images_directory()
presentation_id = uuid.uuid4()
presentation_images_dir = os.path.join(images_dir, str(presentation_id))
os.makedirs(presentation_images_dir, exist_ok=True)
slides_data = []
for i, (xml_content, screenshot_path) in enumerate(zip(slide_xmls, screenshot_paths), 1):
for i, (xml_content, screenshot_path) in enumerate(
zip(slide_xmls, screenshot_paths), 1
):
# Move screenshot to permanent location
screenshot_filename = f"slide_{i}.png"
permanent_screenshot_path = os.path.join(presentation_images_dir, screenshot_filename)
if os.path.exists(screenshot_path) and os.path.getsize(screenshot_path) > 0:
permanent_screenshot_path = os.path.join(
presentation_images_dir, screenshot_filename
)
if (
os.path.exists(screenshot_path)
and os.path.getsize(screenshot_path) > 0
):
# Use shutil.copy2 instead of os.rename to handle cross-device moves
shutil.copy2(screenshot_path, permanent_screenshot_path)
screenshot_url = f"/app_data/images/{presentation_id}/{screenshot_filename}"
screenshot_url = (
f"/app_data/images/{presentation_id}/{screenshot_filename}"
)
else:
# Fallback if screenshot generation failed or file is empty placeholder
screenshot_url = "/static/images/placeholder.jpg"
# Compute normalized fonts for this slide
raw_slide_fonts = extract_fonts_from_oxml(xml_content)
normalized_fonts = sorted({normalize_font_family_name(f) for f in raw_slide_fonts if f})
slides_data.append(SlideData(
slide_number=i,
screenshot_url=screenshot_url,
xml_content=xml_content,
normalized_fonts=normalized_fonts
))
normalized_fonts = sorted(
{normalize_font_family_name(f) for f in raw_slide_fonts if f}
)
slides_data.append(
SlideData(
slide_number=i,
screenshot_url=screenshot_url,
xml_content=xml_content,
normalized_fonts=normalized_fonts,
)
)
return PptxSlidesResponse(
success=True,
slides=slides_data,
total_slides=len(slides_data),
fonts=font_analysis
fonts=font_analysis,
)
# NEW: Fonts-only endpoint leveraging the same font extraction/analysis
@PPTX_FONTS_ROUTER.post("/process", response_model=PptxFontsResponse)
async def process_pptx_fonts(
@ -359,7 +417,7 @@ async def process_pptx_fonts(
if pptx_file.content_type not in POWERPOINT_TYPES:
raise HTTPException(
status_code=400,
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}"
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}",
)
# Create temporary directory for processing
@ -381,6 +439,7 @@ async def process_pptx_fonts(
fonts=font_analysis,
)
def _create_font_alias_config(raw_fonts: List[str]) -> str:
"""Create a temporary fontconfig configuration that aliases variant family names to normalized root families.
Returns the path to the config file.
@ -395,13 +454,16 @@ def _create_font_alias_config(raw_fonts: List[str]) -> str:
fd, fonts_conf_path = tempfile.mkstemp(prefix="fonts_alias_", suffix=".conf")
os.close(fd)
with open(fonts_conf_path, "w", encoding="utf-8") as cfg:
cfg.write("""<?xml version='1.0'?>
cfg.write(
"""<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
<include>/etc/fonts/fonts.conf</include>
""")
"""
)
for src, dst in mappings.items():
cfg.write(f"""
cfg.write(
f"""
<match target="pattern">
<test name="family" compare="eq">
<string>{src}</string>
@ -410,30 +472,34 @@ def _create_font_alias_config(raw_fonts: List[str]) -> str:
<string>{dst}</string>
</edit>
</match>
""")
"""
)
cfg.write("\n</fontconfig>\n")
return fonts_conf_path
async def _install_fonts(fonts: List[UploadFile], temp_dir: str) -> None:
"""Install provided font files to the system."""
fonts_dir = os.path.join(temp_dir, "fonts")
os.makedirs(fonts_dir, exist_ok=True)
for font_file in fonts:
# Save font file
font_path = os.path.join(fonts_dir, font_file.filename)
with open(font_path, "wb") as f:
font_content = await font_file.read()
f.write(font_content)
# Install font (copy to system fonts directory)
try:
subprocess.run([
"cp", font_path, "/usr/share/fonts/truetype/"
], check=True, capture_output=True)
subprocess.run(
["cp", font_path, "/usr/share/fonts/truetype/"],
check=True,
capture_output=True,
)
except subprocess.CalledProcessError as e:
print(f"Warning: Failed to install font {font_file.filename}: {e}")
# Refresh font cache
try:
subprocess.run(["fc-cache", "-f", "-v"], check=True, capture_output=True)
@ -445,44 +511,48 @@ def _extract_slide_xmls(pptx_path: str, temp_dir: str) -> List[str]:
"""Extract slide XML content from PPTX file."""
slide_xmls = []
extract_dir = os.path.join(temp_dir, "pptx_extract")
try:
# Unzip PPTX file
with zipfile.ZipFile(pptx_path, 'r') as zip_ref:
with zipfile.ZipFile(pptx_path, "r") as zip_ref:
zip_ref.extractall(extract_dir)
# Look for slides in ppt/slides/ directory
slides_dir = os.path.join(extract_dir, "ppt", "slides")
if not os.path.exists(slides_dir):
raise Exception("No slides directory found in PPTX file")
# Get all slide XML files and sort them numerically
slide_files = [f for f in os.listdir(slides_dir) if f.startswith("slide") and f.endswith(".xml")]
slide_files = [
f
for f in os.listdir(slides_dir)
if f.startswith("slide") and f.endswith(".xml")
]
slide_files.sort(key=lambda x: int(x.replace("slide", "").replace(".xml", "")))
# Read XML content from each slide
for slide_file in slide_files:
slide_path = os.path.join(slides_dir, slide_file)
with open(slide_path, 'r', encoding='utf-8') as f:
with open(slide_path, "r", encoding="utf-8") as f:
slide_xmls.append(f.read())
return slide_xmls
except Exception as e:
raise Exception(f"Failed to extract slide XMLs: {str(e)}")
async def _generate_screenshots(pptx_path: str, temp_dir: str) -> List[str]:
async def _convert_pptx_to_pdf(pptx_path: str, temp_dir: str) -> str:
"""Generate PNG screenshots of PPTX slides using LibreOffice + ImageMagick."""
screenshots_dir = os.path.join(temp_dir, "screenshots")
os.makedirs(screenshots_dir, exist_ok=True)
try:
# First, get the number of slides by extracting XMLs
slide_xmls = _extract_slide_xmls(pptx_path, temp_dir)
slide_count = len(slide_xmls)
# Build font alias config to force variant families to resolve to normalized root families
raw_fonts: List[str] = []
for xml in slide_xmls:
@ -491,23 +561,32 @@ async def _generate_screenshots(pptx_path: str, temp_dir: str) -> List[str]:
fonts_conf_path = _create_font_alias_config(raw_fonts)
env = os.environ.copy()
env["FONTCONFIG_FILE"] = fonts_conf_path
print(f"Found {slide_count} slides in presentation")
# Step 1: Convert PPTX to PDF using LibreOffice
print("Starting LibreOffice PDF conversion...")
pdf_filename = "temp_presentation.pdf"
pdf_path = os.path.join(screenshots_dir, pdf_filename)
try:
result = subprocess.run([
"libreoffice",
"--headless",
"--convert-to", "pdf",
"--outdir", screenshots_dir,
pptx_path
], check=True, capture_output=True, text=True, timeout=500, env=env)
result = subprocess.run(
[
"libreoffice",
"--headless",
"--convert-to",
"pdf",
"--outdir",
screenshots_dir,
pptx_path,
],
check=True,
capture_output=True,
text=True,
timeout=500,
env=env,
)
print(f"LibreOffice PDF conversion output: {result.stdout}")
if result.stderr:
print(f"LibreOffice PDF conversion warnings: {result.stderr}")
@ -516,74 +595,19 @@ async def _generate_screenshots(pptx_path: str, temp_dir: str) -> List[str]:
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else str(e)
raise Exception(f"LibreOffice PDF conversion failed: {error_msg}")
# Find the generated PDF file (LibreOffice uses original filename)
pdf_files = [f for f in os.listdir(screenshots_dir) if f.endswith('.pdf')]
pdf_files = [f for f in os.listdir(screenshots_dir) if f.endswith(".pdf")]
if not pdf_files:
raise Exception("LibreOffice failed to generate PDF file")
actual_pdf_path = os.path.join(screenshots_dir, pdf_files[0])
print(f"Generated PDF: {actual_pdf_path}")
# Step 2: Convert PDF to individual PNG images using ImageMagick
print("Starting ImageMagick PNG conversion...")
try:
result = subprocess.run([
"convert",
"-density", "150",
actual_pdf_path,
os.path.join(screenshots_dir, "slide_%03d.png")
], check=True, capture_output=True, text=True, timeout=500, env=env)
print(f"ImageMagick conversion output: {result.stdout}")
if result.stderr:
print(f"ImageMagick conversion warnings: {result.stderr}")
except subprocess.TimeoutExpired:
raise Exception("ImageMagick PNG conversion timed out after 120 seconds")
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else str(e)
raise Exception(f"ImageMagick PNG conversion failed: {error_msg}")
# Find generated PNG files (ImageMagick creates slide_000.png, slide_001.png, etc.)
print("Checking for generated PNG files...")
png_files = sorted([f for f in os.listdir(screenshots_dir) if f.startswith("slide_") and f.endswith('.png')])
print(f"Generated PNG files: {png_files}")
if not png_files:
raise Exception("ImageMagick failed to generate any PNG files")
# Rename files from slide_000.png format to slide_1.png format expected by the API
print("Renaming PNG files to expected format...")
screenshot_paths = []
for i in range(slide_count):
# ImageMagick generates slide_000.png, slide_001.png, etc.
source_file = f"slide_{i:03d}.png"
source_path = os.path.join(screenshots_dir, source_file)
# We need slide_1.png, slide_2.png, etc.
target_file = f"slide_{i+1}.png"
target_path = os.path.join(screenshots_dir, target_file)
if os.path.exists(source_path):
# Rename to expected format
shutil.move(source_path, target_path)
screenshot_paths.append(target_path)
print(f"✓ Renamed {source_file} to {target_file}")
else:
print(f"⚠ Warning: Expected file {source_file} not found, creating placeholder")
# Create empty placeholder
with open(target_path, 'w') as f:
f.write("")
screenshot_paths.append(target_path)
print(f"Successfully generated {len(screenshot_paths)} slide screenshots")
return screenshot_paths
return actual_pdf_path
except Exception as e:
# Re-raise the specific exceptions we've already handled
if "timed out" in str(e) or "failed:" in str(e):
raise
# Handle any other unexpected exceptions
raise Exception(f"Screenshot generation failed: {str(e)}")

View file

@ -4,6 +4,7 @@ import math
import os
import random
from typing import Annotated, List, Literal, Optional
import dirtyjson
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy import delete
@ -45,7 +46,10 @@ from utils.llm_calls.generate_presentation_structure import (
from utils.llm_calls.generate_slide_content import (
get_slide_content_from_type_and_outline,
)
from utils.ppt_utils import select_toc_or_list_slide_layout_index
from utils.ppt_utils import (
get_presentation_title_from_outlines,
select_toc_or_list_slide_layout_index,
)
from utils.process_slides import (
process_slide_add_placeholder_assets,
process_slide_and_fetch_assets,
@ -486,7 +490,9 @@ async def generate_presentation_api(
presentation_outlines_text += chunk
try:
presentation_outlines_json = json.loads(presentation_outlines_text)
presentation_outlines_json = dict(
dirtyjson.loads(presentation_outlines_text)
)
except Exception as e:
print(e)
raise HTTPException(
@ -575,6 +581,7 @@ async def generate_presentation_api(
content=request.content,
n_slides=request.n_slides,
language=request.language,
title=get_presentation_title_from_outlines(presentation_outlines),
outlines=presentation_outlines.model_dump(),
layout=layout_model.model_dump(),
structure=presentation_structure.model_dump(),
@ -676,7 +683,9 @@ async def edit_presentation_with_new_content(
slides_to_delete = []
for each_slide in slides:
updated_content = None
new_slide_data = list(filter(lambda x: x.index == each_slide.index, data.data))
new_slide_data = list(
filter(lambda x: x.index == each_slide.index, data.slides)
)
if new_slide_data:
updated_content = deep_update(each_slide.content, new_slide_data[0].content)
new_slides.append(
@ -718,7 +727,9 @@ async def derive_presentation_from_existing_one(
new_slides = []
for each_slide in slides:
updated_content = None
new_slide_data = list(filter(lambda x: x.index == each_slide.index, data.data))
new_slide_data = list(
filter(lambda x: x.index == each_slide.index, data.slides)
)
if new_slide_data:
updated_content = deep_update(each_slide.content, new_slide_data[0].content)
new_slides.append(

File diff suppressed because it is too large Load diff

View file

@ -10,5 +10,5 @@ class SlideContentUpdate(BaseModel):
class EditPresentationRequest(BaseModel):
presentation_id: uuid.UUID
data: List[SlideContentUpdate]
slides: List[SlideContentUpdate]
export_as: Literal["pptx", "pdf"] = "pptx"

View file

@ -11,6 +11,7 @@ dependencies = [
"anthropic>=0.60.0",
"asyncpg>=0.30.0",
"chromadb>=1.0.15",
"dirtyjson>=1.0.8",
"docling>=2.43.0",
"fastapi[standard]>=0.116.1",
"fastmcp>=2.11.0",

View file

@ -95,13 +95,19 @@ class DocumentsLoader:
def load_powerpoint(self, file_path: str) -> str:
return self.docling_service.parse_to_markdown(file_path)
def get_page_images_from_pdf(self, file_path: str, temp_dir: str):
@classmethod
def get_page_images_from_pdf(cls, file_path: str, temp_dir: str) -> List[str]:
with pdfplumber.open(file_path) as pdf:
images = []
for page in pdf.pages:
img = page.to_image(resolution=300)
img.save(os.path.join(temp_dir, f"page_{page.page_number}.png"))
img = page.to_image(resolution=150)
image_path = os.path.join(temp_dir, f"page_{page.page_number}.png")
img.save(image_path)
images.append(image_path)
return images
async def get_page_images_from_pdf_async(self, file_path: str, temp_dir: str):
@classmethod
async def get_page_images_from_pdf_async(cls, file_path: str, temp_dir: str):
return await asyncio.to_thread(
self.get_page_images_from_pdf, file_path, temp_dir
cls.get_page_images_from_pdf, file_path, temp_dir
)

View file

@ -50,7 +50,7 @@ class IconFinderService:
query_texts=[query],
n_results=k,
)
return [f"/static/icons/bold/{each}.png" for each in result["ids"][0]]
return [f"/static/icons/bold/{each}.svg" for each in result["ids"][0]]
ICON_FINDER_SERVICE = IconFinderService()

View file

@ -1,4 +1,5 @@
import asyncio
import dirtyjson
import json
from typing import AsyncGenerator, List, Optional
from fastapi import HTTPException
@ -554,7 +555,7 @@ class LLMClient:
)
if content:
if depth == 0:
return json.loads(content)
return dict(dirtyjson.loads(content))
return content
return None
@ -655,7 +656,7 @@ class LLMClient:
)
if text_content:
return json.loads(text_content)
return dict(dirtyjson.loads(text_content))
return None
async def _generate_anthropic_structured(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M216,112v16c0,53-88,88-88,112,0-24-88-59-88-112V112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M80,56h96a48,48,0,0,1,48,48v0a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8v0A48,48,0,0,1,80,56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M128,56V48a32,32,0,0,1,32-32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="136" cy="112" r="32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="72" x2="56" y2="72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="128" x2="56" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="184" x2="56" y2="184" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="48" width="192" height="160" rx="8" transform="translate(264 -8) rotate(90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M92,163.21a60,60,0,0,1,88,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="176" y1="160" x2="216" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="176" y1="96" x2="216" y2="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="40" width="176" height="176" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="108" cy="120" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M76,168c3.55-13.8,17.09-24,32-24s28.46,10.19,32,24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="176" y1="40" x2="176" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="72" x2="128" y2="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M104,24h48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="168 72 152 160 152 232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="88 72 104 160 104 232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M66.19,160a8,8,0,0,1-7.52-5.27l-26.18-72A8,8,0,0,1,40,72H216a8,8,0,0,1,7.52,10.73l-26.18,72a8,8,0,0,1-7.52,5.27Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M152,96l80,40v32l-80-16v32l16,16v32l-40-16L88,232V200l16-16V152L24,168V136l80-40V48a24,24,0,0,1,48,0Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="72" y1="216" x2="216" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M144,104h64a32,32,0,0,1,32,32v24H61.06a32,32,0,0,1-30.65-22.8L16.34,90.3A8,8,0,0,1,24,80h8l24,24H92.91L80.42,66.53A8,8,0,0,1,88,56h8Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="104" y1="216" x2="244" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M232,180,55.37,130.54A32,32,0,0,1,32,99.73V48a8,8,0,0,1,10.53-7.59L48,42.24,60,75.46,104,88V48a8,8,0,0,1,10.53-7.59L120,42.24l24,57.2,64.56,18A32,32,0,0,1,232,148.32Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="24" y1="216" x2="168" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M88,112.51,58.65,84a8,8,0,0,1,2.2-13.3L68,68l57.53,21.17,54.84-32.75a32,32,0,0,1,41,7.32L240,87.64l-147.41,88a32,32,0,0,1-38-4.32L18.53,136a8,8,0,0,1,2.32-13.19L24,121.27,55.79,132Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M144,100h64a32,32,0,0,1,32,32v24H61.06a32,32,0,0,1-30.65-22.8L16.34,86.3A8,8,0,0,1,24,76h8l24,24H92.91L80.42,62.53A8,8,0,0,1,88,52h8Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="208" cy="204" r="20"/><circle cx="112" cy="204" r="20"/></svg>

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M88,224l24-24V176l24-24,48,72,24-24-32-88,33-31A24,24,0,0,0,175,47L144,80,56,48,32,72l72,48L80,144H56L32,168l40,16Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polygon points="128 160 176 216 80 216 128 160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M52,192H48a16,16,0,0,1-16-16V64A16,16,0,0,1,48,48H208a16,16,0,0,1,16,16V176a16,16,0,0,1-16,16h-4" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="136" r="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="56" y1="32" x2="24" y2="64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="200" y1="32" x2="232" y2="64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="128 88 128 136 176 136" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M216,112c0,48.6-56,120-88,120S40,160.6,40,112a88,88,0,0,1,176,0Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><ellipse cx="92" cy="124" rx="16.97" ry="29.39" transform="translate(-60.74 101.37) rotate(-45)"/><ellipse cx="164" cy="124" rx="29.39" ry="16.97" transform="translate(-39.65 152.28) rotate(-45)"/><line x1="116" y1="184" x2="140" y2="184" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="216" y1="216" x2="40" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="122" y="98" width="104" height="52" rx="8" transform="translate(50 298) rotate(-90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="56" y="32" width="52" height="144" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="56" y1="232" x2="200" y2="232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="88" y="32" width="80" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="32" x2="128" y2="52" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="204" x2="128" y2="224" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="108" x2="128" y2="148" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="64" y="52" width="128" height="56" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="148" width="176" height="56" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="48" x2="128" y2="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="168" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="88" width="176" height="80" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="224" y1="128" x2="204" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="52" y1="128" x2="32" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="148" y1="128" x2="108" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="112" y="100" width="128" height="56" rx="8" transform="translate(304 -48) rotate(90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="52" y="40" width="56" height="176" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="88" y="40" width="80" height="176" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="208" y1="128" x2="168" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="88" y1="128" x2="48" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="40" y1="40" x2="40" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="80" y="56" width="104" height="52" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="80" y="148" width="144" height="52" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="32" y1="56" x2="32" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="72" y="88" width="160" height="80" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="216" y1="40" x2="216" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="72" y="56" width="104" height="52" rx="8" transform="translate(248 164) rotate(180)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="32" y="148" width="144" height="52" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="224" y1="56" x2="224" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="24" y="88" width="160" height="80" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="216" y1="40" x2="40" y2="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="122" y="106" width="104" height="52" rx="8" transform="translate(306 -42) rotate(90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="56" y="80" width="52" height="144" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="56" y1="32" x2="200" y2="32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="88" y="72" width="80" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M240,168l-16,16s-36,40-96,40-96-40-96-40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="208 168 240 168 240 200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="124" cy="132" r="44" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M168,176V84A44,44,0,0,0,87.12,60" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="192" cy="192" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="80" cy="192" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="168" y1="192" x2="104" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M56,192H32a8,8,0,0,1-8-8V72a8,8,0,0,1,8-8H184V169.37" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="92" y1="120" x2="132" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="112" y1="100" x2="112" y2="140" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="184" y1="120" x2="244" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M184,80h38.58A8,8,0,0,1,230,85l14,35v64a8,8,0,0,1-8,8H216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="232" x2="128" y2="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="128" cy="56" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="88" y1="120" x2="168" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M216,144c0,64-88,24-88,88,0-64-88-24-88-88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="64" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="216" x2="128" y2="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M56,120H32a96,96,0,0,0,192,0H200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="164" cy="148" r="16"/><circle cx="92" cy="148" r="16"/><path d="M24,184V161.13C24,103.65,70.15,56.2,127.63,56A104,104,0,0,1,232,160v24a8,8,0,0,1-8,8H32A8,8,0,0,1,24,184Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="48" x2="63.07" y2="79.07" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="224" y1="48" x2="193.1" y2="78.9" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="72 32 72 200 240 200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="72" x2="72" y2="72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M112,72.33A96,96,0,0,1,199.67,160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polygon points="128 232 208 192 224 72 128 32 32 72 48 192 128 232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="168 160 128 88 88 160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="99.11" y1="140" x2="156.89" y2="140" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="128" r="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="32" x2="164.68" y2="134.7" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="44.86" y1="80" x2="152.14" y2="99.58" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="44.86" y1="176" x2="115.46" y2="92.89" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="224" x2="91.32" y2="121.3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="211.14" y1="176" x2="103.86" y2="156.42" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="211.14" y1="80" x2="140.54" y2="163.11" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="104" y1="32" x2="128" y2="72.62" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="152" y1="32" x2="76.36" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="179.64" y1="160" x2="232" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="151.23" y1="111.93" x2="208" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="24" y1="160" x2="133.15" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="52.73" y1="200" x2="48" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="32" y="48" width="192" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="76" cy="92" r="16"/><circle cx="124" cy="92" r="16"/></svg>

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M216,73.52C204.53,62.66,185,56,168,56a63.72,63.72,0,0,0-40,14h0A63.71,63.71,0,0,0,88.88,56C52,55.5,23.06,86.3,24,123.19a119.62,119.62,0,0,0,37.65,84.12A31.92,31.92,0,0,0,83.6,216h87.7a31.75,31.75,0,0,0,23.26-10c15.85-17,21.44-33.2,21.44-33.2h0c-16.79-11.53-24-30.87-24-52.78,0-18.3,11.68-34.81,24-46.48Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M148,12a32.12,32.12,0,0,0-9.77,8.37" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M131.23,224h-6.46a16,16,0,0,1-15.62-12.45l-12.74-56A16,16,0,0,1,112,136H144a16,16,0,0,1,15.62,19.55l-12.74,56A16,16,0,0,1,131.23,224Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="128" cy="108" r="28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M65.74,201.08a96,96,0,1,1,124.52,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M40,161.61c72-59.69,104,56.47,176-3.22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M40,97.61c72-59.69,104,56.47,176-3.22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="24" y="56" width="208" height="40" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M216,96v96a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="104" y1="136" x2="152" y2="136" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="92" y1="136" x2="164" y2="136" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M164,164V128a40,40,0,1,1,44,39.8V200a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V167.8A40,40,0,1,1,92,128v36" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M48,88.2V72A32,32,0,0,1,80,40h96a32,32,0,0,1,32,32V88.2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="88 152 24 152 24 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M224,184A96,96,0,0,0,60.12,116.12L24,152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="168 152 232 152 232 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M32,184a96,96,0,0,1,163.88-67.88L232,152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="80 152 32 104 80 56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="136 152 88 104 136 56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M224,200a96,96,0,0,0-96-96H88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>

After

Width:  |  Height:  |  Size: 532 B

Some files were not shown because too many files have changed in this diff Show more